Android播放器拖动进度条的小图预览

播放器拖动预览,让用户提前了解视频的波澜迭起情节,先走马观花看一遍精彩部分,满足一下好奇心,这就是拖动预览的意义所在。那么我们该如何打造高性能、高效率、高可靠的拖动预览呢?首先,小图预览强调足够小,因为预览画面分辨率没必要高清,分辨率越小解码速度越快、占用内存与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);
        }
    }

至此,完成了视频拖动预览的主要步骤,让用户享受边播放边预览的乐趣。

发布了63 篇原创文章 · 获赞 179 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/u011686167/article/details/103134474