分享好友 移动开发首页 频道列表

使用MediaCodec进行视频的编码和解码

Android开发  2016-11-13 11:520

在Android中播放视频很简单,只要创建一个MediaPlayer实例,然后设置上DataSource和SurfaceView就可以了。但是播放视频还有一种方式就是使用Android提供的MediaCodec,它可以用于编码和解码。另外如果要播放使用Android Widevine加密的视频则必须使用MediaCodec来完成解密和解码的过程。MediaCodec的工作原理很好理解,如下图所示,有一个输入的ByteBuffers向其输入数据,MediaCodec进行处理后会将其输出到一个输出的ByteBuffers里,典型的生产者消费者模型。下面我们来实现一下使用MediaCodec进行解码和编码。

使用MediaCodec进行视频的编码和解码

解码

首先我们先创建一个包装类,对MediaCodec的一些配置和控制操作给包装起来便于调用。当MediaCodec创建并配置好了之后,就需要周期性地进行releaseOutputBuffer操作输出解码后的内容到Surface。在这里我们使用Rxjava的interval操作符来进行这个周期性的操作。

public class VideoDecoder {
    private final Surface mSurface;
    private MediaCodec mDecoder;
    private Subscriber mSubscriber;

    public VideoDecoder(Surface surface) {
        mSurface = surface;
    }

    public void config(MediaFormat mediaFormat) {
        try {
            mDecoder = MediaCodec.createDecoderByType(Config.VIDEO_MIME);
            mDecoder.configure(mediaFormat, mSurface, null, 0);
            mDecoder.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void config(int width, int height, ByteBuffer csd0) {
        Logger.i("config:" + csd0.limit());
        MediaFormat format = MediaFormat.createVideoFormat(Config.VIDEO_MIME, width, height);
        format.setByteBuffer("csd-0", csd0);
        config(format);
    }

    public int dequeueInputBuffer(long timeout) {
        return mDecoder.dequeueInputBuffer(timeout);
    }

    public ByteBuffer getInputBuffer(int index) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            return mDecoder.getInputBuffers()[index];
        } else {
            return mDecoder.getInputBuffer(index);
        }
    }

    /**
     * queue data to the input buffer of codec
     */
    public void queueInputBuffer(int inIndex, int offset, int size, long presentationTimeUs, int flags) {
        mDecoder.queueInputBuffer(inIndex, offset, size, presentationTimeUs, flags);
    }


    /**
     * index to render the content to the surfaceview
     */
    public void start() {
        Logger.i("index");
        if (mSubscriber != null && !mSubscriber.isUnsubscribed()) {
            mSubscriber.unsubscribe();
        }
        mSubscriber = new Subscriber<Boolean>() {
            @Override
            public void onCompleted() {
                stop();
            }

            @Override
            public void on
Error(Throwable e) {
                stop();
            }

            @Override
            public void onNext(Boolean aBoolean) {
                if (aBoolean) {
                    stop();
                    unsubscribe();
                }

            }
        };

        Observable.interval(Config.INTERVAL, TimeUnit.MILLISECONDS)
                .map(new Func1<Long, Boolean>() {
                    @Override
                    public Boolean call(Long aLong) {
                        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                        int outIndex = mDecoder.dequeueOutputBuffer(info, 10000);
                        if (outIndex > 0) {
                            mDecoder.releaseOutputBuffer(outIndex, true);
                        }

                        if ((info.flags & BUFFER_FLAG_END_OF_STREAM) != 0) {
                            Logger.d("OutputBuffer BUFFER_FLAG_END_OF_STREAM");
                            return true;
                        }
                        return false;
                    }
                })
                .subscribeOn(Schedulers.newThread())
                .subscribe(mSubscriber);

    }

    /**
     * stop mFileDecoder
     */
    public void stop() {
        Logger.e("stop");
        if (mSubscriber != null && !mSubscriber.isUnsubscribed()) {
            mSubscriber.unsubscribe();
        }
        if (mDecoder != null) {
            mDecoder.stop();
            mDecoder.release();
        }
    }
}

解码的过程还需要同MediaExtractor结合起来,根据mime type 从MediaExtractor中取出一条track,可以是video也可以是audio, 然后根据这条track的MediaFormat来对MediaCodec进行配置就完成了准备阶段。然后不断地从MediaExtractor中取出Sample数据,将其填充到输入buffer中。这个过程我们使用了Rxjava来完成,一方面逻辑清晰,另一方面可以让我们很容易地控制填充buffer的速度。代码如下:

Observable.range(0, mMediaExtractor.getTrackCount())
            .filter(new Func1<Integer, Boolean>() {
                @Override
                public Boolean call(Integer integer) {
                    //find the video track
                    MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(integer);
                    String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
                    Logger.d(mime);
                    return mime.startsWith("video");
                }
            })
            .flatMap(new Func1<Integer, Observable<Long>>() {
                @Override
                public Observable<Long> call(Integer integer) {
                    //create mFileDecoder according the video track

                    MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(integer);
                    mMediaExtractor.selectTrack(integer);
                    mDecoder.config(mediaFormat);
                    return Observable.interval(Config.INTERVAL, TimeUnit.MILLISECONDS);
                }
            })
            .map(new Func1<Long, Boolean>() {
                @Override
                public Boolean call(Long aLong) {
                    int inIndex = mDecoder.dequeueInputBuffer(10000);
                    if (inIndex >= 0) {
                        ByteBuffer buffer = mDecoder.getInputBuffer(inIndex);
                        int sampleSize = mMediaExtractor.readSampleData(buffer, 0);
                        if (sampleSize < 0) {
                            Logger.d("Input buffer eos");
                            mDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                            return true;
                        } else {
                            mDecoder.queueInputBuffer(inIndex, 0, sampleSize, mMediaExtractor.getSampleTime(), 0);
                            mMediaExtractor.advance();
                        }
                    }
                    return false;
                }
            })
            .subscribe(mSubscriber);

