Android 音视频开发【应用篇】【一】视频播放器 | 8月更文挑战

1,985 阅读4分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

经过前两篇【音频篇】和【视频篇】的学习,我们了解了关于音视频采集和播放的一些知识,接下来将进入应用篇,用之前所学知识来完成具有实际意义的功能

一、知识点回顾

经过前两篇的学习,我们对音频视频采集播放有了一定的了解,但之前对于音频和视频我们是分开进行播放,而在实际开发中,我们往往需要正常的播放音频和视频

首先,我们回顾下关于音视频的一些相关知识

1.1 音频

  • 音频的原始格式是PCM,可以使用AudioRecord对麦克风数据进行采样,从而得到PCM数据
  • 对于PCM音频数据,可以使用MediaCodec对其进行硬编码,最终得到AAC编码格式的数据
  • 反过来,对于AAC编码格式的数据,同样可以使用MediaCodec对其进行硬解码,最终得到PCM原始格式数据
  • AAC编码格式的数据可以直接使用一般的播放器播放,PCM原始格式数据可以使用AudioTrack播放

1.2 视频

  • 视频的原始格式是YUVRGB,可以从相机预览中获取NV21YUV420的一种)数据
  • 对于YUV视频数据,可以使用MediaCodec对其进行硬编码,最终得到AVC编码格式的数据
  • 反过来,对于AVC编码格式的数据,同样可以使用MediaCodec对其进行硬解码,最终得到YUV原始格式数据,当然,还可以使用Surface直接渲染解码的数据

除此之外,还介绍了MediaExtractorMediaMuxer的用法

1.3 MediaExtractor

  • 通过MediaExtractor可以分离Mp4(一种封装格式)中的音频和视频轨道
  • 分离之后,可以对齐进行一帧一帧的读取
  • 读取后的数据,一般用于传入MediaCodec进行解码操作

1.4 MediaMuxer

  • 通过MediaMuxer可以合成Mp4,即将音频和视频编码格式写入一个文件
  • 操作的方式也是通过轨道id
  • 获取编码的数据后,传入对应轨道id,写入MediaMuxer中即可

以上,整体回顾了【音频篇】和【视频篇】介绍的内容,下面我们就来根据学到的知识制作一款简单的播放器

1.5 播放/录制流程图

播放录制流程图.jpg

播放

从上往下看,整个过程就是播放

  • 解封装
  • 解码
  • 渲染

录制

从下往上看,整个过程就是录制

  • 采集
  • 编码
  • 合成

二、视频播放器

在之前的解码音频、视频中,我们均使用一个线程进行操作,那么现在是要做一个能够完整播放视频的功能,则需要两个线程,一个处理音频,一个处理视频

2.1 具体步骤

音频线程

  • MediaExtractorMp4中解析音频轨道
  • 初始化音频的MediaCodec
  • 初始化AudioTrack
  • MediaExtractor取出待解码的数据,传入MediaCodec
  • MediaCodec取出解码后的数据,传入AudioTrack
  • 停止播放,释放资源

视频线程

  • MediaExtractor从Mp4中解析视频轨道
  • 初始化视频的MediaCodec
  • MediaExtractor取出待解码的数据,传入MediaCodec
  • MediaCodec取出解码后的数据,直接渲染到Surface
  • 停止播放,释放资源

2.2 具体实现

音频播放线程

解码的过程,在音频解码这一章有详细的介绍

