IjkPlayer簡要學習及應用

引言


之前學習和使用過EXOPlayer,並結合Shared Elements效果在公司的項目中有應用。文章寫的很爛直接看github代碼吧!

相比EXOPlayer,B站的IjkPlayer逼格很高,是基於ffmpeg開源的輕量級視頻播放器支持Android&iOS。源碼在GitHub,down下來後需要編譯才能運行,具體操作官方都有說明且網上資料很多。

我所編譯的版本是0.8.2,本文會對其大體流程梳理一遍並封裝一個實用性較高的控件

正文


官方的demo跑起來第一個界面形同文件管理
主頁面
找到本機的視頻文件就可以播放了
文件路徑
播放界面
也可以通過ActionBar中的Sample選擇網絡資源。通過後綴.m3u8可以看出是HLS的資源
播放列表
還有ActionBar中的Setting,這裏是一些播放時所用到的參數後文會有詳解。
參數設置

播放操作涉及到的界面是VideoActivity,這裏有官方封裝的播放控件IjkVideoView,在學習了官方設計後,我結合自身的實際需求自己封裝了一個控件在後文會提到,這裏先來學習一下官方的設計。

IjkVideoView


使用時:初始化控件–>設置資源路徑–>start。
控件內部的主要邏輯順序有以下:

初始化:
initRenders() 根據設置初始化渲染器類型(渲染器即SurfaceView、TextureView)
setRender(int render) 根據渲染器類型初始化渲染器
setRenderView(IRenderView renderView) 將渲染器添加到視圖
開始播放:
setVideoURI() 設置資源路徑
openVideo() 初始化播放
-->createPlayer() 創建播放器
-->bindSurfaceHolder() 播放器與渲染器綁定
    private void initRenders() {
        mAllRenders.clear();//渲染器列表
        //根據設置界面所選的渲染器,將其加入列表。
        //這麼做其實是爲了在demo播放的時候手動切換渲染器,用以觀察畢竟是demo
        //切換到時所用到的方法   toggleRender()
        if (mSettings.getEnableSurfaceView())
            mAllRenders.add(RENDER_SURFACE_VIEW);
        if (mSettings.getEnableTextureView() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
            mAllRenders.add(RENDER_TEXTURE_VIEW);
        if (mSettings.getEnableNoView())
            mAllRenders.add(RENDER_NONE);

        if (mAllRenders.isEmpty())
            mAllRenders.add(RENDER_SURFACE_VIEW);
        mCurrentRender = mAllRenders.get(mCurrentRenderIndex);
        setRender(mCurrentRender);
    }
//根據類型初始化渲染器
//這裏將SurfaceView、TextureView進行了封裝,用到了模板設計模式,目的是將同一目的不同的操作交由具體的子類
    public void setRender(int render) {
        switch (render) {
            case RENDER_NONE:
                setRenderView(null);
                break;
            case RENDER_TEXTURE_VIEW: {
                TextureRenderView renderView = new TextureRenderView(getContext());
                if (mMediaPlayer != null) {
                    renderView.getSurfaceHolder().bindToMediaPlayer(mMediaPlayer);
                    renderView.setVideoSize(mMediaPlayer.getVideoWidth(), mMediaPlayer.getVideoHeight());
                    renderView.setVideoSampleAspectRatio(mMediaPlayer.getVideoSarNum(), mMediaPlayer.getVideoSarDen());
                    renderView.setAspectRatio(mCurrentAspectRatio);
                }
                setRenderView(renderView);
                break;
            }
            case RENDER_SURFACE_VIEW: {
                SurfaceRenderView renderView = new SurfaceRenderView(getContext());
                setRenderView(renderView);
                break;
            }
            default:
                Log.e(TAG, String.format(Locale.getDefault(), "invalid render %d\n", render));
                break;
        }
    }
    public void setRenderView(IRenderView renderView) {
        ...
        //切換渲染器時清楚之前的渲染器
        ...

        if (renderView == null)
            return;

        mRenderView = renderView;

        ...
        //簡單起見,將視圖的顯示比例代碼忽略
        ...

        View renderUIView = mRenderView.getView();
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.WRAP_CONTENT,
                FrameLayout.LayoutParams.WRAP_CONTENT,
                Gravity.CENTER);
        renderUIView.setLayoutParams(lp);
        addView(renderUIView);

        mRenderView.addRenderCallback(mSHCallback);//SurfaceView的回調,不詳細展開
        mRenderView.setVideoRotation(mVideoRotationDegree);//旋轉角度,橫豎屏
    }

到此控件已經初始化完畢,在視圖上就可以看到自定義控件。但此時並沒有初始化播放器,視圖顯示的只是一個SurfaceView或TextureView。這時就需要給控件設置播放的資源地址了。

    //原類中重載了幾次
    private void setVideoURI(Uri uri, Map<String, String> headers) {
        mUri = uri;
        mHeaders = headers;
        mSeekWhenPrepared = 0;
        openVideo();
        requestLayout();
        invalidate();
    }

