资讯动态

Android 音视频编解码(二) -- MediaCodec 解码(同步和异步)

发布时间:2026/10/30 1:14:56 来源:尧图企业网站定制
音视频 系列文章Android 音视频开发基础Android 音视频编解码(一) – MediaCodec 初探音视频工程上面的文章Android 音视频编解码(一) – MediaCodec 初探 中已经对 MediaCodec 有个感性的认知这一章来学习 MediaCodec 的解码功能。本章效果如下一、同步解码为了更好的理解 MediaCodec 的工作原理和工作步骤首先先使用同步解码的方式去解码本地视频。1.1 拿到视频参数首先我们先要准备一个视频比如 MP4 格式的它其实已经被编码过的我们可以通过 MediaCodec 来获取该视频的 MediaFormat 信息如果你对 MediaExtractor 不熟悉可以通过 Android 音视频开发(五) – 使用 MediaExtractor 分离音视频并使用 MediaMuxer合成新视频(音视频同步) 来学习定义一个 MyExtractor 里面实现 MediaExtractor 用来专门解析视频拿到视频数据public MyExtractor(String path) { try { mediaExtractor new MediaExtractor(); // 设置数据源 mediaExtractor.setDataSource(path); } catch (IOException e) { e.printStackTrace(); } //拿到所有的轨道 int count mediaExtractor.getTrackCount(); for (int i 0; i count; i) { //根据下标拿到 MediaFormat MediaFormat format mediaExtractor.getTrackFormat(i); //拿到 mime 类型 String mime format.getString(MediaFormat.KEY_MIME); //拿到视频轨 if (mime.startsWith(video)) { videoTrackId i; videoFormat format; } else if (mime.startsWith(audio)) { //拿到音频轨 audioTrackId i; audioFormat format; } } } public void selectTrack(int trackId){ mediaExtractor.selectTrack(trackId); } /** * 读取一帧的数据 * * param buffer * return */ public int readBuffer(ByteBuffer buffer) { //先清空数据 buffer.clear(); //选择要解析的轨道 // mediaExtractor.selectTrack(video ? videoTrackId : audioTrackId); //读取当前帧的数据 int buffercount mediaExtractor.readSampleData(buffer, 0); if (buffercount 0) { return -1; } //记录当前时间戳 curSampleTime mediaExtractor.getSampleTime(); //记录当前帧的标志位 curSampleFlags mediaExtractor.getSampleFlags(); //进入下一帧 mediaExtractor.advance(); return buffercount; }首先先通过 selectTrack 来指定是解析视频还是音频接着使用 readBuffer 这个方法该方法使用 mediaExtractor.readSampleData(buffer, 0); 去拿到当前视频的 buffer并通过 mediaExtractor.advance() 拿到下一帧的数据。1.2 解码流程上一章说到 MediaCodec 的解码是基于以下两张图的MediaCodec 工作图MediaCodec状态图如果你对这两张图的流程不熟悉请阅读 Android 音视频编解码(一) – MediaCodec 初探 。为了方便后续的音频解码这里我们定义一个基类用来解析视频和音频由于是同步解析视频我们应该在线程中去解析所以继承 Runnable:/** * 解码基类用于解码音视频 */ abstract class BaseDecode implements Runnable { final static int VIDEO 1; final static int AUDIO 2; //等待时间 final static int TIME_US 1000; MediaFormat mediaFormat; MediaCodec mediaCodec; MyExtractor extractor; private boolean isDone; public BaseDecode() { try { //获取 MediaExtractor extractor new MyExtractor(Constants.VIDEO_PATH); //判断是音频还是视频 int type decodeType(); //拿到音频或视频的 MediaFormat mediaFormat (type VIDEO ? extractor.getVideoFormat() : extractor.getAudioFormat()); String mime mediaFormat.getString(MediaFormat.KEY_MIME); //选择要解析的轨道 extractor.selectTrack(type VIDEO ? extractor.getVideoTrackId() : extractor.getAudioTrackId()); //创建 MediaCodec mediaCodec MediaCodec.createDecoderByType(mime); //由子类去配置 configure(); //开始工作进入编解码状态 mediaCodec.start(); } catch (IOException e) { e.printStackTrace(); } } }可以看到根据视频或者视频拿到不同的 MediaFormat 并根据 mime 类型创建 MediaCodec ;这样第一步就创建好了接着从上面的状态知道现在处于Uninitialized 状态接着需要调用 configure方法进入 Configured 状态这一步由子类完成比如视频的Override void configure() { mediaCodec.configure(mediaFormat, new Surface(mTextureView.getSurfaceTexture()), null, 0); }可以看到通过mediaCodec.configure() 配置了当前的 MediaFormat和要播放视频的 Surface这里用的是 TextureView。接着调用 MediaCodec 的 start() 进入 Executing 状态可以编解码了。1.3 视频解码解码流程根据这张图1.3.1 输入上面说到 BaseDecode 继承 Runnable 所以解码的流程在 run 方法中。Override public void run() { try { MediaCodec.BufferInfo info new MediaCodec.BufferInfo(); //编码 while (!isDone) { /** * 延迟 TIME_US 等待拿到空的 input buffer下标单位为 us * -1 表示一直等待知道拿到数据0 表示立即返回 */ int inputBufferId mediaCodec.dequeueInputBuffer(TIME_US); if (inputBufferId 0) { //拿到 可用的空的 input buffer ByteBuffer inputBuffer mediaCodec.getInputBuffer(inputBufferId); if (inputBuffer ! null) { /** * 通过 mediaExtractor.readSampleData(buffer, 0) 拿到视频的当前帧的buffer * 通过 mediaExtractor.advance() 拿到下一帧 */ int size extractor.readBuffer(inputBuffer); //解析数据 if (size 0) { mediaCodec.queueInputBuffer( inputBufferId, 0, size, extractor.getSampleTime(), extractor.getSampleFlags() ); } else { //结束,传递 end-of-stream 标志 mediaCodec.queueInputBuffer( inputBufferId, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM ); isDone true; } } } //解码输出交给子类 boolean isFinish handleOutputData(info); if (isFinish){ break; } } done(); } catch (Exception e) { e.printStackTrace(); } } protected void done(){ try { isDone true; //释放 mediacodec mediaCodec.stop(); mediaCodec.release(); //释放 MediaExtractor extractor.release(); } catch (Exception e) { e.printStackTrace(); } } abstract boolean handleOutputData(MediaCodec.BufferInfo info);在一个 while 循环中不断的解码上面的代码做到以下流程从 MediaCodec 拿到一个空闲的 buffer从视频中拿到视频的当前帧的数据并填充到 MediaCodec 的buffer中使用 mediaCodec.queueInputBuffer() 把buffer 的数据交给 MediaCodec 去解码1.3.2 输出上面的输出过程由 handleOutputData() 去实现去到 VideoDecodeSync 代码实现如下Override boolean handleOutputData(MediaCodec.BufferInfo info) { //等到拿到输出的buffer下标 int outputId mediaCodec.dequeueOutputBuffer(info, TIME_US); if (outputId 0){ //释放buffer并渲染到 Surface 中 mediaCodec.releaseOutputBuffer(outputId, true); } // 在所有解码后的帧都被渲染后就可以停止播放了 if ((info.flags MediaCodec.BUFFER_FLAG_END_OF_STREAM) ! 0) { Log.e(TAG, zsr OutputBuffer BUFFER_FLAG_END_OF_STREAM); return true; } return false; }上面的代码也做到两件事拿到output buffer释放 buffer并渲染视频到 Surface 中由 releaseOutputBuffer() 第二个参数控制。这样视频解码部分我们就写好了。效果如下但是你会发现视频好像是倍速播放一样。1.3.3 矫正显示时间戳为什么会出现上面的情况呢正常来说我们的视频播放的帧率大概为 30即 30fps大概每 33.33ms 播放一帧但解码的出一帧的时间大概就是几 ms 的事情如果一解码就直接给 Surface 去显示的话视频看起来就想倍速播放的一样。那你可能说直接延时 30ms 播放就可以了啊那不是标准吗 是的30fps 是标准但不代表每个视频都是30这里就需要学习 音视频的基础知识 DTS 和 PTS。DTS (Decoding Time Stamp) 即解码时间戳这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据PTS (Presentation Time Stamp)显示时间戳这个时间戳告诉播放器什么时候播放这一帧需要注意的是虽然 DTS 、PTS 是用于指导播放端的行为但他们是在编码的时候由编码器生成的。在没有B帧的情况下DTS和 PTS 的输出顺序是一样的一旦存在 B 帧则顺序不一样。这里我们只需要关心 PTS 即显示时间戳。通过 MediaCodec.BufferInfo 的 presentationTimeUs 可以拿到当前的 pts 时间戳单位是微妙它是相对于0开始播放的时间所以我们可以使用系统时间差来模仿两帧的时间差这样当解码过来的 pts 比这个时间差快则延时以下再输出到 Surface 如果不是则直接显示到 Surface 中。由于在线程中所以我们可以使用 Thread.sleep() 去实现在渲染到 Surface 之前// 用于对准视频的时间戳 private long startMs -1; if (outputId 0){ if (mStartMs -1) { mStartMs System.currentTimeMillis(); } //矫正pts sleepRender(info, startMs); //释放buffer并渲染到 Surface 中 mediaCodec.releaseOutputBuffer(outputId, true); } #sleepRender /** * 数据的时间戳对齐 **/ private void sleepRender(MediaCodec.BufferInfo info, long startMs) { /** * 注意这里是以 0 为出事目标的info.presenttationTimes 的单位为微秒 * 这里用系统时间来模拟两帧的时间差 */ long ptsTimes info.presentationTimeUs / 1000; long systemTimes System.currentTimeMillis() - startMs; long timeDifference ptsTimes - systemTimes; // 如果当前帧比系统时间差快了则延时以下 if (timeDifference 0) { try { Thread.sleep(timeDifference); } catch (InterruptedException e) { e.printStackTrace(); } } }现在时间播放就正常了。1.4 解码音频上面已经理解了视频的解码那么音频的解码就比较简单了新建一个 AudioDecodeSync 类继承BaseDecode ,在 configure 方法中去配置 MediaCodec 由于不需要 Surface 传递 null 即可。Override void configure() { mediaCodec.configure(mediaFormat, null, null, 0); }虽然我们不需要使用 Surface 但是需要播放视频那么则需要使用 AudioTrack如果对它不需要可以参考 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音)AudioTrack播放音频.所以在 AudioDecodeSync 的构造方法中需要配置以下 AudioTrack class AudioDecodeSync extends BaseDecode { private int mPcmEncode; //一帧的最小buffer大小 private final int minBufferSize; private AudioTrack audioTrack; public AudioDecodeSync() { //拿到采样率 if (mediaFormat.containsKey(MediaFormat.KEY_PCM_ENCODING)) { mPcmEncode mediaFormat.getInteger(MediaFormat.KEY_PCM_ENCODING); } else { //默认采样率为 16bit mPcmEncode AudioFormat.ENCODING_PCM_16BIT; } //音频采样率 int sampleRate mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); //获取视频通道数 int channelCount mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); //拿到声道 int channelConfig channelCount 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO; minBufferSize AudioTrack.getMinBufferSize(sampleRate, channelConfig, mPcmEncode); /** * 设置音频信息属性 * 1.设置支持多媒体属性比如audiovideo * 2.设置音频格式比如 music */ AudioAttributes attributes new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); /** * 设置音频数据 * 1. 设置采样率 * 2. 设置采样位数 * 3. 设置声道 */ AudioFormat format new AudioFormat.Builder() .setSampleRate(sampleRate) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setChannelMask(channelConfig) .build(); //配置 audioTrack audioTrack new AudioTrack( attributes, format, minBufferSize, AudioTrack.MODE_STREAM, //采用流模式 AudioManager.AUDIO_SESSION_ID_GENERATE ); //监听播放 audioTrack.play(); } }拿到 AudioTrack 之后就可以使用 play() 方法去监听是否有数据写入开始播放音频了。在 handleOutputDataOverride boolean handleOutputData(MediaCodec.BufferInfo info) { //拿到output buffer int outputIndex mediaCodec.dequeueOutputBuffer(info, TIME_US); ByteBuffer outputBuffer; if (outputIndex 0) { outputBuffer mediaCodec.getOutputBuffer(outputIndex); //写数据到 AudioTrack 只实现音频播放 audioTrack.write(outputBuffer, info.size, AudioTrack.WRITE_BLOCKING); mediaCodec.releaseOutputBuffer(outputIndex, false); } // 在所有解码后的帧都被渲染后就可以停止播放了 if ((info.flags MediaCodec.BUFFER_FLAG_END_OF_STREAM) ! 0) { Log.e(TAG, zsr OutputBuffer BUFFER_FLAG_END_OF_STREAM); return true; } return false; }你会发现音频正常播放了且没有什么快进的意思因为音频的时间戳是比较连续的因此不用矫正。1.5 音视频同步那么如何让音视频同步呢其实也不算难就是开辟两个线程让它同时播放即可if (mExecutorService.isShutdown()) { mExecutorService Executors.newFixedThreadPool(2); } mVideoSync new VideoDecodeSync(); mAudioDecodeSync new AudioDecodeSync(); mExecutorService.execute(mVideoSync); mExecutorService.execute(mAudioDecodeSync); }二、异步解码在 5.0 之后google 建议使用异步解码的方式去使用 MediaCodec使用也非常简单只需要使用 setCallback 方法即可。比如解析上面的视频步骤可以如下使用 MediaExtractor 解析视频拿到 MediaFormat使用 MediaCodec.setCallback() 方法调用 mediaCodec.configure() 和 mediaCodec.start() 开始解码所以代码如下class AsyncDecode { MediaFormat mediaFormat; MediaCodec mediaCodec; MyExtractor extractor; public AsyncDecode() { try { //解析视频拿到 mediaformat extractor new MyExtractor(Constants.VIDEO_PATH); mediaFormat (extractor.getVideoFormat()); String mime mediaFormat.getString(MediaFormat.KEY_MIME); extractor.selectTrack(extractor.getVideoTrackId()); mediaCodec MediaCodec.createDecoderByType(mime); } catch (IOException e) { e.printStackTrace(); } } private void start() { //异步解码 mediaCodec.setCallback(new MediaCodec.Callback() { Override public void onInputBufferAvailable(NonNull MediaCodec codec, int index) { ByteBuffer inputBuffer codec.getInputBuffer(index); int size extractor.readBuffer(inputBuffer); if (size 0) { codec.queueInputBuffer( index, 0, size, extractor.getSampleTime(), extractor.getSampleFlags() ); handler.sendEmptyMessage(1); } else { //结束 codec.queueInputBuffer( index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM ); } } Override public void onOutputBufferAvailable(NonNull MediaCodec codec, int index, NonNull MediaCodec.BufferInfo info) { mediaCodec.releaseOutputBuffer(index, true); } Override public void onError(NonNull MediaCodec codec, NonNull MediaCodec.CodecException e) { codec.reset(); } Override public void onOutputFormatChanged(NonNull MediaCodec codec, NonNull MediaFormat format) { } }); //需要在 setCallback 之后配置 configure mediaCodec.configure(mediaFormat, new Surface(mTextureView.getSurfaceTexture()), null, 0); //开始解码 mediaCodec.start(); } }使用异步解码与同步解码的流程基本一致只不过在同步的代码中我们通过int inputBufferId mediaCodec.dequeueInputBuffer(TIME_US);去等待拿到空闲的 input buffer 下标而在异步中则是通过回调void onInputBufferAvailable(NonNull MediaCodec codec, int index)拿到 input buffer 的下标。不出意外的话你的视频已经开始播放了但是也遇到上面的问题就是倍速播放了原因我们也解释过就是没矫正 PTS 的问题。由于我们的代码在主线程中去进行的直接 Thread.sleep() 肯定会卡顿的但是我们可以使用 HandlerThread 或其他线程的方式去解析这里就不贴了具体看源码吧。参考https://www.jianshu.com/p/ba8db84f8fe8https://mp.weixin.qq.com/s?__bizMzUxODQ3MTk5Mgmid2247483868idx1sn99dec978a4640136c870965ba853c204chksmf989298bcefea09dddae613023139e0c6bd2f4eaaa9dc3ec16400645fc4a0764414134ad73eascene38#wechat_redirecthttps://developer.android.google.cn/reference/android/media/MediaCodec?hlen

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