欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-01-11 22:12:04
...

播放器拖动预览,让用户提前了解视频的波澜迭起情节,先走马观花看一遍精彩部分,满足一下好奇心,这就是拖动预览的意义所在。那么我们该如何打造高性能、高效率、高可靠的拖动预览呢?首先,小图预览强调足够小,因为预览画面分辨率没必要高清,分辨率越小解码速度越快、占用内存与CPU资源越低;其次,硬解优先,绑定Surface,解码后直接渲染到Surface上;另外,不必要解码音频,视频帧也可以选择性解码,比如只解码关键帧。

综合上面的方案,使用MediaExtractor+MediaCodec+SurfaceView组合是个不错的选择。如果需要边拖动进度条边移动预览图,建议采用TextureView代替SurfaceView,因为TextureView具有View的属性,可以进行平移、缩放、旋转等动画。下图是拖动预览的效果:

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

 

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);
        }
    }

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