百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

MediaCodec 解码H264/H265码流视频

zhezhongyun 2025-07-03 20:54 34 浏览

MediaCodec 解码H264/H265码流视频

1、使用MediaCodec目的

MediaCodec是Android底层多媒体框架的一部分,通常与MediaExtractor、MediaMuxer、AudioTrack结合使用,可以编码H264、H265、AAC、3gp等常见的音视频格式

MediaCodec工作原理是处理输入数据以产生输出数据

1.1 MediaCodec工作流程

MediaCodec的数据流分为input和output流,并通过异步的方式处理两路数据流,直到手动释放output缓冲区,MediaCodec才将数据处理完毕

  • input流:客户端输入待解码或者待编码的数据
  • output流:客户端输出的已解码或者已编码的数据

1.2 MediaCodec API说明

  • getInputBuffers:获取需要输入流队列,返回ByteBuffer数组
  • queueInputBuffer:输入流入队
  • dequeueInputBuffer: 从输入流队列中取数据进行编码操作
  • getOutputBuffers:获取已经编解码之后的数据输出流队列,返回ByteBuffer数组
  • dequeueOutputBuffer:从输出队列中取出已经编码操作之后的数据
  • releaseOutputBuffer: 处理完成,释放output缓冲区

领音视频资料→「链接」

1.3 基本流程

  • MediaCodec的基本使用遵循上图所示,它的生命周期如下所示:
  • Stoped:创建好MediaCodec,进行配置,或者出现错误
  • Uninitialized: 当创建了一个MediaCodec对象,此时MediaCodec处于Uninitialized,在任何状态调用reset()方法使MediaCodec返回到Uninitialized状态
  • Configured: 使用configure(…)方法对MediaCodec进行配置转为Configured状态
  • Error: 出现错误
  • Executing:可以在Executing状态的任何时候通过调用flush()方法返回到Flushed状态
  • Flushed:调用start()方法后MediaCodec立即进入Flushed状态
  • Running:调用dequeueInputBuffer后,MediaCodec就转入Running状态
  • End-of-Stream:编解码结束后,MediaCodec将转入End-of-Stream子状态
  • Released:当使用完MediaCodec后,必须调用release()方法释放其资源

1.4 基本使用

//解码器
val mVideoDecoder = MediaCodec.createDecoderByType("video/avc")
//编码器
val mVideoEncoder = MediaCodec.createEncoderByType("video/avc")

2、MediaCodec 解码H264/H265

使用MediaCodec 解码H264/H265码流视频,那必须谈下MediaCodec这个神器。附官网数据流程图如下:

input:ByteBuffer输入方;

output:ByteBuffer输出方;

  • 使用者从MediaCodec请求一个空的输入buffer(ByteBuffer),填充满数据后将它传递给MediaCodec处理。
  • MediaCodec处理完这些数据并将处理结果输出至一个空的输出buffer(ByteBuffer)中。
  • 使用者从MediaCodec获取输出buffer的数据,消耗掉里面的数据,使用完输出buffer的数据之后,将其释放回编解码。

2.1 H264码流解码示例代码如下(基本都做了注释)

package com.zqfdev.h264decodedemo;

import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;

/**
 * @author zhangqingfa
 * @createDate 2020/12/10 11:39
 * @description 解码H264播放
 */
public class H264DeCodePlay {

    private static final String TAG = "zqf-dev";
    //视频路径
    private String videoPath;
    //使用android MediaCodec解码
    private MediaCodec mediaCodec;
    private Surface surface;

    H264DeCodePlay(String videoPath, Surface surface) {
        this.videoPath = videoPath;
        this.surface = surface;
        initMediaCodec();
    }

