【Android】 从头搭建视频播放器(1)——概述

转载请注明出处http://www.jianshu.com/p/c12481d3ceae

项目代码地址:https://github.com/Android-Jungle/android-jungle-mediaplayer
 

        近来有做播放器方面的需求,在搭建过程中,逐渐对 Android 上面视频播放器的实现有了一些初步的了解,在此总结一下,在 Android 上面,如何从头考虑设计并最终实现一个功能完备的视频播放器。

1、功能 & 思路

        我们通常看到一个通用的播放器如下:

半屏播放器

        在点击全屏按钮或者旋转屏幕后,可以展开到全屏:

全屏播放器

        我们可以看出,一个通用的播放器有如下一些功能点:

  • 播放/暂停
  • 全屏切换
  • SeekBar 进度调节
  • 手势调节屏幕亮度/音量/播放进度
  • 屏幕旋转支持

        其中,播放器的基础功能我们可以使用系统的 MediaPlayer 或者第三方的一些 Player 实现。全屏切换可以通过更改 ScreenOrientation屏幕布局来完成。其他的手势调节可以通过 GestureDetector 来完成。屏幕旋转通过 OrientationEventListener 来实现。

2、基础设计

        有了一个大致的功能描述以及实现思路之后,我们需要设计底层的基本交互及类。经过功能提炼及交互划分,首先的模块设计如下:

基础结构设计
  • BaseMediaPlayer:定义了一个播放器应该具备的基础接口;你可以跟进这个接口再加播放器底层,来实现不同的播放器;只提供播放的接口,不提供用户 UI 交互;
  • SystemMediaPlayerImpl:继承自 BaseMediaPlayer,是基于 Android 系统 MediaPlayer 的一份实现;
  • StrawMediaPlayer:继承自 FrameLayout,是 Android 上的一个布局 View,封装了播放器的所有操作,用来进行可视化和用户 UI 交互;
  • PlayerBottomControl:播放器底部控件,用于控制播放、进度、全屏调节等;
  • MediaPlayerGestureController:播放器手势控制器,用于手势识别和相应的控制;
  • ScreenOrientationSwitcher:屏幕方向切换控制器。

        基础的模块就这么一些,其中应该还有一些用于展示屏幕亮度、声音、进度的小的 View,自己可以轻松实现,在此没有列出来。

        StrawMediaPlayer 就是我们最终提供出去的播放器控件,上层可以直接使用这个控件。

3、播放器布局

        整个 StrawMediaPlayer 的布局如下:

界面布局

        通过结合功能点进行初步设计,整个播放器层级如下:

  • FrameLayout:用于容纳所有的 View;
  • SurfaceView:用于展示视频内容;
  • LoadingLayout:在视频加载的时候,用于展示 Loading 画面;
  • TopBarControl:顶部的 Bar,用于展示视频信息、返回按钮等等;
  • PlayerBottomControl:底部的 Bar,用于展示视频控制按钮、播放进度、全屏切换等 View。

4、BaseMediaPlayer

        结合模块设计及布局划分,我们先定义 BaseMediaPlayer 应该具备的接口。必须具备的接口如下:

接口 功能
BaseMediaPlayer(context, surfaceView) 构造函数
play(videoInfo) 播放
addPlayerListener(listener) 添加播放回调 Listener
pause() 暂停
resume() 恢复播放
stop() 停止播放
seekTo(millSeconds) 跳转到某个进度播放
setVolume(volume) 调节音量
getDuration() 获取整个视频的时间,返回 ms
getCurrentPosition() 获取视频当前播放的进度,返回 ms
doDestroy() 销毁播放器,释放系统资源
isPlaying() 是否正在播放
isLoading() 是否正在加载视频
getBufferPercent() 获取视频缓冲百分比

5、BaseMediaPlayerListener

        外部有可能需要监听一系列 Player 的事件,这时候,我们需要通过 addPlayerListener 添加一个 Listener,用于捕获事件。该接口定义如下:

BaseMediaPlayerListener

6、BaseMediaPlayer 代码片段

/**
 * BaseMediaPlayer.java
 *
 * @author arnozhang
 * @email  zyfgood12@163.com
 * @date   2015.9.25
 */
public abstract class BaseMediaPlayer {

    protected static final String TAG = "MediaPlayer";


    protected static interface NotifyListenerRunnable {
        void run(BaseMediaPlayerListener listener);
    }


    protected Context mContext;
    protected SurfaceView mSurfaceView;
    protected SurfaceHolder.Callback mSurfaceCallback;
    protected List<BaseMediaPlayerListener> mPlayerListeners = new ArrayList<>();
    protected VideoInfo mVideoInfo;
    protected int mVideoWidth;
    protected int mVideoHeight;
    protected boolean mIsLoading;
    protected boolean mMediaPlayerIsPrepared;
    protected boolean mVideoSizeInitialized;
    protected int mBufferPercent;
    protected int mVideoContainerZoneWidth;
    protected boolean mAutoPlayWhenHolderCreated;


