5.使用MediaExtractor+MediaCodec+SurfaceView播放视频文件

使用MediaExtractor+MediaCodec+SurfaceView播放视频文件

整体类似于上一个播放音频的项目,只不过这里把音频变成了视频。

音频是通过AudioTrack来播放,视频的话可以直接渲染到SurfaceView中。

解码器配置

// MediaCodec 解码器的配置
videoCodec = MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME));
Log.i(TAG, "MediaCodec.createDecoderByType‘s videoFormat is " + videoFormat.getString(MediaFormat.KEY_MIME));
videoCodec.configure(videoFormat, mSurface, null, 0);
videoCodec.start();
public void configure (MediaFormat format, 
                Surface surface, 
                MediaCrypto crypto, 
                int flags)

configure中的第二个参数我们在audio中设置为null,在此处应该设置为surface,则可以解码后直接将视频渲染到surface中。

功能实现–暂停、缓存

不像AudioTrack,可以利用AudioTrack.pause()和AudioTrack.plau()来控制播放和暂停。

因为这里没有这些定义好的接口可以直接利用。

分析

step1:初始化配置,MediaExtractor将视频分轨,得到视频轨道,配置MediaCodec

step2:解码、渲染

要在第二步解码时实现暂停:

  1. 获取inputBufferId
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
  1. 获取inputBuffer
ByteBuffer inputBuffer = codec.getInputBuffer(…);
  1. 将提取的数据加载到inputBuffer
int sampleSize = extractor.readSampleData(inputBuffer, 0);
  1. 将填满数据的inputBuffer提交到编码队列
codec.queueInputBuffer(inputBufferId, …);

这里利用一个中间缓存队列来保存分离出的视频文件,一帧一帧的出队,将其提交到编码队列。3

利用一个线程来将分离出的数据保存在extractorBufferQueue中,这是一个可阻塞的队列。

    LinkedBlockingQueue<ByteBuffer> extractorBufferQueue = new LinkedBlockingQueue<>(100);
    ......
    private class VideoProvider extends Thread {
        @Override
        public void run() {
            super.run();
            boolean isEOS = false;
            while (!isEOS && !Thread.currentThread().isInterrupted()){
                ByteBuffer inputBuffer = ByteBuffer.allocate(230400);
                int sampleSize = extractor.readSampleData(inputBuffer, 0);
                if (sampleSize > 0){
                    try {
                        extractorBufferQueue.put(inputBuffer);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    extractor.advance();
                }else {
                    isEOS = true;
                    break;
                }
            }
            extractor.release();
        }
    }

通过出队列的方式来实现给解码器数据。

inputBuffer.put(extractorBufferQueue.take());

如果暂停了,就利用锁的结构将解码器锁住进入阻塞状态。

但是此时不影响视频的缓存(即,MediaExtractor对视频的分离)

锁、LinkedBlockingQueue、ByteBuffer

在pause后,对playLock.lock()加锁,由于解码时需要对锁的获取,所以暂停时无法播放,点击start后,解锁playLock.unlock();

深入理解Java系列 | LinkedBlockingQueue用法详解 - 掘金 (juejin.cn)

但是缓存视频并不受锁的影响,只要缓存队列有空间就可以继续分离加载视频,而 LinkedBlockingQueue实现了 BlockingQueue接口,本项目主要利用了put和take

方法抛出异常返回特定值阻塞阻塞特定时间
入队add(e)offer(e)put(e)offer(e, time, unit)
出队remove()poll()take()poll(time, unit)
获取队首元素element()peek()不支持不支持

如果队列有空间就一直加载即可。

一文搞懂ByteBuffer使用与原理 - 掘金 (juejin.cn)

利用put(ByteBuffer)方法,将从LinkedBlockingQueue取出的元素直接加载到inputBuffer中,去实现解码渲染。

Code

public class MediaExtractorVideoActivity extends AppCompatActivity implements View.OnClickListener, SurfaceHolder.Callback {
    private static final String TAG = "MediaExtractorVideo";

    private Button mStartButton;
    private Button mPauseButton;
    private Button mStopButton;
    private Surface mSurface;
    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceHolder;


    private MediaExtractor extractor;
    private MediaCodec videoCodec;

    private boolean isPlayed = false;
    public volatile boolean isPause = false;
    private Lock playLock;

LinkedBlockingQueue<ByteBuffer> extractorBufferQueue = new LinkedBlockingQueue<>(100);

    private VideoThread videoThread;
    private VideoProvider videoProvider;

    public static Intent newIntent(Context packageContext) {
        Intent intent = new Intent(packageContext, MediaExtractorVideoActivity.class);
        return intent;
}


    @Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_extractor_video);
bindViews();

}

    private void initPlayer() throws IOException {
        // 选取视频轨道
extractor = new MediaExtractor();
        try {
            extractor.setDataSource("//sdcard/Movies/big_buck_bunny.mp4");
//            extractor.setDataSource("//sdcard/Movies/lesson.mp4");
} catch (IOException e) {
            e.printStackTrace();
}
        int numTracks = extractor.getTrackCount();
        int trackIndex = 0;
        for (int i = 0; i < numTracks; ++i) {
            MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
// 可获取该format所对应的数据类型(以键值对实现,KEY_MIME这个key所对应value记录的是该数据流类型,
// 视频以video/开头,音频以video/开头)
if (mime.startsWith("video/")) {
                trackIndex = i;
}
        }
        MediaFormat videoFormat = extractor.getTrackFormat(trackIndex);
Log.i(TAG, "The video format is" + videoFormat);
extractor.selectTrack(trackIndex);

// MediaCodec 解码器的配置
videoCodec = MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME));
Log.i(TAG, "MediaCodec.createDecoderByType‘s videoFormat is " + videoFormat.getString(MediaFormat.KEY_MIME));
videoCodec.configure(videoFormat, mSurface, null, 0);
videoCodec.start();
}

    private void decodeVideo() throws InterruptedException {

        MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo();

        long startMs = System.currentTimeMillis();
ByteBuffer inputBuffer;
        boolean isEOS = false;
        while (!isEOS && !Thread.currentThread().isInterrupted()) {
            // 获取可用的输入缓冲区的索引
int inputBufferId = videoCodec.dequeueInputBuffer(1000);
Log.i(TAG, "The inputBufferId is " + inputBufferId);
            if (inputBufferId >= 0) {
                playLock.lock();
// 获取输入缓冲区,并向缓冲区写入数据
inputBuffer = videoCodec.getInputBuffer(inputBufferId);
inputBuffer.put(extractorBufferQueue.take());
//                int sampleSize = extractor.readSampleData(inputBuffer, 0);
int sampleSize = inputBuffer.position();
                if (sampleSize > 0) {
                    // 将填满数据的inputBuffer提交到编码队列
videoCodec.queueInputBuffer(inputBufferId, 0, sampleSize, extractor.getSampleTime(), 0);

} else {
                    isEOS = true;
                    break;
}
                playLock.unlock();
}
            // 获取已成功编解码的输出缓冲区的索引
int outputBufferId = videoCodec.dequeueOutputBuffer(videoBufferInfo, 1000);
Log.i(TAG, "the outputBufferId is " + outputBufferId);

            if (outputBufferId >= 0) {
                //将OutputBuffers[outputIndex]中储存的解码后的数据传到surface中渲染
//设置true是指先把视频画面render到surface,然后再把这个buffer里的数据release掉(但要确保在configure的时候有绑定surface)
//设置为false 则只是简单释放资源,在音频解码的情况下设置false,将数据载入AudioTrack后,直接release掉
//这里只需要传outputBuffer的索引即可
// 释放缓冲区
//                sleepRender(videoBufferInfo, startMs);
videoCodec.releaseOutputBuffer(outputBufferId, true);
}
        }
        videoCodec.stop();
videoCodec.release();
}

    //延迟渲染
