写点什么

怎样实现一款「视频录制」应用?

  • 2019-10-10
  • 本文字数:3697 字

    阅读完需:约 12 分钟

怎样实现一款「视频录制」应用?

随着智能手机的普及和网络传输技术的发展,人类的传播媒介正在由图文向视频过度。短视频作为一种底层的内容形态,可以渗入到任何领域、任何行业的 App 中。如今,例如社交、金融、电子商务、政务民生等相当一部分的 APP 都在持续集成这一功能。


相比图片和文字,视频所传递的信息更为具象和丰富,视频录制正从「专业短视频制作功能」演变为「一个基础功能」,从而嵌套在各个 APP 的使用场景里。


那么对于有「视频录制」需求的 APP 来说,要怎样实现这一功能呢?在这里,主要以七牛短视频 SDK 中的 Android 平台为例来简单讨论一下此功能的实现方法(当然感兴趣的伙伴也可以直接拉到文末,扫描二维码进行体验)。


需求分析可以更好地辅助落地执行的实施,所以在下手之前,可以先对「视频录制」这样一个需求做一下简单的分析。从功能性需求点上,可以分为以下几块:


1.核心需求


  • 摄像头拍摄视频

  • 麦克风采集音频

  • 能够预览

  • 编码压缩

  • 能够在本地保存为一个 mp4 文件


2.控制型需求


  • 能够控制摄像头拍摄,支持曝光度,闪光灯,前后摄像头切换,画面对焦等功能

  • 能够控制麦克风采集,包括声道数,采样率,音频格式等参数

  • 能够控制最终输出视频的分辨率,码率,FPS 等参数


3.开放型需求


  • 能够支持一些第三方的视频特效(美颜特效,AR 特效等)

  • 能够支持一些第三方的音频特效(变声等)


4.性能和兼容性需求


  • 整个过程的耗时不可太长

  • 能够覆盖尽可能多的 Android 机型


5.高级需求


  • 支持录制时增加背景音乐混音

  • 支持分段拍摄


从以上归纳可以看出,除了核心需求外,在大家的使用场景中,潜在的需求是比较多的。需求决定着架构的设计,只有真正理清楚需求之后,才能去谈如何设计。


比如,NO.3 就决定着我们应该有丰富的回调接口,能够把视频或者音频数据回调给外部,从而满足多样化的二次开发。NO.4 则需要七牛云同时支持硬编码和软编码来减少时耗和增加兼容性。而 NO.5 则是一些比较高级的功能,是一些发散的需求,可能会发散出很多的玩法,这就要求我们对需求的发展及外延有一定的预判能力。


从逻辑层面来说,我们整体的架构图可以是这样的:



宏观来讲,整个过程可分为数据的采集、处理、编码、封装和输出这几个部分。接下来咱们分别简单讨论一下每一个模块大致是如何实现的。

采集模块

采集模块是整个数据的输入源头。对于 Android 平台来说,视频和音频的采集模块主要是用 Camera 和 AudioRecord 来分别实现的。


Camera 能够分别回调 YUV 和纹理两种形式的数据,其相应的方法分别如下:


// 从摄像头回调 YUV 数据void Camera.setPreviewCallbackWithBuffer(PreviewCallback cb);// 从摄像头回调纹理数据void Camera.setPreviewTexture(SurfaceTexture surfaceTexture);
复制代码


这两种数据分别由 CPU 和 GPU 来处理,我们主要用纹理来传递数据,以帮助客户减少耗时。当把一个 SurfaceTexture 作为 Camera 的预览目标,Camera 则会把 SurfaceTexture 创建的 Surface 作为一个输出源,我们通过调用 updateTexImage() 方法,从摄像机采集的图片流中取得每帧图片的纹理。


这里需要说明的是,从 Camera 中获取的纹理并不是我们常用的 GL_TEXTURE_2D 类型,而是 GL_TEXTURE_EXTERNAL_OES 类型,所以我们对纹理设置参数也要使用 GL_TEXTURE_EXTERNAL_OES 类型。同样的,在 shader 中也需要使用 samplerExternalOES 采样方式来声明纹理,如在 FragmentShader 的代码如下:


