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

   2016-11-13 0
核心提示:在Android中播放视频很简单,只要创建一个MediaPlayer实例,然后设置上DataSource和SurfaceView就可以了。但是播放视频还有一种方式就是使用Android提供的MediaCodec,它可以用于编码和解码。另外如果要播放使用Android Widevine加密的视频则必须使用MediaCod

在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

 
标签: 安卓开发
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 安卓中通知功能的具体实现
    安卓中通知功能的具体实现
    通知[Notification]是Android中比较有特色的功能,当某个应用程序希望给用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知实现。使用通知的步骤1、需要一个NotificationManager来获得NotificationManager manager = (NotificationManager
    02-05 安卓开发
  • Android view系统分析-setContentView
    Android view系统分析-setContentView
    第一天上班,列了一下今年要学习的东西。主要就是深入学习Android相关的系统源代码,夯实基础。对于学习Android系统源代码,也没什么大概,就从我们平常使用最基础的东西学起,也就是从view这个切入点开始学习Android的源码,在没分析源码之前,我们有的时候
    02-05 安卓开发
  • 如何进行网络视频截图/获取视频的缩略图
    如何进行网络视频截图/获取视频的缩略图
    小编导读:获取视频的缩略图,截图正在播放的视频某一帧,是在音视频开发中,常遇到的问题。本文是主要用于点播中截图视频,同时还可以获取点播视频的缩略图进行显示,留下一个问题,如下图所示, 如果要获取直播中节目视频缩略图,该怎么做呢?(ps:直播是直
  • Android NDK 层发起 HTTP 请求的问题及解决
    Android NDK 层发起 HTTP 请求的问题及解决
    前言新的一年,大家新年快乐~~鸡年大吉!本次给大家带来何老师的最新文章~虽然何老师还在过节,但依然放心不下广大开发者,在此佳节还未结束之际,给大家带来最新的技术分享~ 事件的起因不说了,总之是需要实现一个 NDK 层的网络请求。为了多端适用,还是选择
  • Android插件化(六): OpenAtlasの改写aapt以防止资源ID冲突
    Android插件化(六): OpenAtlasの改写aapt以防
    引言Android应用程序的编译中,负责资源打包的是aapt,如果不对打包后的资源ID进行控制,就会导致插件中的资源ID冲突。所以,我们需要改写aapt的源码,以达到通过某种方式传递资源ID的Package ID,通过aapt打包时获取到这个Package ID并且应用才插件资源的命名
    02-05 安卓开发
  • Android架构(一)MVP架构在Android中的实践
    Android架构(一)MVP架构在Android中的实践
    为什么要重视程序的架构设计 对程序进行架构设计的原因,归根结底是为了 提高生产力 。通过设计是程序模块化,做到模块内部的 高聚合 和模块之间的 低耦合 (如依赖注入就是低耦合的集中体现)。 这样做的好处是使得程序开发过程中,开发人员主需要专注于一点,
    02-05 安卓开发
  • 安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件 作者: 飞龙 这个教程中我们要分析一个锁机软件。像这种软件都比较简单,完全可以顺着入口看下去,但我这里还是用关键点来定位。首先这个软件的截图是这样,进入这个界面之后,除非退出模拟器,否则没办法回到桌面。上面那
    02-05 安卓开发
  • Android插件化(二):OpenAtlas插件安装过程分析
    Android插件化(二):OpenAtlas插件安装过程分析
    在前一篇博客 Android插件化(一):OpenAtlas架构以及实现原理概要 中,我们对应Android插件化存在的问题,实现原理,以及目前的实现方案进行了简单的叙述。从这篇开始,我们要深入到OpenAtlas的源码中进行插件安装过程的分析。 插件的安装分为3种:宿主启动时立
    02-05 安卓开发
  • [译] Android API 指南
    [译] Android API 指南
    众所周知,Android开发者有中文网站了,API 指南一眼看去最左侧的菜单都是中文,然而点进去内容还是很多是英文,并没有全部翻译,我这里整理了API 指南的目录,便于查看,如果之前还没有通读,现在可以好好看一遍。注意,如果标题带有英文,说明官方还没有翻
  • 使用FileProvider解决file:// URI引起的FileUriExposedException
    使用FileProvider解决file:// URI引起的FileUri
    问题以下是一段简单的代码,它调用系统的相机app来拍摄照片:void takePhoto(String cameraPhotoPath) {File cameraPhoto = new File(cameraPhotoPath);Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);takePhotoIntent.putExtra(Medi
    02-05 安卓开发
点击排行