硬解碼播放器上如何實現(xiàn)截GIF功能?

現(xiàn)在主流的播放器都提供了錄制GIF圖的功能。GIF圖就是將一幀幀連續(xù)的圖像連續(xù)的展示出來,形成動畫。所以生成GIF圖可以分成兩步,首先要獲取一組連續(xù)的圖像,第二步是將這組圖像合成一個GIF文件。關(guān)于GIF文件合成,網(wǎng)絡(luò)上有很多開源的工具類。我們今天主要來看下如何從播放器中獲取一組截圖。話不多說先了解下視頻播放的流程。


播放流程
1.1、解碼后從圖像幀池中獲取圖像幀數(shù)據(jù)

從上面流程圖中可以看出,截圖只需要獲取解碼后的圖像幀數(shù)據(jù)即可,即從圖像幀池中拿出指定幀圖像就好了。當(dāng)我們使用FFmpeg軟解碼播放時,圖像幀池在我們自己的代碼里,所以我們可以拿到任意幀。但是但我們使用系統(tǒng)MediaCodec接口硬解碼播放視頻時,視頻解碼都是系統(tǒng)的MediaCodec模塊來做的,如果我們想要從MediaCodec里拿出圖像幀數(shù)據(jù)來就得研究MediaCodec的接口了。

MediaCodec

MediaCodec的工作流程如上圖所示。MediaCodec類是Android底層多媒體框架的一部分,它用來訪問底層編解碼組件,通常與MediaExtractor、MediaSync、Image、Surface和AudioTrack等類一起使用。

簡單的說,編解碼器(Codec)的功能就是把輸入的原始數(shù)據(jù)處理成可用的輸出數(shù)據(jù)。它使用一組input buffer和一組output buffer來異步的處理數(shù)據(jù)。一個簡單的數(shù)據(jù)處理流程大致分三步:

  1. MediaCodec獲取一個input buffer,然后把從數(shù)據(jù)源中拆包出來的原始數(shù)據(jù)填到這個input buffer中;
  2. 把填滿原始數(shù)據(jù)的input buffer送到MediaCodec中,MediaCodec會將這些原始數(shù)據(jù)解碼成圖像幀數(shù)據(jù),并將這些圖像幀數(shù)據(jù)放入到output buffer中;
  3. MediaCodec中獲取一個有可用圖像幀數(shù)據(jù)output buffer,然后可以將output buffer輸出到surface或者bitmap中就可以渲染到屏幕或者保存在圖片文件中了。
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
String mime = format.getString(MediaFormat.KEY_MIME);

// 創(chuàng)建視頻解碼器,配置解碼器
MediaCodec mVideoDecoder = MediaCodec.createDecoderByType(mime);
mVideoDecoder.configure(format, surface, null, 0);

// 1、獲取input buffer,將原始視頻數(shù)據(jù)包塞到input buffer中
int inputBufferIndex = mVideoDecoder.dequeueInputBuffer(50000);
ByteBuffer buffer = mVideoDecoder.getInputBuffer(inputBufferIndex);

// 2、將帶有原始視頻數(shù)據(jù)的input buffer送到MediaCodec中解碼,解碼數(shù)據(jù)會放置到output buffer中
mVideoDecoder.queueInputBuffer(mVideoBufferIndex, 0, size, presentationTime, 0);

// 3、獲取帶有視頻幀數(shù)據(jù)的output buffer,釋放output buffer時會將數(shù)據(jù)渲染到在配置解碼器時設(shè)置的surface上
int outputBufferIndex = mVideoDecoder.dequeueOutputBuffer(info, 10000);
mVideoDecoder.releaseOutputBuffer(outputBufferIndex, render);