public static final String TEXTURE_EXTERNAL_FS =        "#extension GL_OES_EGL_image_external : require\n" +        "precision mediump float;\n" +        "uniform samplerExternalOES u_tex;\n" +        "varying vec2 v_tex_coord;\n" +        "void main() {\n" +        "  gl_FragColor = texture2D(u_tex, v_tex_coord);\n" +        "}\n";
复制代码


音频采集则相对简单一些,AudioRecord 的关键使用方法如下:


// AudioRecord 的构造函数,我们可以把一些配置相关的参数传递进去
AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes);// 创建好了AudioRecord实例之后,通过该方法开始麦克风采集void AudioRecord.startRecording();// 在采集的过程中,通过该方法不断的从缓冲区循环提取 PCM 数据int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes);// 停止采集,释放资源void AudioRecord.stop();
复制代码

处理模块

处理模块可以分为「外部处理」和「内部处理」两部分,外部处理是指对第三方合作伙伴的可扩展,内部处理是我们自有的处理逻辑。


拿到摄像机和麦克风采集的数据之后,首先要把数据回调给最外层。当第三方的合作伙伴拿到我们传出的纹理或者 PCM 数据,就可在此基础上做一些特效处理,如音频相关的变声特效、视频相关的人脸识别、美颜、AR、滤镜等等,继而把处理完的数据再次返回给我们。当数据进到的内部处理模块之后,又可以根据自身的处理业务来进行二次处理,例如纹理的裁剪,旋转或者音频相关的混音。


层与层之间的数据传输是通过回调接口来实现的,我们可以设计如下接口来实现视频和音频的数据传输:


// 视频 yuv 数据回调public interface VideoYUVFrameListener { boolean onVideoFrameAvailable(byte[] data, int width, int height, int rotation, int fmt, long timestampNs);}
// 视频纹理数据回调public interface VideoTextureFrameListener { void onSurfaceCreated(); void onSurfaceChanged(int width, int height); void onSurfaceDestroy(); int onVideoFrameAvailable(int texId, int texWidth, int texHeight, long timestampNs, float[] transformMatrix);}
// 音频 PCM 数据回调public interface AudioFrameListener { void onAudioFrameAvailable(byte[] data, long timestampNs);}
复制代码


视频处理模块的主要原理是通过回调机制,用 OpenGL 把相应的特效离屏渲染到纹理上,进而把纹理进行裁剪,缩放,旋转等操作。作为「一张纹理的艺术之旅」,经过如此层层处理后,最终的纹理囊括了各层处理的效果之和。而相比于视频,音频处理模块的主要原理是通过重采样、混音、或一些 3A 算法直接对 PCM 数据作修改。

预览编码模块

视频帧处理完成之后,我们需要把纹理数据传递给一个 SurfaceView 用于预览。此时,上一阶段对纹理的处理结果便可以在此 SurfaceView 上表现出来。另外,我们还需要把该纹理数据传递给视频编码器进行编码。预览和编码虽然是两个线程,但是却共享一个纹理,如此会减少资源的占用,帮助效率的提高。


为了适配更多的 Android 机型,系统在支持 MediaCodec 的同时,也要支持 x264 软编,不过最主要的编码方式应该还是以硬编码为主,原因是硬编码在时耗上要远远优于软编码。在硬编模式下,整个拍摄模块核心实现图如下所示:



可以看出,整个流程是生产者/消费者模式,摄像机首先作为数据的生产者向外提供数据,数据会输出到一个 Surface 上,此 Surface 即 SurfaceTexture 内部创建的。与此同时,我们会通过 SurfaceTexture 源源不断的获取数据,接着把数据通过 GLES 分别渲染到 SurfaceView 的 Surface 和 MediaCodec 的 Surface 中。


下一步, SurfaceFlinger 作为消费者,负责把 SurfaceView 的 Surface 中的数据输出到屏幕。同理,MediaServer 作为消费者,负责把 MediaCodec 的 Surface 中的数据输出到编码器进行编码。


以上是视频编码的方式,相较于视频编码,音频编码则简单的多,我们只需对编码器设定编码参数后持续向编码器输入 PCM 数据即可,编码器会把编码后的数据回调给开发者。