编码

编码同解码一样,还是一个输入-处理-输出的过程。通过下面的方法,我们可以得到一个用来作为输入的Surface:

mSurface = mCodec.createInputSurface();

得到这个Surface之后,我们首先需要通过lockCanvas获得一个Canvas。 有了Canvas,我们就可以在上面画任何我们想画的东西了, 如画一些圆。

Canvas canvas = mSurface.lockCanvas(null);
try {
    onDraw(canvas);
} finally {
    mSurface.unlockCanvasAndPost(canvas);
}

void onDraw(Canvas canvas) {
    canvas.drawColor(Color.BLUE);

    if (mPaint == null) {
        mPaint = new TextPaint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.YELLOW);
    }
    canvas.drawCircle(OUTPUT_WIDTH / 2, OUTPUT_HEIGHT / 2, currentRadius, mPaint);
    currentRadius += 10;
    currentRadius = currentRadius > 100 ? 10 : currentRadius;
}

在这个Canvas上画的内容会传输MediaCodec进行处理,然后会将编码后的内容输出到MediaCodec的显示Surface上。这个过程需要我们对输入和输出的buffer做一些处理,如输出了一定长度的buffer并release之后,我们就可以通知输入的buffer来输入同样长度的内容。也就是说输入和输出的速度是由我们来控制的。

int status = mCodec.dequeueOutputBuffer(mBufferInfo, 10000);
if (status >= 0) {
    // encoded sample
    ByteBuffer data = mCodec.getOutputBuffer(status);
    if (data != null) {
        final int endOfStream = mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM;
        // pass to whoever listens to
        if (endOfStream == 0 && mLister != null) {
            mLister.onSampleEncoded(mBufferInfo, data);
        }
        // releasing buffer is important
        mCodec.releaseOutputBuffer(status, false);
        if (endOfStream == MediaCodec.BUFFER_FLAG_END_OF_STREAM)
            return true;
    }
}

public void onSampleEncoded(MediaCodec.BufferInfo info, ByteBuffer data) {
    Logger.v("onSample encoded");
    if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
        mDecoder.config(OUTPUT_WIDTH, OUTPUT_HEIGHT, data);
        mDecoder.start();

    } else {
        int inIndex = mDecoder.dequeueInputBuffer(10000);
        if (inIndex >= 0) {
            ByteBuffer buffer = mDecoder.getInputBuffer(inIndex);
            buffer.put(data);
            if (info.size < 0) {
                Logger.d("Input buffer eos");
                mDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            } else {
                mDecoder.queueInputBuffer(inIndex, 0, info.size, info.presentationTimeUs, info.flags);
            }
        }
    }
}

最终我们就可以在输出的SurfaceView上看到不断重复画的圆了。

本文中的源代码在 github

查看更多关于【Android开发】的文章

展开全文
相关推荐
反对 0
举报 0
评论 0
图文资讯
热门推荐
优选好物
更多热点专题
更多推荐文章
Supporting Multiple Screens
术语和概念Screen size 屏幕尺寸又称「屏幕大小」,是屏幕对角线的物理尺寸。单位英寸 inch,比如 Samsung Note4 是 5.7 英寸。Resolution 屏幕分辨率屏幕纵横方向上物理像素的总数,比如 Samsung Note4 是 2560x1440,表示纵向有 2560 个像素,横向有 1440

0评论2017-02-05363

Android插件化(4):OpenAtlasの插件的卸载与更新
如果看过我的前两篇博客Android插件化(2):OpenAtlas插件安装过程分析和Android插件化(3):OpenAtlas的插件重建以及使用时安装,就知道在插件的安装过程中OpenAtlas做了哪些事,那么插件的卸载就只需要把持久化和内存中的内容移除即可。1.插件的卸载插件卸载的

0评论2017-02-05229

个人简历
吴朝晖/男/1993.1本科/南京师范大学中北学院信息系工作年限:1年以内技术博客:wuzhaohui026.github.ioGitHub:https://github.com/wuzhaohui026期望职位:Android开发(初级Android工程师)期望薪资:税前月薪5.5k~7k期望城市:常州工作经历常州慧展信息科技有

0评论2017-02-05126

Android插件化(五):OpenAtlasの四大组件的Hack
引言到目前为止,我们已经分析了OpenAtlas中插件的安装,卸载,更新,以及安装好插件之后组件类的加载过程,但是对于这些是如何引发的还不知道,比如,在宿主的一个Activit中调用startActivity()跳转到插件中的一个Activity,如何判断这个Activity在的插件是否

0评论2017-02-0598

更多推荐