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

阅读数:65 2019 年 10 月 10 日 18:27

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

随着智能手机的普及和网络传输技术的发展,人类的传播媒介正在由图文向视频过度。短视频作为一种底层的内容形态,可以渗入到任何领域、任何行业的 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

评论

发布