如今主流的播放器都提供了录制GIF图的功能。GIF图就是将一帧帧连续的图像连续的展现出来,造成动画。因此生成GIF图能够分红两步,首先要获取一组连续的图像,第二步是将这组图像合成一个GIF文件。关于GIF文件合成,网络上有不少开源的工具类。咱们今天主要来看下如何从播放器中获取一组截图。话很少说先了解下视频播放的流程。 android
从上面流程图中能够看出,截图只须要获取解码后的图像帧数据便可,即从图像帧池中拿出指定帧图像就行了。当咱们使用FFmpeg软解码播放时,图像帧池在咱们本身的代码里,因此咱们能够拿到任意帧。可是但咱们使用系统MediaCodec
接口硬解码播放视频时,视频解码都是系统的MediaCodec
模块来作的,若是咱们想要从MediaCodec
里拿出图像帧数据来就得研究MediaCodec
的接口了。bash
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);
String mime = format.getString(MediaFormat.KEY_MIME);
// 建立视频解码器,配置解码器
MediaCodec mVideoDecoder = MediaCodec.createDecoderByType(mime);
mVideoDecoder.configure(format, surface, null, 0);
// 一、获取input buffer,将原始视频数据包塞到input buffer中
int inputBufferIndex = mVideoDecoder.dequeueInputBuffer(50000);
ByteBuffer buffer = mVideoDecoder.getInputBuffer(inputBufferIndex);
// 二、将带有原始视频数据的input buffer送到MediaCodec中解码,解码数据会放置到output buffer中
mVideoDecoder.queueInputBuffer(mVideoBufferIndex, 0, size, presentationTime, 0);
// 三、获取带有视频帧数据的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
* 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的状况下,咱们改怎么实现截图功能呢?ide
这时咱们转换思路,既然硬解码后的图像帧数据不方便获取(方案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 <= 0 or height is <= 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类的图像数据。ui
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的图像数据。this
获取到所需的图像数据之后,剩下的就是合成GIF文件了。那这样就实现了在使用MediaCodec
硬解码播放视频的状况下生成GIF图的需求。