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

至此,完成了視頻拖動預覽的主要步驟,讓用戶享受邊播放邊預覽的樂趣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章