硬解码播放器上如何实现截GIF功能?

现在主流的播放器都提供了录制GIF图的功能。GIF图就是将一帧帧连续的图像连续的展示出来,形成动画。所以生成GIF图可以分成两步,首先要获取一组连续的图像,第二步是将这组图像合成一个GIF文件。关于GIF文件合成,网络上有很多开源的工具类。我们今天主要来看下如何从播放器中获取一组截图。话不多说先了解下视频播放的流程。


播放流程
1.1、解码后从图像帧池中获取图像帧数据

从上面流程图中可以看出,截图只需要获取解码后的图像帧数据即可,即从图像帧池中拿出指定帧图像就好了。当我们使用FFmpeg软解码播放时,图像帧池在我们自己的代码里,所以我们可以拿到任意帧。但是但我们使用系统MediaCodec接口硬解码播放视频时,视频解码都是系统的MediaCodec模块来做的,如果我们想要从MediaCodec里拿出图像帧数据来就得研究MediaCodec的接口了。

MediaCodec

MediaCodec的工作流程如上图所示。MediaCodec类是Android底层多媒体框架的一部分,它用来访问底层编解码组件,通常与MediaExtractor、MediaSync、Image、Surface和AudioTrack等类一起使用。

简单的说,编解码器(Codec)的功能就是把输入的原始数据处理成可用的输出数据。它使用一组input buffer和一组output buffer来异步的处理数据。一个简单的数据处理流程大致分三步:

  1. MediaCodec获取一个input buffer,然后把从数据源中拆包出来的原始数据填到这个input buffer中;
  2. 把填满原始数据的input buffer送到MediaCodec中,MediaCodec会将这些原始数据解码成图像帧数据,并将这些图像帧数据放入到output buffer中;
  3. MediaCodec中获取一个有可用图像帧数据output buffer,然后可以将output buffer输出到surface或者bitmap中就可以渲染到屏幕或者保存在图片文件中了。
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
String mime = format.getString(MediaFormat.KEY_MIME);

// 创建视频解码器,配置解码器
MediaCodec mVideoDecoder = MediaCodec.createDecoderByType(mime);
mVideoDecoder.configure(format, surface, null, 0);

// 1、获取input buffer,将原始视频数据包塞到input buffer中
int inputBufferIndex = mVideoDecoder.dequeueInputBuffer(50000);
ByteBuffer buffer = mVideoDecoder.getInputBuffer(inputBufferIndex);

// 2、将带有原始视频数据的input buffer送到MediaCodec中解码,解码数据会放置到output buffer中
mVideoDecoder.queueInputBuffer(mVideoBufferIndex, 0, size, presentationTime, 0);

// 3、获取带有视频帧数据的output buffer,释放output buffer时会将数据渲染到在配置解码器时设置的surface上
int outputBufferIndex = mVideoDecoder.dequeueOutputBuffer(info, 10000);
mVideoDecoder.releaseOutputBuffer(outputBufferIndex, render);

上面是使用MediaCodec播放视频的基本流程。我们的目标是在这个播放过程中获取到一帧视频图片。从上面的过程可以看到在获取视频帧数据的output buffer方法dequeueOutputBuffer返回的不是一个buffer对象,而只是一个buffer序列号,渲染时只将这个outputBufferIndex传递给MediaCodecMediaCodec就会将对应index的渲染到初始配置是设置的surface中。要实现截图就得获取到output buffer的数据,我们现在需要的一个通过outputBufferIndex获取到output buffer方法。看了下MediaCodec的接口还真有这样的方法,详细如下:

/**
 * Returns a read-only ByteBuffer for a dequeued output buffer
 * index. The position and limit of the returned buffer are set
 * to the valid output data.
 *
 * After calling this method, any ByteBuffer or Image object
 * previously returned for the same output index MUST no longer
 * be used.
 *
 * @param index The index of a client-owned output buffer previously
 *              returned from a call to {@link #dequeueOutputBuffer},
 *              or received via an onOutputBufferAvailable callback.
 *
 * @return the output buffer, or null if the index is not a dequeued
 * output buffer, or the codec is configured with an output surface.
 *
 * @throws IllegalStateException if not in the Executing state.
 * @throws MediaCodec.CodecException upon codec error.
 */