緊接着就調用了openVideo()方法,在此之前先剖析下createPlayer(),此方法在openVideo()中調用

    public IMediaPlayer createPlayer(int playerType) {
        IMediaPlayer mediaPlayer = null;
        //根據設置界面所選的播放器進行創建,有EXOPlayer和原生的MediaPlayer,這裏不是重點直接跳到IjkPlayer
        switch (playerType) {

            ...省略其他播放器...

            case Settings.PV_PLAYER__IjkMediaPlayer:
            default: {
                IjkMediaPlayer ijkMediaPlayer = null;
                if (mUri != null) {
                    ijkMediaPlayer = new IjkMediaPlayer();
                    ijkMediaPlayer.native_setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG);

                    ...
                    此處省略了一堆設置裏面設置的播放參數
                    ...

                }
                mediaPlayer = ijkMediaPlayer;
            }
            break;
        }
        //這裏是一個關於TextureView的代理寫法
        if (mSettings.getEnableDetachedSurfaceTextureView()) {
            mediaPlayer = new TextureMediaPlayer(mediaPlayer);
        }

        return mediaPlayer;
    }
    private void openVideo() {
        if (mUri == null || mSurfaceHolder == null) {
            // not ready for playback just yet, will try again later
            return;
        }
        // we shouldn't clear the target state, because somebody might have
        // called start() previously
        //demo切換播放時用
        release(false);

        AudioManager am = (AudioManager) mAppContext.getSystemService(Context.AUDIO_SERVICE);
        am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);

        try {
            mMediaPlayer = createPlayer(mSettings.getPlayer());

            // TODO: create SubtitleController in MediaPlayer, but we need
            // a context for the subtitle renderers
            final Context context = getContext();
            // REMOVED: SubtitleController

            // REMOVED: mAudioSession
            //一堆監聽
            mMediaPlayer.setOnPreparedListener(mPreparedListener);
            mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
            mMediaPlayer.setOnCompletionListener(mCompletionListener);
            mMediaPlayer.setOnErrorListener(mErrorListener);
            mMediaPlayer.setOnInfoListener(mInfoListener);
            mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
            mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener);
            mMediaPlayer.setOnTimedTextListener(mOnTimedTextListener);
            mCurrentBufferPercentage = 0;
            //設置資源URI
            String scheme = mUri.getScheme();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
                    mSettings.getUsingMediaDataSource() &&
                    (TextUtils.isEmpty(scheme) || scheme.equalsIgnoreCase("file"))) {
                IMediaDataSource dataSource = new FileMediaDataSource(new File(mUri.toString()));
                mMediaPlayer.setDataSource(dataSource);
            }  else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                mMediaPlayer.setDataSource(mAppContext, mUri, mHeaders);
            } else {
                mMediaPlayer.setDataSource(mUri.toString());
            }
            //將渲染器與播放器綁定
            bindSurfaceHolder(mMediaPlayer, mSurfaceHolder);
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mMediaPlayer.setScreenOnWhilePlaying(true);
            mPrepareStartTime = System.currentTimeMillis();
            mMediaPlayer.prepareAsync();//這裏已經開始異步緩衝了,會回調到OnPreparedListener,根據具體狀態開始播放
            if (mHudViewHolder != null)
                mHudViewHolder.setMediaPlayer(mMediaPlayer);

            // REMOVED: mPendingSubtitleTracks

            // we don't set the target state here either, but preserve the
            // target state that was there before.
            mCurrentState = STATE_PREPARING;
            attachMediaController();
        } catch (IOException ex) {
            Log.w(TAG, "Unable to open content: " + mUri, ex);
            mCurrentState = STATE_ERROR;
            mTargetState = STATE_ERROR;
            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
        } catch (IllegalArgumentException ex) {
            Log.w(TAG, "Unable to open content: " + mUri, ex);
            mCurrentState = STATE_ERROR;
            mTargetState = STATE_ERROR;
            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
        } finally {
            // REMOVED: mPendingSubtitleTracks.clear();
        }
    }
    //具體的操作已經轉到了相應的子類TextureRenderView、SurfaceRenderView
    private void bindSurfaceHolder(IMediaPlayer mp, IRenderView.ISurfaceHolder holder) {
        if (mp == null)
            return;

        if (holder == null) {
            mp.setDisplay(null);
            return;
        }

        holder.bindToMediaPlayer(mp);
    }

到此官方的IjkVideoView就已經初始化完成並開始播放資源,其餘public方法是爲了操作控件或增加播放控制控件所使用的。根據其主體思路我自己封裝了VideoViewIjk。

VideoViewIjk


我命名時習慣功能放前別名放後,大體的思路如下:
初始化播放器–>初始化播放視圖SurfaceView–>實現必要的監聽–>公開操作方法

    private void initPlayer() {
        IjkMediaPlayer ijkMediaPlayer = new IjkMediaPlayer();

        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", mediacodec);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", mediacodec_auto_rotate);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", mediacodec_handle_resolution_change);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", opensles);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", overlay_format);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", framedrop);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", start_on_prepared);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "http-detect-range-support", http_detect_range_support);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "skip_loop_filter", skip_loop_filter);

        mediaPlayer = ijkMediaPlayer;
        mediaPlayer.setOnPreparedListener(this);
        mediaPlayer.setOnVideoSizeChangedListener(this);
        mediaPlayer.setOnCompletionListener(this);
        mediaPlayer.setOnErrorListener(this);
        mediaPlayer.setOnInfoListener(this);
        mediaPlayer.setOnBufferingUpdateListener(this);
        mediaPlayer.setOnSeekCompleteListener(this);
    }
    private void initView() {
        surfaceView = new SurfaceView(getContext());
        surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                mediaPlayer.setDisplay(surfaceView.getHolder());
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });
        LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        addView(surfaceView, 0, layoutParams);
    }

這裏其實沒什麼好貼的,只是將官方demo中不需要的方法移除了,具體的可以看我的GitHub

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