    public BaseMediaPlayer(Context context, SurfaceView surfaceView) {
        mContext = context;
        mSurfaceView = surfaceView;

        initSurfaceCallback();
        SurfaceHolder videoHolder = mSurfaceView.getHolder();
        videoHolder.addCallback(mSurfaceCallback);
    }

    public void play(VideoInfo videoInfo) {
        if (!PlayerUtils.isNetworkAvailable()) {
            StrawToast.makeText(mContext, R.string.network_connection_failed).show();
            return;
        }

        mIsLoading = true;
        mVideoSizeInitialized = false;
        mMediaPlayerIsPrepared = false;
        mVideoInfo = videoInfo;

        Handler handler = ThreadManager.getInstance().getUIHandler();
        handler.removeCallbacks(mLoadingFailedRunnable);
        handler.postDelayed(mLoadingFailedRunnable, 30 * 1000);
    }

    public void addPlayerListener(BaseMediaPlayerListener listener) {
        mPlayerListeners.add(listener);
    }

    public abstract void pause();

    public abstract void resume();

    public abstract void stop();

    public abstract void seekTo(int millSeconds);

    public abstract void setVolume(float volume);

    public abstract int getDuration();

    public abstract int getCurrentPosition();

    public abstract void doDestroy();

    public abstract boolean isPlaying();

    protected abstract void playWithDisplayHolder(SurfaceHolder holder);

    public boolean isLoading() {
        return mIsLoading;
    }

    public boolean isLoadingOrPlaying() {
        return isLoading() || isPlaying();
    }

    public int getBufferPercent() {
        return mBufferPercent;
    }

    public Context getContext() {
        return mContext;
    }

    private void initSurfaceCallback() {
        mSurfaceCallback = new SurfaceHolder.Callback() {
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            }

            public void surfaceCreated(SurfaceHolder holder) {
                if (mAutoPlayWhenHolderCreated) {
                    mAutoPlayWhenHolderCreated = false;
                    playWithDisplayHolder(holder);
                }
            }

            public void surfaceDestroyed(SurfaceHolder holder) {
                if (isPlaying()) {
                    stop();
                }
            }
        };
    }

    protected void updateSurfaceSize() {
        updateSurfaceSize(mVideoContainerZoneWidth);
    }

    public void updateSurfaceSize(int containerWidth) {
        if (mVideoContainerZoneWidth == containerWidth) {
            return;
        }

        mVideoContainerZoneWidth = containerWidth;
        if (mVideoWidth == 0 || mVideoHeight == 0) {
            return;
        }

        final float ratio = (float) mVideoWidth / (float) mVideoHeight;
        int width = 0;
        int height = (int) (containerWidth / ratio);
        if (height > mVideoHeight) {
            width = containerWidth;
        } else {
            height = mVideoHeight;
            width = (int) (mVideoWidth * ratio);
        }

        ViewGroup.LayoutParams params = mSurfaceView.getLayoutParams();
        params.width = width;
        params.height = height;
        mSurfaceView.setLayoutParams(params);
    }

    protected Runnable mLoadingFailedRunnable = new Runnable() {
        @Override
        public void run() {
            notifyLoadFailed();
        }
    };

    protected void notifyListener(NotifyListenerRunnable runnable) {
        for (BaseMediaPlayerListener listener : mPlayerListeners) {
            runnable.run(listener);
        }
    }

    protected void notifyLoading() {
        LogUtils.e(TAG, "MediaPlayer Loading...");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onLoading();
            }
        });
    }

    protected void notifyFinishLoading() {
        LogUtils.e(TAG, "MediaPlayer Finish Loading!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onFinishLoading();
            }
        });
    }

    protected void notifyLoadFailed() {
        LogUtils.e(TAG, "MediaPlayer Load **Failed**!!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onLoadFailed();
            }
        });
    }

    protected void notifyError(final int what, final String message) {
        LogUtils.e(TAG, "MediaPlayer Error. what = %d, message = %s.", what, message);

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onError(what, message);
            }
        });
    }

    protected void notifyStartPlay() {
        LogUtils.e(TAG, "MediaPlayer Will Play!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onStartPlay();
            }
        });
    }

    protected void notifyPlayComplete() {
        LogUtils.e(TAG, "MediaPlayer Play Current Complete!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onPlayComplete();
            }
        });
    }

    protected void notifyPaused() {
        LogUtils.e(TAG, "MediaPlayer Paused.");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onPaused();
            }
        });
    }

    protected void notifyResumed() {
        LogUtils.e(TAG, "MediaPlayer Resumed.");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onResumed();
            }
        });
    }

    protected void notifyStopped() {
        LogUtils.e(TAG, "MediaPlayer Stopped!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onStopped();
            }
        });
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容