Android APP 音视频(01)MediaCodec解码H264码流

说明: 此MediaCodec解码H264实操主要针对Android12.0系统。通过读取sd卡上的H264码流Me获取视频数据,将数据通过mediacodec解码输出到surfaceview上。


1 H264码流和MediaCodec解码简介

1.1 H264码流简介

H.264,也被称为MPEG-4 AVC(Advanced Video Coding),是一种广泛使用的数字视频压缩标准,主要用于视频编码。H.264标准由ITU-T视频编码专家组(VCEG)和ISO/IEC动态图像专家组(MPEG)共同开发,旨在提供比之前的视频编码标准更高的数据压缩效率。

H.264是一种基于块的编码技术,它将视频帧分为多个宏块(Macroblocks,MBs),每个宏块包含亮度信息和色度信息。

关于H264码流相关概念还有:

**帧类型,**包括I、P、B三种类型,说明如下:

  • I帧(Intra-coded frames):关键帧,不依赖其他帧进行解码,包含完整的图像信息。
  • P帧(Predictive-coded frames):预测帧,依赖前一个I帧或P帧进行解码,包含相对于前一帧的差分信息。
  • B帧(Bidirectional predictive-coded frames):双向预测帧,依赖前后两个帧进行解码,用于提高压缩效率。

编码过程:包括帧内预测(Intra prediction)、帧间预测(Inter prediction)、变换(Transform)、量化(Quantization)和熵编码(Entropy coding)等步骤。

码流结构:H.264码流由一系列的NAL单元(Network Abstraction Layer Units)组成,每个NAL单元包含一个头部和数据负载,头部定义了负载的类型和重要性。

等等概念,想要有更多了解,可查看以下文章,持续更新中:

系统化学习 H264视频编码(01)基础概念

系统化学习 H264视频编码(02) I帧 P帧 B帧 引入及相关概念解读

系统化学习 H264视频编码(03)数据压缩流程及相关概念

。。。

1.2 MediaCodec解码说明

MediaCodec 是 Android 提供的一个音视频编解码器类,允许应用程序对音频和视频数据进行编码(压缩)和解码(解压缩)。它在 Android 4.1(API 级别 16)版本中引入,广泛应用于处理音视频数据,如播放视频、录制音频等。

以下是 MediaCodec 解码的基本步骤:

  1. 创建 MediaCodec 实例 :通过调用 MediaCodec.createDecoderByType 方法并传入解码类型(如 "video/avc" 或 "audio/mp4a-latm")来创建解码器。

  2. 配置解码参数 :通过调用 configure 方法配置解码器,传入解码参数如解码格式、输出格式等。

  3. 准备输出 Surface:为解码器准备输出 Surface。输出 Surface 用于接收解码后的数据,并显示在屏幕上。

  4. 开始解码 :调用 start 方法启动解码器。

  5. 发送输入数据 :将待解码的数据通过 write 方法发送到解码器的输入队列。

  6. 处理输出数据 :监听输出队列,通过 dequeueOutputBuffer 方法获取解码后的数据,并将其显示在屏幕上。

  7. 停止解码 :解码完成后,调用 stop 方法停止解码器。

  8. 释放资源 :调用 release 方法释放解码器资源。

通过这些步骤,应用程序可以实现对视频和音频数据的高效编解码处理。针对本工程,主要通过从sd卡上读取h264码流,通过mediacodec解码视频并播放到surfaceview上。

2 MediaCodec解码H264码流代码完整解读(android Q)

2.1 关于权限部分的处理

关于权限,需要在AndroidManifest.xml中添加权限,具体如下所示:

html 复制代码
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />

关于运行时权限的请求等,这里给出一个工具类参考代码,具体如下所示:

java 复制代码
public class Permission {
    public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 1;
    //需要申请权限的数组
    private static final String[] permissions = {
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.CAMERA
    };
    //保存真正需要去申请的权限
    private static final List<String> permissionList = new ArrayList<>();

    public static int RequestCode = 100;

    public static void requestManageExternalStoragePermission(Context context, Activity activity) {
        if (!Environment.isExternalStorageManager()) {
            showManageExternalStorageDialog(activity);
        }
    }

    private static void showManageExternalStorageDialog(Activity activity) {
        AlertDialog dialog = new AlertDialog.Builder(activity)
                .setTitle("权限请求")
                .setMessage("请开启文件访问权限,否则应用将无法正常使用。")
                .setNegativeButton("取消", null)
                .setPositiveButton("确定", (dialogInterface, i) -> {
                    Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                    activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE);
                })
                .create();
        dialog.show();
    }

    public static void checkPermissions(Activity activity) {
        for (String permission : permissions) {
            if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
                permissionList.add(permission);
            }
        }

        if (!permissionList.isEmpty()) {
            requestPermission(activity);
        }
    }

    public static void requestPermission(Activity activity) {
        ActivityCompat.requestPermissions(activity,permissionList.toArray(new String[0]),RequestCode);
    }
}

这样,如果后面又更多的权限,都可以使用该方法来处理,处理方式为:

java 复制代码
Permission.checkPermissions(this);
Permission.requestManageExternalStoragePermission(getApplicationContext(), this);

2.2 解码的处理