封装和输出模块

mp4 的封包可以用 MediaMuxer 来实现,但从兼容性上来考虑,最好用 FFmpeg 来封包。例如,倘若编码出来的视频带有 B 帧,那么如下图所示,MediaMuxer 仅仅在 Android 7.0 以上才能够支持。



无论是使用 MediaMuxer 抑或 FFmpeg 来封包,最终都会在本地输出一个视频文件,至此大家即完成了从拍摄视频到最终输出的整个流程。


综上流程,我们实现「视频录制」中主要用到的 API 如下图所示:



可以看到,多媒体开发和 APP 开发有所不同,主要用到的是更偏底层的一些 API。除此之外,还需要对音视频的编码标准,常用格式,FFmpeg,OpenGL 等知识有一定的了解,所以开发的门槛还是相对高的。


正是因为有这些门槛,中小型团队、创业初阶段的公司、或不把视频拍摄作为核心业务的团队实在无需像这样从 0 到 1 的重量化地造轮子。这就譬如厨师无需从除草、耕作、施肥、种菜都一一亲力亲为,而是需要把时间和精力用来钻研食材的味道和烹饪本身。


同样的道理,开发者们选择直接用一款适合自己公司的音视频 SDK 其实是效率最高的做法。七牛云短视频 SDK 包含量以上所有需求的实现,帮助 B 端用户节省时间成本和人力成本,帮助用户专注精力于自己的核心业务。在 SDK 的使用上,七牛云大大简化了接入流程,力求缩短从想法到产品的距离。


自从 2017 年上线以来,七牛云已经帮助数以千计的客户快速地集成音视频能力,受到了业内合作伙伴和客户的高度评价。未来,七牛云短视频 SDK 将持续在音视频领域深耕,为更多的客户提供优质的解决方案。


本文转载自公众号七牛云(ID:qiniutek)。


原文链接:


https://mp.weixin.qq.com/s/c4XiFNs66Qa5Yyb3k2zhbw


2019-10-10 18:27743

评论

发布
暂无评论
发现更多内容

架构师训练营第六周作业

郎哲158

极客大学架构师训练营

第六周总结

Geek_ac4080

架构师训练营第六周总结

月殇

极客大学架构师训练营

架构师训练营—第五周学习总结

Geek_shu1988

分布式CAP原理

Jacky.Chen

架构师训练营—第六周学习总结

Geek_shu1988

架构师训练营—第六周作业

Geek_shu1988

week06作业

龙卷风

架构师一期

CAP原理简述及应用

博古通今小虾米

CAP

第六周作业 (作业一)

Geek_83908e

极客大学架构师训练营

技术选型二第六周作业「架构师训练营第 1 期」

天天向善

架构师训练营 Week6 - 技术选型 - 分布式数据库,NoSQL,Zookeeper,搜索引擎

极客大学架构师训练营

第六周作业

wanlinwang

极客大学架构师训练营

架构师训练营第 1 期 -Week6 - 课后练习

鲁大江

极客大学架构师训练营

技术选型(1) 课后作业

ABS

架构师训练营—第五周作业

Geek_shu1988

架构师训练营第六周作业

月殇

极客大学架构师训练营

架构师训练营第六周课程笔记及心得

Airs

架构师训练营 week2 学习总结

花果山

极客大学架构师训练营

第六周-CAP原则理解

袭望

Week2 框架设计

贺志鹏

极客大学架构师训练营

doris临时故障恢复图

happy

week06学习总结

龙卷风

架构师一期

架构师训练营 第六周学习总结

郎哲158

极客大学架构师训练营

架构一期第六周作业

Airs

架构师训练营第 2 期 第二周作业 1

月下独酌

Week_06 总结+作业

golangboy

极客大学架构师训练营

架构师训练营第 1 期 -Week6 - 技术选型二学习总结

鲁大江

极客大学架构师训练营

架构师训练营 - 第 6周课后作业-CAP原理

树森

周练习 6

何毅曦

思考 - 从传统雪崩到K8S

东风微鸣

k8s

怎样实现一款「视频录制」应用?_移动_七牛云_InfoQ精选文章