private void sleepRender(MediaCodec.BufferInfo audioBufferInfo, long startMs) {
        while (audioBufferInfo.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
            try {
                Thread.sleep(1);
} catch (InterruptedException e) {
                e.printStackTrace();
                break;
}
        }
    }

    private void bindViews() {
        mStartButton = (Button) findViewById(R.id.btn_media_extractor_video_start);
mPauseButton = (Button) findViewById(R.id.btn_media_extractor_video_pause);
mStopButton = (Button) findViewById(R.id.btn_media_extractor_video_stop);

mStartButton.setOnClickListener(this);
mPauseButton.setOnClickListener(this);
mStopButton.setOnClickListener(this);

mSurfaceView = (SurfaceView) findViewById(R.id.sfv_media_extractor_video);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(this);
mSurface = mSurfaceHolder.getSurface();
}

    @Override
public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_media_extractor_video_start:

                if (!isPlayed){
                    // 没有开始播放,初始化
try {
                        initPlayer();
} catch (IOException e) {
                        e.printStackTrace();
}
                    isPause = false;
playLock = new ReentrantLock();
extractorBufferQueue = new LinkedBlockingQueue<>(100);

// 线程开始执行
videoThread = new VideoThread();
videoProvider = new VideoProvider();
videoThread.start();
videoProvider.start();
}
                isPlayed = true;

                if (isPause){
                    playLock.unlock();
}
                isPause = false;

mStartButton.setEnabled(false);
mPauseButton.setEnabled(true);
mStopButton.setEnabled(true);
                break;
            case R.id.btn_media_extractor_video_pause:

                isPause = true;
playLock.lock();

mStartButton.setEnabled(true);
mPauseButton.setEnabled(false);
mStopButton.setEnabled(false);
                break;
            case R.id.btn_media_extractor_video_stop:

                videoThread.interrupt();
videoProvider.interrupt();
isPlayed = false;

mStartButton.setEnabled(true);
mPauseButton.setEnabled(false);
mStopButton.setEnabled(false);
                break;
}
    }

    @Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {

    }

    @Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {

    }

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

    }

    private class VideoThread extends Thread{
        @Override
public void run() {
            super.run();
            try {
                decodeVideo();
} catch (InterruptedException e) {
                e.printStackTrace();
}
        }
    }

    private class VideoProvider extends Thread {
        @Override
public void run() {
            super.run();
            boolean isEOS = false;
            while (!isEOS && !Thread.currentThread().isInterrupted()){
                ByteBuffer inputBuffer = ByteBuffer.allocate(230400);
                int sampleSize = extractor.readSampleData(inputBuffer, 0);
                if (sampleSize > 0){
                    try {
                        extractorBufferQueue.put(inputBuffer);
} catch (InterruptedException e) {
                        e.printStackTrace();
}
                    extractor.advance();
}else {
                    isEOS = true;
                    break;
}
            }
            extractor.release();
}
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值