关于解码部分,主要是MediaCodec的初始化、解码处理部分,代码如下所示:

java 复制代码
public class H264Decoder implements  Runnable {
    private final String path;
    private final String TAG = "H264Decoder";
    MediaCodec mediaCodec;
    boolean enablePlay = false;

    public H264Decoder(String path, Surface surface, int width , int height) {
        this.path = path;
        try {
            mediaCodec = MediaCodec.createDecoderByType("video/avc");
            MediaFormat mediaformat = MediaFormat.createVideoFormat("video/avc", width, height);
            mediaformat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
            mediaCodec.configure(mediaformat, surface, null, 0);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void play() {
        enablePlay = true;
        mediaCodec.start();
        new Thread(this).start();
    }

    public void stop(){
        enablePlay = false;
    }
    @Override
    public void run() {
        try {
            byte[] bytes = null;
            try {
                //注意:这里是从文件中一次性读H264取码流数据,因此不适合特别大的视频
                bytes = getBytes(path);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            int startIndex = 0;
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            while (enablePlay) {
                int nextFrameStart = findByFrame(bytes, startIndex+5, bytes.length);
                //MediaCodec输入缓冲区操作
                int inIndex =  mediaCodec.dequeueInputBuffer(10000);
                if (inIndex >= 0) {
                    ByteBuffer byteBuffer = mediaCodec.getInputBuffer(inIndex);
                    int length = nextFrameStart - startIndex;
                    byteBuffer.put(bytes, startIndex, length);
                    mediaCodec.queueInputBuffer(inIndex, 0, length, 0, 0);
                    startIndex = nextFrameStart;
                }

                //MediaCodec输出缓冲区操作
                int outIndex =mediaCodec.dequeueOutputBuffer(info,10000);
                if (outIndex >= 0) {
                    try {
                        //这里延迟下,避免刷的过快
                        Thread.sleep(40);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    mediaCodec.releaseOutputBuffer(outIndex, true);
                }
            }
        } catch (Exception e) {
            Log.i(TAG, "run decoder error:"+e.toString());
        }
    }

    private int findByFrame( byte[] bytes, int start, int totalSize) {
        for (int i = start; i <= totalSize-4; i++) {
            //这里是一帧的结束符 00 00 00 01 或者 00 00 01
            if (((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x00) && (bytes[i + 3] == 0x01))
            ||((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x01))) {
                return i;
            }
        }
        return -1;
    }

    public byte[] getBytes(String path) throws IOException {
        InputStream is = new DataInputStream(Files.newInputStream(new File(path).toPath()));
        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 主流程代码参考实现

这里以 H264decoderActivity 为例,给出一个MediaCodec解码功能代码的参考实现。具体实现如下:

java 复制代码
public class H264decoderActivity extends AppCompatActivity {
    H264Decoder h264Decoder;
    private final String TAG = "MainActivity";
    Context mContext;
    Surface surface;
    private boolean isPlaying = false; // 用于跟踪播放状态

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        mContext = this;
        setContentView(R.layout.h264_decode_activity_main);

        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        initSurface();
        Permission.checkPermissions(this);
        Permission.requestManageExternalStoragePermission(getApplicationContext(), this);
        Button playButton = findViewById(R.id.button);
        playButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 切换播放状态
                isPlaying = !isPlaying;
                // 根据播放状态更新按钮文本
                if (isPlaying) {
                    playButton.setText(R.string.stopplay);
                    //Environment.DIRECTORY_DOWNLOADS), "ags/out.h264").getAbsolutePath(),
                    h264Decoder = new H264Decoder(
                            new File(Environment.getExternalStoragePublicDirectory(
                                    Environment.DIRECTORY_DOWNLOADS), "ags/outputtest4.h264").getAbsolutePath(),
                            surface,1280,720);
                    h264Decoder.play();
                } else {
                    playButton.setText(R.string.startplay);
                    h264Decoder.stop();
                }
            }
        });
    }

    private void initSurface() {
        SurfaceView mSurface = findViewById(R.id.preview);
        mSurface.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
                Log.d(TAG,"surfaceCreated");
                surface=surfaceHolder.getSurface();
            }

            @Override
            public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
                Log.d(TAG,"surfaceChanged");
            }

            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
                Log.d(TAG,"surfaceDestroyed");
            }
        });
    }
}

这里涉及的layout布局文件内容如下:

html 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <SurfaceView
        android:id="@+id/preview"
        android:layout_width="372dp"
        android:layout_height="240dp"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/playtest"
        app:layout_constraintTop_toBottomOf="@id/preview"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        tools:ignore="MissingConstraints" />

</androidx.constraintlayout.widget.ConstraintLayout>

2.4 解码 demo实现效果

这里是找一个mp4格式的测试视频,使用ffmpeg将mp4格式中的视频码流输出出来。使用命令为:

bash 复制代码
$ffmpeg -i inputtest.mp4 -vcodec libx264 -preset slow -b:v 2000k -crf 21 out.h264

将其push到sd卡上,完整路径为:/sdcard/Download/ags/outputtest4.h264。实际运行效果展示如下:

相关推荐
阿巴斯甜11 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker12 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952713 小时前
Andorid Google 登录接入文档
android
黄林晴14 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android