android 硬解码播放音视频


前言

个人学习项目,有很多不成熟的地方,期待讨论指正:https://github.com/yang0yyan/MediaPlayer2/tree/dev

首先解码(硬解码),然后播放视频和音频,实现音视频播放,并在此基础上实现在横竖屏切换和后台播放功能,进度条实时显示和进度跳转,倍速快放和慢放

播放结束后点击播放重头开始播放,
支持暂停继续

只讲方法,个人见解
Android音视频开发,主要分为两部分:解码和播放。

介绍如何解码(硬解码),然后播放视频和音频,在此基础上实现在播放音视频过程中横竖屏切换和后台播放功能。

本章介绍如何用MediaCodec(硬)解码,后面会介绍FFmpeg实现软解码和硬解码(涉及到NDK开发)
MediaCodec是Android提供的用于对音视频进行编解码的类,解码后获得音频和视频原始数据,再使用AudioTrack和SurfaceView分别播放音频和视频,视频播放分为原生开发和NDK开发两种

音频和视频播放最重要的是将音视频同步,同步思路:https://hanshuliang.blog.youkuaiyun.com/article/details/104891200

一、MediaCodec硬解码

主要涉及两个类:MediaExtractor和MediaCodec
MediaExtractor负责解析媒体数据,拿到未解码的压缩数据,MediaCodec则将压缩数据解码为原始数据。

MediaExtractor:媒体提取器,从数据源中提取解复用的媒体数据。引用网络文件时,需要android.Manifest.permission#INTERNET权限
MediaFormat:封装描述媒体数据格式的信息,无论是音频还是视频,以及可选的特征元数据
MediaCodec:媒体编解码器

MediaCodec解码数据,将音频数据解码为PCM格式数据,将视频数据解码为NV12格式数据(指定为NV12格式),输入压缩数据,输出PCM格式原始音频数据和NV12格式(需指定)原始视频数据

流程:

  1. 通过MediaCodecList获取手机支持的音视频硬解码格式
  2. 分离出音视频文件中的音频和视频
  3. 获取音频、视频格式,分别判断是否支持硬解码
  4. 获取音频、视频相关参数,并设置硬解码异步回调
  5. 通过异步回调解码,onInputBufferAvailable中输入压缩数据,onOutputBufferAvailable输出解码的原始数据
  6. 音视频同步实现暂停播放(以一个外部时钟为基准)

0. 获取手机支持硬解码的音频、视频格式

    private List<String> videoDecoderInfos = new ArrayList<>();
    private List<String> audioDecoderInfos = new ArrayList<>();
    
    private void getMediaCodecList() {
   
        videoDecoderInfos.clear();
        audioDecoderInfos.clear();
        
        MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
        MediaCodecInfo[] mediaCodecInfos = mediaCodecList.getCodecInfos();
        for (int i = mediaCodecInfos.length - 1; i >= 0; i--) {
   
            MediaCodecInfo codecInfo = mediaCodecInfos[i];
            if (!codecInfo.isEncoder()) {
   
                for (String t : codecInfo.getSupportedTypes()) {
   
                    if (t.startsWith("video/")) {
   
                        videoDecoderInfos.add(t);
                    } else if (t.startsWith("audio/")) {
   
                        audioDecoderInfos.add(t);
                    }
                }
            }
        }
        
        Log.d(TAG, "getMediaCodecList: " + videoDecoderInfos.toString());
        Log.d(TAG, "getMediaCodecList: " + audioDecoderInfos.toString());
    }

例:

支持的视频格式:[video/x-vnd.on2.vp9, video/x-vnd.on2.vp9, video/x-vnd.on2.vp8, video/x-vnd.on2.vp8, video/mp4v-es, video/mp4v-es, video/hevc, video/hevc, video/3gpp, video/3gpp, video/avc, video/avc, video/av01, video/x-vnd.on2.vp9, video/x-vnd.on2.vp8, video/mp4v-es, video/mpeg2, video/hevc, video/3gpp, video/divx4, video/divx, video/avc]

支持的音频格式:[audio/vorbis, audio/raw, audio/opus, audio/opus, audio/mpeg, audio/g711-mlaw, audio/g711-alaw, audio/flac, audio/amr-wb, audio/3gpp, audio/mp4a-latm, audio/vorbis, audio/raw, audio/mpeg, audio/gsm, audio/g711-mlaw, audio/g711-alaw, audio/flac, audio/amr-wb, audio/3gpp, audio/mp4a-latm, audio/flac]

在这里插入图片描述

1. 获取音频和视频

public void readMedia(Uri uri) {
   
    MediaExtractor mediaExtractor = new MediaExtractor();
    try {
   
        mediaExtractor.setDataSource(context, uri, null);
        // 获取通道数
        int trackCount = mediaExtractor.getTrackCount();
        for (int i = 0; i < trackCount; i++) {
   
            MediaFormat mediaFormat = mediaExtractor.getTrackFormat(i);
            String mime = mediaFormat.getString(MediaFormat.KEY_MIME);

            if (mime.startsWith("video/") && videoSupport != 1) {
   
                readVideo(mediaFormat, i);
            } else if (mime.startsWith("audio/") && audioSupport != 1) {
   
                readAudio(mediaFormat, i);
            }
        }
    } catch (IOException e) {
   
        e.printStackTrace();
    } finally {
   
        mediaExtractor.release();
    }
}

2. 获取视频参数并设置解码异步回调

获取视频参数