上面是使用MediaCodec播放視頻的基本流程。我們的目標是在這個播放過程中獲取到一幀視頻圖片。從上面的過程可以看到在獲取視頻幀數(shù)據(jù)的output buffer方法dequeueOutputBuffer返回的不是一個buffer對象,而只是一個buffer序列號,渲染時只將這個outputBufferIndex傳遞給MediaCodecMediaCodec就會將對應(yīng)index的渲染到初始配置是設(shè)置的surface中。要實現(xiàn)截圖就得獲取到output buffer的數(shù)據(jù),我們現(xiàn)在需要的一個通過outputBufferIndex獲取到output buffer方法??戳讼?code>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時設(shè)置了surface,那么我們通過這個接口獲取到的output buffer都是null。原因是當(dāng)我們給MediaCodec時設(shè)置了surface作為數(shù)據(jù)輸出對象時,output buffer直接使用的是native buffer沒有將數(shù)據(jù)映射或者拷貝到ByteBuffer中,這樣會使圖像渲染更加高效。播放器主要的最主要的功能還是要播放,所以設(shè)置surface是必須的,那么在拿不到放置解碼后視頻幀數(shù)據(jù)的ByteBuffer的情況下,我們改怎么實現(xiàn)截圖功能呢?

1.2、渲染后從View中獲取圖像幀數(shù)據(jù)

這時我們轉(zhuǎn)換思路,既然硬解碼后的圖像幀數(shù)據(jù)不方便獲?。ǚ桨?),那么我們能不能等到圖像幀數(shù)據(jù)渲染到View上后再從View中去獲取數(shù)據(jù)呢(方案2)?

截圖方案

我們視頻播放器使用的SurfaceVIew + MediaCodec的方式來實現(xiàn)的。那我們來調(diào)研下從SurfaceVIew 中獲取圖像的技術(shù)實現(xiàn)。然后我們就有了這篇文章《為啥從SurfaceView中獲取不到圖片?》。結(jié)束就是從SurfaceView無法獲取到渲染出來的圖像。為了獲取視頻截圖我們換用TextureView + MediaCodec的方式來實現(xiàn)播放。從TextureView中獲取當(dāng)前顯示幀圖像方法如下。

/**
 * <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;
}

到目前為止完成了一小步,實現(xiàn)了從播放器中獲取一張圖像的功能。接下來我們看下如何獲取一組圖像。

1.3 獲取一組連續(xù)的圖像

單張圖像都獲取成功了,獲取多張圖像還難嗎?由于我們獲取圖片的方式是等到圖像在View中渲染出來后再從View中獲取的。那么問題來了,如要生成一張播放時長為5s的GIF,收集這組圖像是不是真的得持續(xù)5s,讓5s內(nèi)所有數(shù)據(jù)都在View上渲染了一次才能收集到呢?這種體驗肯定是不允許的,為此我們使用類似倍速播放的功能,讓5s內(nèi)的圖像數(shù)據(jù)快速的在View上渲染一遍,以此來快速的獲取5s類的圖像數(shù)據(jù)。

if (isScreenShot) {
    // GIF圖不需要所有幀數(shù)據(jù),定義每秒5張,那么每200ms渲染一幀數(shù)據(jù)即可
    render = (info.presentationTimeUs - lastFrameTimeMs) > 200;
}else{
    // 同步音頻的時間
    render = mediaPlayer.get_sync_info(info.presentationTimeUs) != 0;
}

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

mVideoDecoder.releaseOutputBuffer(mVideoBufferIndex, render);

如上述代碼所示,在截圖模式下圖像渲染不在與音頻同步,這樣就實現(xiàn)了圖像快速渲染。另外就是GIF圖每秒只有幾張圖而已,這里定義是5張,那么只需要從視頻源的每秒30幀數(shù)據(jù)中選出5張圖渲染出來即可。這樣我們就快速的獲取到了5s的圖像數(shù)據(jù)。

獲取到所需的圖像數(shù)據(jù)以后,剩下的就是合成GIF文件了。那這樣就實現(xiàn)了在使用MediaCodec硬解碼播放視頻的情況下生成GIF圖的需求。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容