private static class AudioPlayThread extends Thread {
    private static final long TIMEOUT_MS = 2000L;
    private String path;
    private MediaExtractor mediaExtractor;
    private MediaCodec mediaCodec;
    private AudioTrack audioTrack;
    private MediaFormat format;
    private String mime;
    private boolean isStopPlay = false;
    public AudioPlayThread(String path) {
        this.path = path;
    }
    @Override
    public void run() {
        super.run();
        initMediaExtractor();
        initMediaCodec();
        initAudioTrack();
        play();
    }
    private void initMediaExtractor() {
        if (TextUtils.isEmpty(path)) {
            return;
        }
        try {
            mediaExtractor = new MediaExtractor();
            mediaExtractor.setDataSource(path);
            int trackCount = mediaExtractor.getTrackCount();
            for (int i = 0; i < trackCount; i++) {
                format = mediaExtractor.getTrackFormat(i);
                mime = format.getString(MediaFormat.KEY_MIME);
                if (!TextUtils.isEmpty(mime) && mime.startsWith("audio/")) {
                    mediaExtractor.selectTrack(i);
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            mediaExtractor = null;
            format = null;
            mime = null;
        }
    }
    private void initMediaCodec() {
        if (format == null || TextUtils.isEmpty(mime)) {
            return;
        }
        try {
            mediaCodec = MediaCodec.createDecoderByType(mime);
            mediaCodec.configure(format, null, null, 0);
        } catch (IOException e) {
            e.printStackTrace();
            mediaCodec = null;
        }
    }
    private void initAudioTrack() {
        if (format == null) {
            return;
        }
        int sampleRateInHz = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
        int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
        int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        int bufferSizeInBytes = AudioTrack.getMinBufferSize(
                sampleRateInHz,
                channelConfig, audioFormat
        );
        audioTrack = new AudioTrack(
                AudioManager.STREAM_MUSIC,
                sampleRateInHz,
                channelConfig,
                audioFormat,
                bufferSizeInBytes,
                AudioTrack.MODE_STREAM
        );
    }
    private void play() {
        if (mediaExtractor == null || mediaCodec == null || audioTrack == null) {
            return;
        }
        long startMs = System.currentTimeMillis();
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        mediaCodec.start();
        audioTrack.play();
        for (; ; ) {
            if (isStopPlay) {
                release();
                break;
            }
            int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
            if (inputBufferId >= 0) {
                ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
                int readSize = -1;
                if (inputBuffer != null) {
                    readSize = mediaExtractor.readSampleData(inputBuffer, 0);
                }
                if (readSize <= 0) {
                    mediaCodec.queueInputBuffer(
                            inputBufferId,
                            0,
                            0,
                            0,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    isStopPlay = true;
                } else {
                    mediaCodec.queueInputBuffer(inputBufferId, 0, readSize, mediaExtractor.getSampleTime(), 0);
                    mediaExtractor.advance();
                }
            }
            int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
            if (outputBufferId >= 0) {
                ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
                if (outputBuffer != null && info.size > 0) {
                    while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
                        try {
                            sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            break;
                        }
                    }
                    byte[] data = new byte[info.size];
                    outputBuffer.get(data);
                    outputBuffer.clear();
                    audioTrack.write(data, 0, info.size);
                }
                mediaCodec.releaseOutputBuffer(outputBufferId, false);
            }
        }
    }
    void stopPlay() {
        isStopPlay = true;
        try {
            join(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private void release() {
        if (mediaExtractor != null) {
            mediaExtractor.release();
            mediaExtractor = null;
        }
        if (mediaCodec != null) {
            mediaCodec.stop();
            mediaCodec.release();
            mediaCodec = null;
        }
        if (audioTrack != null) {
            audioTrack.stop();
            audioTrack.release();
            audioTrack = null;
        }
    }
}

视频播放线程 解码的过程,在视频解码这一章有详细的介绍

private static class VideoPlayThread extends Thread {
    private static final long TIMEOUT_MS = 2000L;
    private String path;
    private Surface surface;
    private MediaExtractor mediaExtractor;
    private MediaCodec mediaCodec;
    private MediaFormat format;
    private String mime;
    private boolean isStopPlay = false;
    public VideoPlayThread(String path, Surface surface) {
        this.path = path;
        this.surface = surface;
    }
    @Override
    public void run() {
        super.run();
        initMediaExtractor();
        initMediaCodec();
        play();
    }
    private void initMediaExtractor() {
        if (TextUtils.isEmpty(path)) {
            return;
        }
        try {
            mediaExtractor = new MediaExtractor();
            mediaExtractor.setDataSource(path);
            int trackCount = mediaExtractor.getTrackCount();
            for (int i = 0; i < trackCount; i++) {
                format = mediaExtractor.getTrackFormat(i);
                mime = format.getString(MediaFormat.KEY_MIME);
                if (!TextUtils.isEmpty(mime) && mime.startsWith("video/")) {
                    mediaExtractor.selectTrack(i);
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            mediaExtractor = null;
            format = null;
            mime = null;
        }
    }
    private void initMediaCodec() {
        if (format == null || TextUtils.isEmpty(mime) || surface == null) {
            return;
        }
        try {
            mediaCodec = MediaCodec.createDecoderByType(mime);
            mediaCodec.configure(format, surface, null, 0);
        } catch (IOException e) {
            e.printStackTrace();
            mediaCodec = null;
        }
    }
    private void play() {
        if (mediaExtractor == null || mediaCodec == null) {
            return;
        }
        long startMs = System.currentTimeMillis();
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        mediaCodec.start();
        for (; ; ) {
            if (isStopPlay) {
                release();
                break;
            }
            int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
            if (inputBufferId >= 0) {
                ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
                int readSize = -1;
                if (inputBuffer != null) {
                    readSize = mediaExtractor.readSampleData(inputBuffer, 0);
                }
                if (readSize <= 0) {
                    mediaCodec.queueInputBuffer(
                            inputBufferId,
                            0,
                            0,
                            0,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    isStopPlay = true;
                } else {
                    mediaCodec.queueInputBuffer(inputBufferId, 0, readSize, mediaExtractor.getSampleTime(), 0
                    mediaExtractor.advance();
                }
            }
            int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
            if (outputBufferId >= 0) {
                ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
                if (outputBuffer != null && info.size > 0) {
                    while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
                        try {
                            sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            break;
                        }
                    }
                    byte[] data = new byte[info.size];
                    outputBuffer.get(data);
                    outputBuffer.clear();
                    // 得到的data数据就是YUV数据,可以拿去做对应的业务
                }
                mediaCodec.releaseOutputBuffer(outputBufferId, true);
            }
        }
    }
    void stopPlay() {
        isStopPlay = true;
        try {
            join(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private void release() {
        if (mediaExtractor != null) {
            mediaExtractor.release();
            mediaExtractor = null;
        }
        if (mediaCodec != null) {
            mediaCodec.stop();
            mediaCodec.release();
            mediaCodec = null;
        }
        surface = null;
    }
}

外部调用

外部需要调用的话,则需要创建两个线程对象,并启动即可,在不需要时,记得及时停止播放,并释放资源

private AudioPlayThread audioPlayThread;
private VideoPlayThread videoPlayThread;
​
public void stat(String path, Surface surface) {
    stop();
    audioPlayThread = new AudioPlayThread(path);
    videoPlayThread = new VideoPlayThread(path, surface);
    audioPlayThread.start();
    videoPlayThread.start();
}
​
public void stop() {
    if (audioPlayThread != null) {
        audioPlayThread.stopPlay();
        audioPlayThread = null;
    }
    if (videoPlayThread != null) {
        videoPlayThread.stopPlay();
        videoPlayThread = null;
    }
}

至此,一个简单的视频播放器就完成了!!!

三、GitHub

VideoPlayer.java

VideoPlayActivity.java