简介 在学习了Android 音视频的基本的相关知识,并整理了相关的API之后,应该对基本的音视频有一定的轮廓了。
下面是一个Android音视频中相当重要的几个API:MediaCodec (音视频编解码)、MediaExtractor (音视频解封装)、MediaMuxer (音视频封装)。
学习这个API的时候,主要的方向为:
学习 MediaCodec API,完成音频 AAC 硬编、硬解 学习 MediaCodec API,完成视频 H.264 的硬编、硬解 MediaCodec类可以用于使用一些基本的多媒体编解码器(音视频编解码组件),它是Android基本的多媒体支持基础架构的一部分通常和MediaExtractor (解封装音视频), MediaSync (音视频同步), MediaMuxer (封装音视频), MediaCrypto (解码加密的媒体数据), MediaDrm (解密DRM), Image, Surface, AudioTrack 一起使用。
一个编解码器 可以处理输入 的数据来产生输出 的数据,编解码器使用一组输入和输出缓冲器 来异步 处理数据。你可以创建 一个空的输入缓冲区,填充 数据后发送 到编解码器进行处理。编解码器使用输入的数据进行转换 ,然后输出 到一个空的输出缓冲区。最后你获取 到输出缓冲区的数据,消耗掉里面的数据,释放 回编解码器。如果后续还有数据需要继续处理,编解码器就会重复这些操作。输出流程如下:
编解码器的生命周期:
主要的生命周期为:Stopped、Executing、Released。
Stopped的状态下也分为三种子状态:Uninitialized、Configured、Error。 Executing的状态下也分为三种子状态:Flushed, Running、End-of-Stream。 生命周期图:同步(左)与异步(右)
MediaCodec可以处理具体的视频流,主要有这几个方法:
getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组 queueInputBuffer:输入流入队列 dequeueInputBuffer:从输入流队列中取数据进行编码操作 getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组 dequeueOutputBuffer:从输出队列中取出编码操作之后的数据 releaseOutputBuffer:处理完成,释放ByteBuffer数据 流控基本概念:
流控就是流量控制。涉及到了 TCP 和视频编码:对 TCP 来说就是控制单位时间内发送数据包的数据量,对编码来说就是控制单位时间内输出数据的数据量。
TCP 的限制条件是网络带宽,流控就是在避免造成或者加剧网络拥塞的前提下,尽可能利用网络带宽。带宽够、网络好,我们就加快速度发送数据包,出现了延迟增大、丢包之后,就放慢发包的速度(因为继续高速发包,可能会加剧网络拥塞,反而发得更慢)。 视频编码的限制条件最初是解码器的能力,码率太高就会无法解码,后来随着 codec 的发展,解码能力不再是瓶颈,限制条件变成了传输带宽/文件大小,我们希望在控制数据量的前提下,画面质量尽可能高。 无论是要发送的 TCP 数据包,还是要编码的图像,都可能出现“尖峰”,也就是短时间内出现较大的数据量。TCP 面对尖峰,可以选择不为所动(尤其是网络已经拥塞的时候),这没有太大的问题,但如果视频编码也对尖峰不为所动,那图像质量就会大打折扣了。如果有几帧数据量特别大,但仍要把码率控制在原来的水平,那势必要损失更多的信息,因此图像失真就会更严重。 Android 硬编码流控:
MediaCodec 流控相关的接口并不多,一是配置时设置目标码率和码率控制模式,二是动态调整目标码率(Android 19 版本以上)。
配置时指定目标码率和码率控制模式
1 2 3 mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); mVideoCodec.configure(mediaFormat, null , null , MediaCodec.CONFIGURE_FLAG_ENCODE);
码率控制模式有三种:
CQ 表示完全不控制码率,尽最大可能保证图像质量; CBR 表示编码器会尽量把输出码率控制为设定值,即我们前面提到的“不为所动”; VBR 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低; Android 流控策略选择:
质量要求高、不在乎带宽、解码器支持码率剧烈波动的情况下,可以选择 CQ 码率控制策略。 VBR 输出码率会在一定范围内波动,对于小幅晃动,方块效应会有所改善,但对剧烈晃动仍无能为力;连续调低码率则会导致码率急剧下降,如果无法接受这个问题,那 VBR 就不是好的选择。 CBR 的优点是稳定可控,这样对实时性的保证有帮助。所以 WebRTC 开发中一般使用的是CBR。 MediaCodec常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 createEncoderByType(@NonNul String type):静态构造方法,type为指定的音视频格式,创建指定格式的编码器 createDecoderByType(@NonNull String type):静态构造方法,type为指定的音视频格式,创建指定格式的解码器 configure( @Nullable MediaFormat format, @Nullable Surface surface, @Nullable MediaCrypto crypto, @ConfigureFlag int flags) int dequeueInputBuffer (long timeoutUs) queueInputBuffer( int index, int offset, int size, long presentationTimeUs, int flags ) int dequeueOutputBuffer ( @NonNull BufferInfo info, // 这个BufferInfo需要自己手动创建,调用后,会把该索引的数据的信息写在里面 long timeoutUs // 等待时间 ) releaseOutputBuffer(int index, boolean render):释放指定索引位置的buffer
MediaCodec是系统级别的编解码库,底层还是调用native方法,使用MediaCodec的基本流程是:
创建与文件相匹配的MediaCodec -> MediaCodec写入数据,进行编码/解码 -> 读取MediaCodec编/解码结果
【音频MediaCodec的具体使用详见音频篇编解码】
MediaExtractor字面意思是多媒体提取器,它在Android的音视频开发里主要负责提取视频或者音频中的信息和数据流(例如将视频文件,解析头信息,剥离出音频与视频)。
setDataSource(String path):即可以设置本地文件又可以设置网络文件 getTrackCount():得到源文件通道数 getTrackFormat(int index):获取指定(index)的通道格式 getSampleTime():返回当前的时间戳 readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的数据按偏移量读取到ByteBuffer中; advance():读取下一帧数据 release(): 读取结束后释放资源 通过setDataSource()设置数据源,数据源可以是本地文件地址,也可以是网络地址
1 2 MediaExtractor mVideoExtractor = new MediaExtractor ();mVideoExtractor.setDataSource(mVideoPath);
可以通过getTrackFormat(int index)来获取各个track的MediaFormat,通过MediaFormat来获取track的详细信息,如:MimeType、分辨率、采样频率、帧率等等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 for (int i = 0 ; i < mVideoExtractor.getTrackCount(); i++) { MediaFormat format = mVideoExtractor.getTrackFormat(i); } MediaFormat mediaFormat = extractor.getTrackFormat(0 );String mimeFormat = mediaFormat.getString(MediaFormat.KEY_MIME);Log.e(TAG, "mediaExtractor: 获取MIME格式内容=" +mimeFormat); MediaFormat mediaFormat = extractor.getTrackFormat(0 );String language = mediaFormat.getString(MediaFormat.KEY_LANGUAGE);Log.e(TAG, "mediaExtractor: 获取语言格式内容=" +language); MediaFormat mediaFormat = extractor.getTrackFormat(0 );int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);long durationTime = mediaFormat.getLong(MediaFormat.KEY_DURATION);int maxByteCount = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);int bitRate = mediaFormat.getInteger(MediaFormat.KEY_BIT_RATE);int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);int maxWidth = mediaFormat.getInteger(MediaFormat.KEY_MAX_WIDTH);int maxHeight = mediaFormat.getInteger(MediaFormat.KEY_MAX_HEIGHT);int colorFormat = mediaFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);int frameRate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
获取到track的详细信息后,通过selectTrack(int index)选择指定的通道
1 2 3 4 if (format.getString(MediaFormat.KEY_MIME).startsWith("video/" )) { mVideoExtractor.selectTrack(i); break ; }
指定通道之后就可以从MediaExtractor中读取数据了
1 2 3 4 5 6 7 8 while (true ) { int sampleSize = mVideoExtractor.readSampleData(buffer, 0 ); if (sampleSize < 0 ) { break ; } mVideoExtractor.advance(); }
在读取结束之后,记得释放资源
1 mVideoExtractor.release();
利用MediaExtractor提取的aac和.h264文件不经过处理没办法播放,需要MediaMuxer合并生成可以播放的文件(aac文件和.h264需要首先利用MediaMuxer生成MP4文件,才能进行合并)。
MediaMuxer从api18开始提供,可以封装编码后的视频流和音频流到视频文件中。目前MediaMuxer支持的文件输出格式包括MP4,webm和3gp。
MediaMuxer(String path, int format) addTrack(MediaFormat format):利用MediaFormat添加音频或视频轨道 release():释放MediaMuxer的资源 setLocation(float latitude,float longitude):设置并存储地理位置信息到生成文件中 setOrientationHint(int degrees):设置输出视频回放的方向提示 start():开始muxer,等待数据的输入 stop():停止muxer,调用这个函数后将生成合成的文件 writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):往muxer中写入编码的数据。 生成MediaMuxer对象:
通过new MediaMuxer(String path, int format)指定视频文件输出路径和文件格式: MediaMuxer mMediaMuxer = new MediaMuxer(mOutputVideoPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
addTrack
addTrack(MediaFormat format),添加媒体通道,传入MediaFormat对象,通常从MediaExtractor或者MediaCodec中获取,也可以自己创建 addTrack会返回trackindex
调用start函数
MediaMuxer.start();
写入数据
调用MediaMuxer.writeSampleData()向mp4文件中写入数据了。每次只能添加一帧视频数据或者单个Sample的音频数据,需要BufferInfo对象作为参数。 BufferInfo info = new BufferInfo(); info.offset = 0; info.size = sampleSize; info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME; info.presentationTimeUs = mVideoExtractor.getSampleTime(); mMediaMuxer.writeSampleData(videoTrackIndex, buffer, info); info.size 必须填入数据的大小 info.flags 需要给出是否为同步帧/关键帧 info.presentationTimeUs 必须给出正确的时间戳,注意单位是 us,第二次getSampleTime()和首次getSampleTime()的时间差。
释放关闭资源
结束写入后关闭以及释放资源: MediaMuxer.stop(); MediaMuxer.release();
官网say that it is generally used like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 MediaMuxer muxer = new MediaMuxer ("temp.mp4" , OutputFormat.MUXER_OUTPUT_MPEG_4);MediaFormat audioFormat = new MediaFormat (...);MediaFormat videoFormat = new MediaFormat (...);int audioTrackIndex = muxer.addTrack(audioFormat);int videoTrackIndex = muxer.addTrack(videoFormat);ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);boolean finished = false ;BufferInfo bufferInfo = new BufferInfo (); muxer.start(); while (!finished) { finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo); if (!finished) { int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex; muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo); } }; muxer.stop(); muxer.release();
综合例子 视频换音 使用MediaExtractor和MediaMuxer来实现视频的换音
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 private void muxingAudioAndVideo () throws IOException { MediaMuxer mMediaMuxer = new MediaMuxer (mOutputVideoPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); MediaExtractor mVideoExtractor = new MediaExtractor (); mVideoExtractor.setDataSource(mVideoPath); int videoTrackIndex = -1 ; for (int i = 0 ; i < mVideoExtractor.getTrackCount(); i++) { MediaFormat format = mVideoExtractor.getTrackFormat(i); if (format.getString(MediaFormat.KEY_MIME).startsWith("video/" )) { mVideoExtractor.selectTrack(i); videoTrackIndex = mMediaMuxer.addTrack(format); break ; } } MediaExtractor mAudioExtractor = new MediaExtractor (); mAudioExtractor.setDataSource(mAudioPath); int audioTrackIndex = -1 ; for (int i = 0 ; i < mAudioExtractor.getTrackCount(); i++) { MediaFormat format = mAudioExtractor.getTrackFormat(i); if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/" )) { mAudioExtractor.selectTrack(i); audioTrackIndex = mMediaMuxer.addTrack(format); } } mMediaMuxer.start(); if (-1 != videoTrackIndex) { MediaCodec.BufferInfo info = new MediaCodec .BufferInfo(); info.presentationTimeUs = 0 ; ByteBuffer buffer = ByteBuffer.allocate(100 * 1024 ); while (true ) { int sampleSize = mVideoExtractor.readSampleData(buffer, 0 ); if (sampleSize < 0 ) { break ; } info.offset = 0 ; info.size = sampleSize; info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME; info.presentationTimeUs = mVideoExtractor.getSampleTime(); mMediaMuxer.writeSampleData(videoTrackIndex, buffer, info); mVideoExtractor.advance(); } } if (-1 != audioTrackIndex) { MediaCodec.BufferInfo info = new MediaCodec .BufferInfo(); info.presentationTimeUs = 0 ; ByteBuffer buffer = ByteBuffer.allocate(100 * 1024 ); while (true ) { int sampleSize = mAudioExtractor.readSampleData(buffer, 0 ); if (sampleSize < 0 ) { break ; } info.offset = 0 ; info.size = sampleSize; info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME; info.presentationTimeUs = mAudioExtractor.getSampleTime(); mMediaMuxer.writeSampleData(audioTrackIndex, buffer, info); mAudioExtractor.advance(); } } mVideoExtractor.release(); mAudioExtractor.release(); mMediaMuxer.stop(); mMediaMuxer.release(); }
硬解码h.265视频及音频进行播放 视频解码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 MediaExtractor mediaExtractor = new MediaExtractor ();try { mediaExtractor.setDataSource(path); } catch (IOException e1) { e1.printStackTrace(); } String mimeType = null ;for (int i = 0 ; i < mediaExtractor.getTrackCount(); i++) { MediaFormat format = mediaExtractor.getTrackFormat(i); mimeType = format.getString(MediaFormat.KEY_MIME); if (mimeType.startsWith("video/" )) { mediaExtractor.selectTrack(i); try { mediaCodec = MediaCodec.createDecoderByType(mimeType); } catch (IOException e) { e.printStackTrace(); } mediaCodec.configure(format, surface, null , 0 ); break ; } } mediaCodec.start(); ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers(); ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers(); MediaCodec.BufferInfo info = new MediaCodec .BufferInfo(); while (!Thread.interrupted()) { if (!bIsEos) { int inIndex = mediaCodec.dequeueInputBuffer(0 ); if (inIndex >= 0 ) { ByteBuffer buffer = inputBuffers[inIndex]; int nSampleSize = mediaExtractor.readSampleData(buffer, 0 ); if (nSampleSize < 0 ) { Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM" ); mediaCodec.queueInputBuffer(inIndex, 0 , 0 , 0 , MediaCodec.BUFFER_FLAG_END_OF_STREAM); bIsEos = true ; } else { mediaCodec.queueInputBuffer(inIndex, 0 , nSampleSize, mediaExtractor.getSampleTime(), 0 ); mediaExtractor.advance(); } } } int outIndex = mediaCodec.dequeueOutputBuffer(info, 0 ); switch (outIndex) { case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED" ); outputBuffers = mediaCodec.getOutputBuffers(); break ; case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: Log.d(TAG, "New format " + mediaCodec.getOutputFormat()); break ; case MediaCodec.INFO_TRY_AGAIN_LATER: Log.d(TAG, "dequeueOutputBuffer timed out!" ); break ; default : ByteBuffer buffer = outputBuffers[outIndex]; Log.v(TAG, "We can't use this buffer but render it due to the API limit, " + buffer); mediaCodec.releaseOutputBuffer(outIndex, true ); break ; } if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 ) { Log.d("DecodeActivity" , "OutputBuffer BUFFER_FLAG_END_OF_STREAM" ); break ; } } mediaCodec.stop(); mediaCodec.release(); mediaExtractor.release();
这样视频的解码就已经完成了,此时surfaceView已经可以播放视频了,接下来是音频解码。
音频解码的过程和上面大同小异,主要区别在于,视频是用surfaceView播放显示的,而音频我们需要使用AudioTrack来播放。
音频解码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 public class AudioPlayer { private int mFrequency; private int mChannel; private int mSampBit; private AudioTrack mAudioTrack; public AudioPlayer (int frequency, int channel, int sampbit) { this .mFrequency = frequency; this .mChannel = channel; this .mSampBit = sampbit; } public void init () { if (mAudioTrack != null ) { release(); } int minBufSize = AudioTrack.getMinBufferSize(mFrequency, mChannel, mSampBit); mAudioTrack = new AudioTrack (AudioManager.STREAM_MUSIC, mFrequency, mChannel, mSampBit, minBufSize, AudioTrack.MODE_STREAM); mAudioTrack.play(); } private void release () { if (mAudioTrack != null ) { mAudioTrack.stop(); mAudioTrack.release(); } } public void play (byte [] data, int offset, int length) { if (data == null || data.length == 0 ) { return ; } try { mAudioTrack.write(data, offset, length); } catch (Exception e) { e.printStackTrace(); } } } String mimeType; for (int i = 0 ; i < mediaExtractor.getTrackCount(); i++) { MediaFormat format = mediaExtractor.getTrackFormat(i); mimeType = format.getString(MediaFormat.KEY_MIME); if (mimeType.startsWith("audio/" )) { mediaExtractor.selectTrack(i); try { mediaCodec = MediaCodec.createDecoderByType(mimeType); } catch (IOException e) { e.printStackTrace(); } mediaCodec.configure(format, null , null , 0 ); mPlayer = new AudioPlayer (format.getInteger(MediaFormat.KEY_SAMPLE_RATE), AudioFormat .CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT); mPlayer.init(); break ; } } if (mediaCodec == null ) { Log.e(TAG, "Can't find video info!" ); return ; } mediaCodec.start(); while (!Thread.interrupted()) { if (!bIsEos) { int inIndex = mediaCodec.dequeueInputBuffer(0 ); if (inIndex >= 0 ) { ByteBuffer buffer = inputBuffers[inIndex]; int nSampleSize = mediaExtractor.readSampleData(buffer, 0 ); if (nSampleSize < 0 ) { Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM" ); mediaCodec.queueInputBuffer(inIndex, 0 , 0 , 0 , MediaCodec.BUFFER_FLAG_END_OF_STREAM); bIsEos = true ; } else { mediaCodec.queueInputBuffer(inIndex, 0 , nSampleSize, mediaExtractor.getSampleTime(), 0 ); mediaExtractor.advance(); } } } int outIndex = mediaCodec.dequeueOutputBuffer(info, 0 ); switch (outIndex) { case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED" ); outputBuffers = mediaCodec.getOutputBuffers(); break ; case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: Log.d(TAG, "New format " + mediaCodec.getOutputFormat()); break ; case MediaCodec.INFO_TRY_AGAIN_LATER: Log.d(TAG, "dequeueOutputBuffer timed out!" ); break ; default : ByteBuffer buffer = outputBuffers[outIndex]; Log.v(TAG, "We can't use this buffer but render it due to the API limit, " + buffer); while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) { try { sleep(10 ); } catch (InterruptedException e) { e.printStackTrace(); break ; } } byte [] outData = new byte [info.size]; buffer.get(outData); buffer.clear(); mPlayer.play(outData, 0 , info.size); mediaCodec.releaseOutputBuffer(outIndex, true ); break ; } if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 ) { Log.d("DecodeActivity" , "OutputBuffer BUFFER_FLAG_END_OF_STREAM" ); break ; } }
参考与鸣谢 https://www.cnblogs.com/renhui/p/7478527.html https://www.jianshu.com/p/e7eae2541e01 https://www.cnblogs.com/guanxinjing/p/11378133.html https://www.jianshu.com/p/66acab100e4b https://blog.csdn.net/qq_25412055/article/details/78990538 https://blog.csdn.net/u010126792/article/details/86510903