MediaCodec 也能播视频?

2020-12-14  本文已影响0人  毛先森

目录

播放 h264 效果

是什么

MediaCodec 是 Android 原生提供的音视频编解码框架,由于直接与硬件交互(dsp),所以是以硬件编解码的方式工作

重要方法

解码

将 h264等编码数据转换为 YUV 等原始数据,如果我们怎样手动把 h264 数据渲染到 SurfaceView 上呢?

  1. 创建 SurfaceView 控件,并获取到 Surface
  2. 初始化 MediaCodec ,获取解码器并且配置
  3. 获取文件路径,将文件转换为 byte 数组
  4. 获取当前可用的 ByteBuffer,并填入原始数据
  5. 获取输出 ByteBuffer,取出数据
  6. MediaCodec 初始化时与 Surface 绑定,只要在 releaseOutputBuffer() 中开启渲染,此时 SurfaceView 在就能正常出现解码后的视频数据

最佳实践

// 解码工具类

public class H264Player implements Runnable {

    // 本地 h264 文件路径
    private String path;
    private Surface surface;
    private MediaCodec mediaCodec;
    private Context context;

    public H264Player(Context context, String path, Surface surface) {

        this.context = context;
        this.path = path;
        this.surface = surface;
        try {
            this.mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
            // 视频宽高暂时写死
            MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 368, 384);
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
            mediaCodec.configure(mediaFormat, surface, null, 0);
        } catch (IOException e) {
            // 解码芯片不支持,走软解
            e.printStackTrace();
        }
    }


    public void play() {
        mediaCodec.start();
        new Thread(this::run).start();
    }

    @Override
    public void run() {
        // 解码 h264
        decodeH264();
    }

    private void decodeH264() {
        byte[] bytes = null;
        try {
            bytes = getBytes(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 获取队列
        ByteBuffer[] byteBuffers = mediaCodec.getInputBuffers();
        int startIndex = 0;
        int nextFrameStart;
        int totalCount = bytes.length;

        while (true) {
            if (startIndex >= totalCount) {
                break;
            }
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            nextFrameStart = findFrame(bytes, startIndex+1,totalCount);
            // 往 ByteBuffer 中塞入数据
            int index = mediaCodec.dequeueInputBuffer(10 * 1000);
            Log.e("index",index+"");
            // 获取 dsp 成功
            if (index >= 0) {
                // 拿到可用的 ByteBuffer
                ByteBuffer byteBuffer = byteBuffers[index];
                byteBuffer.clear();
                byteBuffer.put(bytes, startIndex, nextFrameStart - startIndex);
                // 识别分隔符,找到分隔符对应的索引
                mediaCodec.queueInputBuffer(index, 0, nextFrameStart - startIndex, 0, 0);
                startIndex = nextFrameStart;

            }else {
                continue;
            }


            // 从 ByteBuffer 中获取解码好的数据
            int outIndex = mediaCodec.dequeueOutputBuffer(info,10 * 1000);
            if (outIndex > 0){
                try {
                    Thread.sleep(33);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mediaCodec.releaseOutputBuffer(outIndex, true);
            }

        }
    }

    private int findFrame(byte[] bytes, int startIndex, int totalSize) {
        for (int i = startIndex; i < totalSize - 4; i++) {
            if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x01) {
                return i;
            }

        }
        return -1;
    }


    /**
     * 一次性读取文件
     *
     * @param path
     * @return
     * @throws IOException
     */
    public byte[] getBytes(String path) throws IOException {
        InputStream is = new DataInputStream(new FileInputStream(new File(path)));
        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;
    }


}

在 Activity 中播放


public class MainActivity extends AppCompatActivity {
    private H264Player h264Player;

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

    private void initSurface() {
        SurfaceView surfaceView = findViewById(R.id.surface);
        SurfaceHolder surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder surfaceHolder) {
                h264Player = new H264Player(MainActivity.this, new File(Environment.getExternalStorageDirectory(), "test.h264").getAbsolutePath(), surfaceHolder.getSurface());
                h264Player.play();
            }

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

            @Override
            public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
            }
        });
    }

    private boolean checkPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                && checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE
            }, 1);
        }
        return false;
    }
}

总结

这次我们播放的 h264 文件,需要先使用 FFmpeg 从 mp4 文件分离出 h264,放到手机储存卡根目录(也可以放到本地其他文件夹,文件路径对应修改就好了),通过创建 MediaCodec 解码器,绑定 Surface,获取 ByteBuffer 等一系列操作,最终实现了手动解码视频文件,并且渲染到 SurfaceView,这个 Demo 主要用于熟悉 Api,不适宜用于实际工业级别开发(比如整个读取文件是不可取的)

相关链接

android-mediacodec-google官方demo

上一篇下一篇

猜你喜欢

热点阅读