public void readVideo(MediaFormat mediaFormat, int index) throws IOException {
   
    String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
    // 判断媒体格式是否支持硬解码
    if (!videoDecoderInfos.contains(mime) || audioSupport == -1) {
   
        videoSupport = -1;
        return;
    }
    videoSupport = 1;
    //surface上所需的顺时针旋转的视频的度数
    rotation = 0;
    if (mediaFormat.containsKey(MediaFormat.KEY_ROTATION)) {
   
        rotation = mediaFormat.getInteger(MediaFormat.KEY_ROTATION);
    }
    // 视频帧数
    int videoSampleRateInHz = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
    // 宽高
    videoWidth = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
    videoHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
    // 视频时长
    videoDurationUs = mediaFormat.getLong(MediaFormat.KEY_DURATION);
    //设置解码输出格式为NV12(YUV420SemiPlanar)
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
    
    if (null != videoMediaExtractor){
   
    	videoMediaExtractor.release();
    	videoMediaExtractor = null
    }
    videoMediaExtractor = new MediaExtractor();

    videoMediaExtractor.setDataSource(context, fileUri, null);
    // 选择视频通道
    videoMediaExtractor.selectTrack(index);
    // 创建解码器
    videoMediaCodec = MediaCodec.createDecoderByType(mime);
    // 设置异步回调,输入解码的数据,输出解码的NV12数据
    videoMediaCodec.setCallback(videoCallback, new Handler(videoThread.childLooper));
    //videoMediaCodec.configure(mediaFormat, null, null, 0);
    // 设置surface,用以显示图像,无须手动写入
    videoMediaCodec.configure(mediaFormat, surface, null, 0);
}

解码异步回调

private MediaCodec.Callback videoCallback = new MediaCodec.Callback() {
   
	byte[] data;
	// 输入解码的数据
    @Override
    public void onInputBufferAvailable(@NonNull MediaCodec codec, int inIndex) {
   
    	// 获取输入缓存区
        ByteBuffer inBuffer = codec.getInputBuffer(inIndex);
        // 读取压缩数据
        int size = videoMediaExtractor.readSampleData(inBuffer, 0);
        if (size < 0) {
   
            codec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
        } else {
   
            codec.queueInputBuffer(inIndex, 0, size, videoMediaExtractor.getSampleTime(), 0);
            videoMediaExtractor.advance();
            inBuffer.clear();
        }
    }
	// 输出解码的NV12数据
    @Override
    public void onOutputBufferAvailable(@NonNull MediaCodec codec, int outIndex, @NonNull MediaCodec.BufferInfo outBufferInfo) {
   
    	// 判断是否读完
        if ((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
   
            codec.releaseOutputBuffer(outIndex, true);
            releaseVideo();
            return;
        }
        // 添加图像延时
        if (outBufferInfo.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
   
            sleep(outBufferInfo.presentationTimeUs / 1000 - (System.currentTimeMillis() - startMs));
        }
        // 获取解码后的NV12视频数据
//            if (outBufferInfo.size > 0) {
   
//				  // 获取输出缓存区
//                ByteBuffer outBuffer = codec.getOutputBuffer(outIndex);
//                outBuffer.position(outBufferInfo.offset);
//                outBuffer.limit(outBufferInfo.offset + outBufferInfo.size);
//                if (data == null)
//                    data = new byte[outBufferInfo.size];
//                Arrays.fill(data, (byte) 0);
//                outBuffer.get(data);
//                mediaDecodeCallback.onVideoOutput(data);
//                outBuffer.clear();
//            }
        // videoMediaCodec.configure(mediaFormat, surface, null, 0);
        // 在configure中设置了surface,在调用releaseOutputBuffer时会自动显示图像
        // 释放输出缓存区
        codec.releaseOutputBuffer(outIndex, true);
    }

    @Override
    public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
   
        releaseVideo();
        releaseAudio();
    }

    @Override
    public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
   
    }
};

注意释放资源

private void releaseVideo() {
   
    if (null != videoMediaCodec) {
   
        videoMediaCodec.stop();
        videoMediaCodec.release();
        videoMediaCodec = null;
    }
    if (null != videoMediaExtractor) {
   
        videoMediaExtractor.release();
        videoMediaExtractor = null;
    }
}

3. 获取音频参数并设置解码异步回调

public void readAudio(MediaFormat mediaFormat, int index) throws IOException {
   
    String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
    // 采样率
    sampleRateInHz = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
    int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
    // 音频数据的格式
    audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    if (mediaFormat.containsKey(MediaFormat.KEY_PCM_ENCODING)) {
   
        audioFormat = mediaFormat.getInteger(MediaFormat.KEY_PCM_ENCODING);
    }
    // 音频通道
    channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
    if (channelCount == 1) {
   
        channelConfig = AudioFormat.CHANNEL_OUT_MONO;
    }
    // 音频时长
    audioDurationUs = mediaFormat.getLong(MediaFormat.KEY_DURATION);
    if (null != audioMediaExtractor){
   
    	audioMediaExtractor.release();
    	audioMediaExtractor = null;
    }
    audioMediaExtractor = new MediaExtractor();
    audioMediaExtractor.setDataSource(context, fileUri, null);
    // 选择视频通道
    audioMediaExtractor.selectTrack(index);
    audioMediaCodec = MediaCodec.createDecoderByType(mime);
    // 异步回调
    audioMediaCodec.setCallback(audioCallback, new Handler(audioThread.childLooper));
    audioM
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值