播放器拖動預覽,讓用戶提前瞭解視頻的波瀾迭起情節,先走馬觀花看一遍精彩部分,滿足一下好奇心,這就是拖動預覽的意義所在。那麼我們該如何打造高性能、高效率、高可靠的拖動預覽呢?首先,小圖預覽強調足夠小,因爲預覽畫面分辨率沒必要高清,分辨率越小解碼速度越快、佔用內存與CPU資源越低;其次,硬解優先,綁定Surface,解碼後直接渲染到Surface上;另外,不必要解碼音頻,視頻幀也可以選擇性解碼,比如只解碼關鍵幀。具體源碼:https://github.com/xufuji456/FFmpegAndroid
綜合上面的方案,使用MediaExtractor+MediaCodec+SurfaceView組合是個不錯的選擇。如果需要邊拖動進度條邊移動預覽圖,建議採用TextureView代替SurfaceView,因爲TextureView具有View的屬性,可以進行平移、縮放、旋轉等動畫。下圖是拖動預覽的效果:
1、解封裝
使用系統的MediaExtractor進行解封裝抽幀,由於視頻裏一般包含有視頻軌、音頻軌,可能還有字幕軌,所以我們需要遍歷所有軌道,選擇相應的視頻軌。具體過程如下:
mediaExtractor = new MediaExtractor();
MediaFormat mediaFormat = null;
String mimeType = "";
mediaExtractor.setDataSource(mFilePath);
for (int i=0; i<mediaExtractor.getTrackCount(); i++) {
mediaFormat = mediaExtractor.getTrackFormat(i);
mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
if (mimeType != null && mimeType.startsWith("video/")) {
mediaExtractor.selectTrack(i);
break;
}
}
if (mediaFormat == null || mimeType == null) {
return;
}
int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
long duration = mediaFormat.getLong(MediaFormat.KEY_DURATION);
2、設置預覽分辨率
根據視頻原有分辨率大小,按照分辨率等級,實現動態設置預覽分辨率的策略。需要注意的是,預覽分辨率需要等比例縮小,否則硬解可能出問題。如下參考代碼所示:
/**
* 根據原分辨率大小動態設置預覽分辨率
* @param mediaFormat mediaFormat
*/
private void setPreviewRatio(MediaFormat mediaFormat) {
if (mediaFormat == null) {
return;
}
int videoWidth = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
int videoHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
int previewRatio;
if (videoWidth >= RATIO_1080) {
previewRatio = 10;
} else if (videoWidth >= RATIO_480) {
previewRatio = 6;
} else if (videoWidth >= RATIO_240) {
previewRatio = 4;
} else {
previewRatio = 1;
}
int previewWidth = videoWidth / previewRatio;
int previewHeight = videoHeight / previewRatio;
mediaFormat.setInteger(MediaFormat.KEY_WIDTH, previewWidth);
mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, previewHeight);
}
3、配置MediaCodec
根據MediaExtractor解析出來的MediaFormat,包括width、height、duration、mimeType等元數據,來初始化MediaCodec,並且傳入Surface來綁定渲染界面:
//配置MediaCodec,並且start
mediaCodec = MediaCodec.createDecoderByType(mimeType);
mediaCodec.configure(mediaFormat, mSurface, null, 0);
mediaCodec.start();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
4、解碼與渲染
在子線程中,循環調用MediaExtractor抽幀,然後是MediaCodec解碼,解出來的數據直接渲染到Surface上。需要注意的是,解碼是異步的,存在獲取超時時間(根據實際情況設定),並且有返回結果,我們應該結果碼進行處理:
while (!isInterrupted()) {
if (!isPreviewing) {
SystemClock.sleep(SLEEP_TIME);
continue;
}
//從緩衝區取出一個緩衝塊,如果當前無可用緩衝塊,返回inputIndex<0
int inputIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIME);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputIndex];
int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
//入隊列
if (sampleSize < 0) {
mediaCodec.queueInputBuffer(inputIndex,0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
mediaCodec.queueInputBuffer(inputIndex, 0, sampleSize, mediaExtractor.getSampleTime(), 0);
mediaExtractor.advance();
}
}
//出隊列
int outputIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIME);
switch (outputIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.i(TAG, "output format changed...");
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.i(TAG, "try again later...");
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.i(TAG, "output buffer changed...");
break;
default:
//渲染到surface
mediaCodec.releaseOutputBuffer(outputIndex, true);
break;
}
}
5、預覽圖跟隨移動
因爲要跟隨進度條的移動而移動,這裏選擇TextureView。要實現預覽圖中心點跟隨進度條焦點移動,首先要計算出預覽圖寬度Width、右間距Margin,還有移動終點,否則會一直往右移出屏幕界面。在onLayout時,進行獲取與計算:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (moveEndPos == 0) {
int previewWidth = texturePreView.getWidth();
previewHalfWidth = previewWidth / 2;
int marginEnd = 0;
MarginLayoutParams layoutParams = (MarginLayoutParams) texturePreView.getLayoutParams();
if (layoutParams != null) {
marginEnd = layoutParams.getMarginEnd();
}
moveEndPos = screenWidth - previewWidth - marginEnd;
Log.i(TAG, "previewWidth=" + previewWidth);
}
}
設置進度條拖動監聽器,在進度條發生改變時,計算出當前移動偏移量並且轉化爲屏幕偏移值,從而更新預覽圖的偏移位置。具體計算如下:
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (!fromUser) {
return;
}
previewBar.setProgress(progress);
if (hardwareDecode != null && progress < duration) {
// us to ms
hardwareDecode.seekTo(progress * 1000);
}
int percent = progress * screenWidth / duration;
if (percent > previewHalfWidth && percent < moveEndPos && texturePreView != null) {
texturePreView.setTranslationX(percent - previewHalfWidth);
}
}
6、預覽圖的顯示與隱藏
我們不需要每時每刻顯示預覽圖,只需要在拖動過程中顯示。那麼我們可以在進度條事件監聽回調操作。在onStartTrackingTouch顯示,在onStopTrackingTouch隱藏:
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (texturePreView != null) {
texturePreView.setVisibility(VISIBLE);
}
if (hardwareDecode != null) {
hardwareDecode.setPreviewing(true);
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (texturePreView != null) {
texturePreView.setVisibility(GONE);
}
if (mPreviewBarCallback != null) {
mPreviewBarCallback.onStopTracking(seekBar.getProgress());
}
if (hardwareDecode != null) {
hardwareDecode.setPreviewing(false);
}
}
至此,完成了視頻拖動預覽的主要步驟,讓用戶享受邊播放邊預覽的樂趣。