    private void initMediaCodec() {
        try {
            Log.e(TAG, "videoPath " + videoPath);
            //创建解码器 H264的Type为  AAC
            mediaCodec = MediaCodec.createDecoderByType("video/avc");
            //创建配置
            MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 540, 960);
            //设置解码预期的帧速率【以帧/秒为单位的视频格式的帧速率的键】
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
            //配置绑定mediaFormat和surface
            mediaCodec.configure(mediaFormat, surface, null, 0);
        } catch (IOException e) {
            e.printStackTrace();
            //创建解码失败
            Log.e(TAG, "创建解码失败");
        }
    }

    /**
     * 解码播放
     */
    void decodePlay() {
        mediaCodec.start();
        new Thread(new MyRun()).start();
    }

    private class MyRun implements Runnable {

        @Override
        public void run() {
            try {
                //1、IO流方式读取h264文件【太大的视频分批加载】
                byte[] bytes = null;
                bytes = getBytes(videoPath);
                Log.e(TAG, "bytes size " + bytes.length);
                //2、拿到 mediaCodec 所有队列buffer[]
                ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
                //开始位置
                int startIndex = 0;
                //h264总字节数
                int totalSize = bytes.length;
                //3、解析
                while (true) {
                    //判断是否符合
                    if (totalSize == 0 || startIndex >= totalSize) {
                        break;
                    }
                    //寻找索引
                    int nextFrameStart = findByFrame(bytes, startIndex + 1, totalSize);
                    if (nextFrameStart == -1) break;
                    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                    // 查询10000毫秒后,如果dSP芯片的buffer全部被占用,返回-1;存在则大于0
                    int inIndex = mediaCodec.dequeueInputBuffer(10000);
                    if (inIndex >= 0) {
                        //根据返回的index拿到可以用的buffer
                        ByteBuffer byteBuffer = inputBuffers[inIndex];
                        //清空缓存
                        byteBuffer.clear();
                        //开始为buffer填充数据
                        byteBuffer.put(bytes, startIndex, nextFrameStart - startIndex);
                        //填充数据后通知mediacodec查询inIndex索引的这个buffer,
                        mediaCodec.queueInputBuffer(inIndex, 0, nextFrameStart - startIndex, 0, 0);
                        //为下一帧做准备,下一帧首就是前一帧的尾。
                        startIndex = nextFrameStart;
                    } else {
                        //等待查询空的buffer
                        continue;
                    }
                    //mediaCodec 查询 "mediaCodec的输出方队列"得到索引
                    int outIndex = mediaCodec.dequeueOutputBuffer(info, 10000);
                    Log.e(TAG, "outIndex " + outIndex);
                    if (outIndex >= 0) {
                        try {
                            //暂时以休眠线程方式放慢播放速度
                            Thread.sleep(33);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //如果surface绑定了,则直接输入到surface渲染并释放
                        mediaCodec.releaseOutputBuffer(outIndex, true);
                    } else {
                        Log.e(TAG, "没有解码成功");
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    //读取一帧数据
    private int findByFrame(byte[] bytes, int start, int totalSize) {
        for (int i = start; i < totalSize - 4; i++) {
            //对output.h264文件分析 可通过分隔符 0x00000001 读取真正的数据
            if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x01) {
                return i;
            }
        }
        return -1;
    }

    private byte[] getBytes(String videoPath) throws IOException {
        InputStream is = new DataInputStream(new FileInputStream(new File(videoPath)));
        int len;
        int size = 1024;
        byte[] buf;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        buf = new byte[size];
        while ((len = is.read(buf, 0, size)) != -1)
            bos.write(buf, 0, len);
        buf = bos.toByteArray();
        return buf;
    }
}

2.2 H265示例代码如下

package com.zqfdev.h264decodedemo;

import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;

/**
 * @author zhangqingfa
 * @createDate 2020/12/10 11:39
 * @description 解码H264播放
 */
public class H265DeCodePlay {

    private static final String TAG = "zqf-dev";
    //视频路径
    private String videoPath;
    //使用android MediaCodec解码
    private MediaCodec mediaCodec;
    private Surface surface;

    H265DeCodePlay(String videoPath, Surface surface) {
        this.videoPath = videoPath;
        this.surface = surface;
        initMediaCodec();
    }

    private void initMediaCodec() {
        try {
            Log.e(TAG, "videoPath " + videoPath);
            //创建解码器 H264的Type为  AAC
            mediaCodec = MediaCodec.createDecoderByType("video/hevc");
            //创建配置
            MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/hevc", 368, 384);
            //设置解码预期的帧速率【以帧/秒为单位的视频格式的帧速率的键】
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
            //配置绑定mediaFormat和surface
            mediaCodec.configure(mediaFormat, surface, null, 0);
        } catch (IOException e) {
            e.printStackTrace();
            //创建解码失败
            Log.e(TAG, "创建解码失败");
        }
    }

    /**
     * 解码播放
     */
    void decodePlay() {
        mediaCodec.start();
        new Thread(new MyRun()).start();
    }

    private class MyRun implements Runnable {

        @Override
        public void run() {
            try {
                //1、IO流方式读取h264文件【太大的视频分批加载】
                byte[] bytes = null;
                bytes = getBytes(videoPath);
                Log.e(TAG, "bytes size " + bytes.length);
                //2、拿到 mediaCodec 所有队列buffer[]
                ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
                //开始位置
                int startIndex = 0;
                //h264总字节数
                int totalSize = bytes.length;
                //3、解析
                while (true) {
                    //判断是否符合
                    if (totalSize == 0 || startIndex >= totalSize) {
                        break;
                    }
                    //寻找索引
                    int nextFrameStart = findByFrame(bytes, startIndex + 1, totalSize);
                    if (nextFrameStart == -1) break;
                    Log.e(TAG, "nextFrameStart " + nextFrameStart);
                    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                    // 查询10000毫秒后,如果dSP芯片的buffer全部被占用,返回-1;存在则大于0
                    int inIndex = mediaCodec.dequeueInputBuffer(10000);
                    if (inIndex >= 0) {
                        //根据返回的index拿到可以用的buffer
                        ByteBuffer byteBuffer = inputBuffers[inIndex];
                        //清空byteBuffer缓存
                        byteBuffer.clear();
                        //开始为buffer填充数据
                        byteBuffer.put(bytes, startIndex, nextFrameStart - startIndex);
                        //填充数据后通知mediacodec查询inIndex索引的这个buffer,
                        mediaCodec.queueInputBuffer(inIndex, 0, nextFrameStart - startIndex, 0, 0);
                        //为下一帧做准备,下一帧首就是前一帧的尾。
                        startIndex = nextFrameStart;
                    } else {
                        //等待查询空的buffer
                        continue;
                    }
                    //mediaCodec 查询 "mediaCodec的输出方队列"得到索引
                    int outIndex = mediaCodec.dequeueOutputBuffer(info, 10000);
                    Log.e(TAG, "outIndex " + outIndex);
                    if (outIndex >= 0) {
                        try {
                            //暂时以休眠线程方式放慢播放速度
                            Thread.sleep(33);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //如果surface绑定了,则直接输入到surface渲染并释放
                        mediaCodec.releaseOutputBuffer(outIndex, true);
                    } else {
                        Log.e(TAG, "没有解码成功");
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    //读取一帧数据
    private int findByFrame(byte[] bytes, int start, int totalSize) {
        for (int i = start; i < totalSize - 4; i++) {
            //对output.h264文件分析 可通过分隔符 0x00000001 读取真正的数据
            if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x01) {
                return i;
            }
        }
        return -1;
    }

    private byte[] getBytes(String videoPath) throws IOException {
        InputStream is = new DataInputStream(new FileInputStream(new File(videoPath)));
        int len;
        int size = 1024;
        byte[] buf;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        buf = new byte[size];
        while ((len = is.read(buf, 0, size)) != -1)
            bos.write(buf, 0, len);
        buf = bos.toByteArray();
        return buf;
    }
}

2.3 MainActivity代码如下

package com.zqfdev.h264decodedemo;

import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.io.File;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

public class MainActivity extends AppCompatActivity {
    private String[] permiss = {"android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE"};
    private H264DeCodePlay h264DeCodePlay;
//    private H265DeCodePlay h265DeCodePlay;
    private String videoPath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        checkPermiss();
        initView();
    }

    private void checkPermiss() {
        int code = ActivityCompat.checkSelfPermission(this, permiss[0]);
        if (code != PackageManager.PERMISSION_GRANTED) {
            // 没有写的权限,去申请写的权限
            ActivityCompat.requestPermissions(this, permiss, 11);
        }
    }

    private void initView() {
        File dir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
        if (!dir.exists()) dir.mkdirs();
        final File file = new File(dir, "output.h264");
//        final File file = new File(dir, "output.h265");
        if (!file.exists()) {
            Log.e("Tag", "文件不存在");
            return;
        }
        videoPath = file.getAbsolutePath();
        final SurfaceView surface = findViewById(R.id.surface);
        final SurfaceHolder holder = surface.getHolder();
        holder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
                h264DeCodePlay = new H264DeCodePlay(videoPath, holder.getSurface());
                h264DeCodePlay.decodePlay();
//                h265DeCodePlay = new H265DeCodePlay(videoPath, holder.getSurface());
//                h265DeCodePlay.decodePlay();
            }

            @Override
            public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {

            }

            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {

            }
        });
    }
}

测试的H264 / H265码流视频通过FFmpeg抽取可得到。

命令行:

ffmpeg -i 源视频.mp4 -codec copy -bsf: h264_mp4toannexb -f h264 输出视频.h264

最后效果如下:

相关推荐

Python入门学习记录之一:变量_python怎么用变量

写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...

python变量命名规则——来自小白的总结

python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...

Python入门学习教程:第 2 章 变量与数据类型

2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...

绘制学术论文中的“三线表”具体指导

在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...

Python基础语法知识--变量和数据类型

学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...

一文搞懂 Python 中的所有标点符号

反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...

Python变量类型和运算符_python中变量的含义

别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...

从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序

在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...

Python中下划线 ‘_’ 的用法,你知道几种

Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...

解锁Shell编程:变量_shell $变量

引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...

一文学会Python的变量命名规则!_python的变量命名有哪些要求

目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...

更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for

src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...

C++第五课:变量的命名规则_c++中变量的命名规则

变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....

Rust编程-核心篇-不安全编程_rust安全性

Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...

探秘 Python 内存管理:背后的神奇机制

在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...