@Nullable
public ByteBuffer getOutputBuffer(int index) {
    ByteBuffer newBuffer = getBuffer(false /* input */, index);
    synchronized(mBufferLock) {
        invalidateByteBuffer(mCachedOutputBuffers, index);
        mDequeuedOutputBuffers.put(index, newBuffer);
    }
    return newBuffer;
}

注意接口文档对返回值的描述 return the output buffer, or null if the index is not a dequeued output buffer, or the codec is configured with an output surface. 也就是说如果我们在初始化MediaCodec时设置了surface,那么我们通过这个接口获取到的output buffer都是null。原因是当我们给MediaCodec时设置了surface作为数据输出对象时,output buffer直接使用的是native buffer没有将数据映射或者拷贝到ByteBuffer中,这样会使图像渲染更加高效。播放器主要的最主要的功能还是要播放,所以设置surface是必须的,那么在拿不到放置解码后视频帧数据的ByteBuffer的情况下,我们改怎么实现截图功能呢?

1.2、渲染后从View中获取图像帧数据

这时我们转换思路,既然硬解码后的图像帧数据不方便获取(方案1),那么我们能不能等到图像帧数据渲染到View上后再从View中去获取数据呢(方案2)?

截图方案

我们视频播放器使用的SurfaceVIew + MediaCodec的方式来实现的。那我们来调研下从SurfaceVIew 中获取图像的技术实现。然后我们就有了这篇文章《为啥从SurfaceView中获取不到图片?》。结束就是从SurfaceView无法获取到渲染出来的图像。为了获取视频截图我们换用TextureView + MediaCodec的方式来实现播放。从TextureView中获取当前显示帧图像方法如下。

/**
 * <p>Returns a {@link android.graphics.Bitmap} representation of the content
 * of the associated surface texture. If the surface texture is not available,
 * this method returns null.</p>
 *
 * <p>The bitmap returned by this method uses the {@link Bitmap.Config#ARGB_8888}
 * pixel format.</p>
 *
 * <p><strong>Do not</strong> invoke this method from a drawing method
 * ({@link #onDraw(android.graphics.Canvas)} for instance).</p>
 *
 * <p>If an error occurs during the copy, an empty bitmap will be returned.</p>
 *
 * @param width The width of the bitmap to create
 * @param height The height of the bitmap to create
 *
 * @return A valid {@link Bitmap.Config#ARGB_8888} bitmap, or null if the surface
 *         texture is not available or width is &lt;= 0 or height is &lt;= 0
 *
 * @see #isAvailable()
 * @see #getBitmap(android.graphics.Bitmap)
 * @see #getBitmap()
 */
public Bitmap getBitmap(int width, int height) {
    if (isAvailable() && width > 0 && height > 0) {
        return getBitmap(Bitmap.createBitmap(getResources().getDisplayMetrics(),
                width, height, Bitmap.Config.ARGB_8888));
    }
    return null;
}

到目前为止完成了一小步,实现了从播放器中获取一张图像的功能。接下来我们看下如何获取一组图像。

1.3 获取一组连续的图像

单张图像都获取成功了,获取多张图像还难吗?由于我们获取图片的方式是等到图像在View中渲染出来后再从View中获取的。那么问题来了,如要生成一张播放时长为5s的GIF,收集这组图像是不是真的得持续5s,让5s内所有数据都在View上渲染了一次才能收集到呢?这种体验肯定是不允许的,为此我们使用类似倍速播放的功能,让5s内的图像数据快速的在View上渲染一遍,以此来快速的获取5s类的图像数据。

if (isScreenShot) {
    // GIF图不需要所有帧数据,定义每秒5张,那么每200ms渲染一帧数据即可
    render = (info.presentationTimeUs - lastFrameTimeMs) > 200;
}else{
    // 同步音频的时间
    render = mediaPlayer.get_sync_info(info.presentationTimeUs) != 0;
}

if (render) {
    lastFrameTimeMs = info.presentationTimeUs;
}

mVideoDecoder.releaseOutputBuffer(mVideoBufferIndex, render);

如上述代码所示,在截图模式下图像渲染不在与音频同步,这样就实现了图像快速渲染。另外就是GIF图每秒只有几张图而已,这里定义是5张,那么只需要从视频源的每秒30帧数据中选出5张图渲染出来即可。这样我们就快速的获取到了5s的图像数据。

获取到所需的图像数据以后,剩下的就是合成GIF文件了。那这样就实现了在使用MediaCodec硬解码播放视频的情况下生成GIF图的需求。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351