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

2019-03-01 14:39:35 / 打印

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

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

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

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

从 MediaCodec获取一个 input buffer,然后把从数据源中拆包出来的原始数据填到这个 input buffer中;

把填满原始数据的 input buffer送到 MediaCodec中, MediaCodec会将这些原始数据解码成图像帧数据,并将这些图像帧数据放入到 output buffer中;

从 MediaCodec中获取一个有可用图像帧数据 output buffer,然后可以将 output buffer输出到 surface或者 bitmap中就可以渲染到屏幕或者保存在图片文件中了。

MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);

一个、kmplayer中文版以及文件名就是插件;

String mime = format.getString(MediaFormat.KEY_MIME);

// 创建视频解码器,配置解码器

MediaCodec mVideoDecoder = MediaCodec.createDecoderByType(mime);

mVideoDecoder.configure(format, surface, null, 0);

2. 书就行动 宽阔繁体让kmplayer官网

// 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传递给 MediaCodec, MediaCodec就会将对应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

* @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.

public ByteBuffer getOutputBuffer(int index) {

ByteBuffer newBuffer = getBuffer(false /* input */, index);

synchronized(mBufferLock) {

invalidateByteBuffer(mCachedOutputBuffers, index);

mDequeuedOutputBuffers.put(index, newBuffer);

return newBuffer;

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

这时我们转换思路,既然硬解码后的图像帧数据不方便获取(方案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;

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

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

if (isScreenShot) {

// GIF图不需要所有帧数据,定义每秒5张,那么每200ms渲染一帧数据即可

render = (info.presentationTimeUs - lastFrameTimeMs) > 200;

// 同步音频的时间

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 图的需求。