Android播放器拖动进度条的小图预览
播放器拖动预览,让用户提前了解视频的波澜迭起情节,先走马观花看一遍精彩部分,满足一下好奇心,这就是拖动预览的意义所在。那么我们该如何打造高性能、高效率、高可靠的拖动预览呢?首先,小图预览强调足够小,因为预览画面分辨率没必要高清,分辨率越小解码速度越快、占用内存与CPU资源越低;其次,硬解优先,绑定Surface,解码后直接渲染到Surface上;另外,不必要解码音频,视频帧也可以选择性解码,比如只解码关键帧。
综合上面的方案,使用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);
}
}
至此,完成了视频拖动预览的主要步骤,让用户享受边播放边预览的乐趣。
推荐阅读