老王的技术 2019-06-21
当下抖音非常火热,你是不是也很心动做一个类似的 App?
短视频内容生产
优质短视频内容的产生依赖于短视频的采集和特效编辑,这就要求在进行抖音 App 开发时,用到基础的美颜、混音、滤镜、变速、图片视频混剪、字幕等功能。
在这些功能基础上,进行预处理,结合 OpenGL、AI、AR 技术,产生很多有趣的动态贴纸玩法,使得短视频内容更具创意。
视频录制的大致实现流程是先由 Camera 、 AudioRecord 进行最原始的相机画面以及声音的采集,然后将采集的数据进行滤镜、降噪等前处理,处理完成后由 MediaCodec 进行硬件编码,最后采用 MediaMuxer 生成最终的 MP4 文件。
短视频处理播放
视频的处理和播放主要是视频的清晰度、观看流畅度方面的体验。在这方面来讲,可以采用“窄带高清”技术,在节省码率的同时能够提供更加清晰的观看体验,经过测试,同等视频质量下最高可以节省 20-40% 带宽。
除了带宽之外,短视频内容的存储和 CDN 优化也尤为重要,通常我们需要上传到云存储服务器的内容是短视频内容和封面内容。
而 CDN 优化带给短视频平台的则是进一步的短视频首次载入和循环播放方面的体验。
比如针对首播慢的问题,像阿里云播放器支持 QUIC 协议,基于 CDN 的调度,可以使短视频首次播放秒开的成功率达到 98%。
此外在循环播放时还可以边播放边缓存,用户反复观看某一短视频时就不用耗费流量了。
录制视频的方式
在 Android 系统当中,如果需要一台 Android 设备来获取到一个 MP4 这样的视频文件的话,主流的方式一共与三种:
MediaRecorder:是 Android 系统直接提供给我们的录制类,用于录制音频和视频的一个类,简单方便。
它不需要理会中间录制过程,结束录制后可以直接得到音频文件进行播放,录制的音频文件是经过压缩的,需要设置编码器,录制的音频文件可以用系统自带的播放器播放。
优点:大部分以及集成,直接调用相关接口即可,代码量小,简单稳定;缺点:无法实时处理音频;输出的音频格式不是很多。
MediaCodec+MediaMuxer:MediaCodec 与 MediaMuxer 结合使用同样能够实现录制的功能。
MediaCodec 是 Android 提供的编解码类,MediaMuxer 则是复用类(生成视频文件)。
从易用性的角度上来说肯定不如 MediaRecorder,但是允许我们进行更加灵活的操作,比如需要给录制的视频添加水印等各种效果。
优点: 与 MediaRecorder 一样低功耗速度快,并且更加灵活;缺点: 支持的格式有限,兼容性问题。
FFmpeg:FFmpeg(Fast forword mpeg,音视频转换器)是一个开源免费跨平台的视频和音频流方案,它提供了录制/音视频编解码、转换以及流化音视频的完整解决方案。
主要的作用在于对多媒体数据进行解协议、解封装、解码以及转码等操作。
优点:格式支持非常的强,十分的灵活,功能强大,兼容性好;缺点:C语言些的音视频编解码程序,使用起来不是很方便。
虽然从数据看来 FFmpeg 是最好的,但是我们得首先排除这种,因为他的易用性是最差的。
其次,MediaRecorder 也是需要排除的,所以在这里我比较推荐 MediaCodec+MediaMuxer 这种方式。
编码器参数
码率:数据传输时单位时间传送的数据位数,KBPS:千位每秒。码率和质量成正比,也和文件体积成正比。码率超过一定数值,对图像的质量没有多大的影响。
帧数:每秒显示多少个画面,FPS。
关键帧间隔:在 H.264 编码中,编码后输出的压缩图像数据有多种,可以简单的分为关键帧和非关键帧。
关键帧能够进行独立解码,看成是一个图像经过压缩的产物。而非关键帧包含了与其他帧的“差异”信息,也可以称呼为“参考帧”,它的解码需要参考关键帧才能够解码出一个图像。非关键帧拥有更高的压缩率。
MediaCodec+MediaMuxer 的使用
MediaMuxer 和 MediaCodec 这两个类,它们的参考文:
里边有使用的框架。这个组合可以实现很多功能,比如音视频文件的编辑(结合 MediaExtractor),用 OpenGL 绘制 Surface 并生成 MP4 文件,屏幕录像以及类似 Camera App 里的录像功能(虽然这个用 MediaRecorder 更合适)等。
它们一个是生成视频,一个生成音频,这里把它们结合一下,同时生成音频和视频。
基本框架和流程如下:
首先是录音线程,主要参考 HWEncoderExperiments。通过 AudioRecord 类接收来自麦克风的采样数据,然后丢给 Encoder 准备编码:
AudioRecord audio_recorder; audio_recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, buffer_size); // ... audio_recorder.startRecording(); while (is_recording) { byte[] this_buffer = new byte[frame_buffer_size]; read_result = audio_recorder.read(this_buffer, 0, frame_buffer_size); // read audio raw data // … presentationTimeStamp = System.nanoTime() / 1000; audioEncoder.offerAudioEncoder(this_buffer.clone(), presentationTimeStamp); // feed to audio encoder }
这里也可以设置 AudioRecord 的回调(通过 setRecordPositionUpdateListener())来触发音频数据的读取。
offerAudioEncoder() 里主要是把 Audio 采样数据送入音频 MediaCodec 的 InputBuffer 进行编码:
ByteBuffer[] inputBuffers = mAudioEncoder.getInputBuffers(); int inputBufferIndex = mAudioEncoder.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(this_buffer); ... mAudioEncoder.queueInputBuffer(inputBufferIndex, 0, this_buffer.length, presentationTimeStamp, 0); }
下面,参考 Grafika-SoftInputSurfaceActivity,并加入音频处理。主循环大体分四部分:
try { // Part 1 prepareEncoder(outputFile); ... // Part 2 for (int i = 0; i < NUM_FRAMES; i++) { generateFrame(i); drainVideoEncoder(false); drainAudioEncoder(false); } // Part 3 ... drainVideoEncoder(true); drainAudioEncoder(true); } catch (IOException ioe) { throw new RuntimeException(ioe); } finally { // Part 4 releaseEncoder(); }
第 1 部分是准备工作,除了 Video 的 MediaCodec,这里还初始化了 Audio 的 MediaCodec:
MediaFormat audioFormat = new MediaFormat(); audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100); audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); ... mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE); mAudioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mAudioEncoder.start();
第 2 部分进入主循环,App 在 Surface 上直接绘图,由于这个 Surface 是从 MediaCodec 中用 createInputSurface() 申请来的,所以画完后不用显式用 queueInputBuffer() 交给 Encoder。
drainVideoEncoder() 和 drainAudioEncoder() 分别将编码好的音视频从 Buffer 中拿出来(通过 dequeueOutputBuffer()),然后交由 MediaMuxer 进行混合(通过 writeSampleData())。
注意音视频通过 PTS(Presentation Time Stamp,决定了某一帧的音视频数据何时显示或播放)来同步,音频的 time stamp 需在 AudioRecord 从 MIC 采集到数据时获取并放到相应的 bufferInfo 中。
视频由于是在 Surface 上画,因此直接用 dequeueOutputBuffer() 出来的 bufferInfo 中的就行,最后将编码好的数据送去 MediaMuxer 进行多路混合。
注意这里 Muxer 要等把 audio track 和 video track 都加入了再开始。
MediaCodec 在一开始调用 dequeueOutputBuffer() 时会返回一次 INFO_OUTPUT_FORMAT_CHANGED消息。
我们只需在这里获取该 MediaCodec 的 format,并注册到 MediaMuxer 里。
接着判断当前 audio track 和 video track 是否都已就绪,如果是的话就启动 Muxer。
总结来说,drainVideoEncoder() 的主逻辑大致如下,drainAudioEncoder 也是类似的,只是把 video 的 MediaCodec 换成 audio 的 MediaCodec 即可:
while(true) { int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { ... } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { encoderOutputBuffers = mVideoEncoder.getOutputBuffers(); } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat newFormat = mAudioEncoder.getOutputFormat(); mAudioTrackIndex = mMuxer.addTrack(newFormat); mNumTracksAdded++; if (mNumTracksAdded == TOTAL_NUM_TRACKS) { mMuxer.start(); } } else if (encoderStatus < 0) { ... } else { ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; ... if (mBufferInfo.size != 0) { mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo); } mVideoEncoder.releaseOutputBuffer(encoderStatus, false); if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } } }
第 3 部分是结束录制,发送 EOS 信息,这样在 drainVideoEncoder() 和 drainAudioEncoder 中就可以根据 EOS 退出内循环。
第 4 部分为清理工作。把 audio 和 video 的 MediaCodec,MediaCodec 用的 Surface 及 MediaMuxer 对象释放。
最后几点注意:
总结