一、流媒体

什么是流媒体技术?
简单的说,就是边下载,边播放。
也就是说,客户端在播放前,无需下载整个媒体文件,而是在播放缓存区已下载的媒体数据同时,持续不断的接收媒体流的剩余部分。

更专业一点的定义是:
流媒体技术的主要特点是以“流(Streaming)”的形式在基于IP协议的互联网中进行多媒体数据的实时、连续传播。

常用的流媒体协议有哪些?

 RTSP (RTP, SDP), RTMP
 HTTP progressive streaming
 HLS - HTTP live streaming (M3U8)

RTSP/RTP流媒体协议

什么是RTSP/RTP流媒体协议?

RTSP/RTP是目前最流行、使用最广泛的实时流媒体协议,它实际上由一组标准化协议构成:

 
1.jpg

其中,RTSP是Real Time Streaming Protocol(实时流媒体协议),RTP是Real Time Transport Protocol(实时传输协议)。
RTSP是一种双向实时数据传输协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作

 

HTTP渐进下载流媒体播放(HTTP progressive streaming)

什么是HTTP progressive streaming?

1.基于HTTP的渐进下载,是在下载完成后再播放的模式基础上做了一些小的改进。
2.客户端在开始播放前仅需等待一段较短的时间用于下载和缓冲该媒体文件最前面一部分的数据,之后便可以一边下载一边播放。开始播放前的缓冲通常需要几十秒甚至上百秒的时间。
3.只有满足特定封装条件的媒体文件格式才支持渐进下载播放,例如编码参数必须放在文件的起始部位,音视频文件完全按照时间顺序交织等。

HTTP Live Streaming协议

什么是HTTP Live Streaming?

最初是苹果公司针对其移动设备开发的流媒体协议。
让内容提供者通过普通Web服务器向客户端提供接近实时的音视频流媒体服务,包括直播和点播。
支持将同一节目编码为不同码率的多个替换流,客户端可以根据带宽变化在替换流之间智能切换。

二、解码

我们播放的视频文件都是经过压缩的,因为这样有利于节约存储空间。那么在播放过程,就需要进行一个反射的解压缩过程。

软解码和硬解码

解码分为硬解码和软解码两种

区别

举个栗子,CPU 相当于公司的 CEO ,GPU相当于公司技术总监、产品经理之类,来了一个需求,如果采用软解码,那就是让 CEO 去画原型,去一线写代码,这期间还要忙着各种大小的事物处理,如果采用硬解码,那就是CEO 朝技术总监、产品经理发指令,让他们去完成一件事,并且定期查询完成的程度。

硬解码:就是调用GPU的专门模块进行解码,由显卡核心GPU来对视频进行解码工作。
软解码:通过软件让CPU来对视频进行解码处理。

优缺点

网上看到一句话,“硬解码是将原来全部交由CPU来处理的视频数据的一部分交由GPU来做,而GPU的并行运算能力要远远高于CPU,这样可以大大的降低对CPU的负载,CPU的占用率较低了之后就可以同时运行一些其他的程序了。”

两者优缺点:

 
2.jpg

上面对比中一个是功耗一个是总功耗,这个也很容易理解,GPU的电路更复杂,并行运算能力要远远高于CPU,于是耗电量就更高,GPU功耗大,但运行速度提升更多,功耗 = 功率 * 时间,所以就算功率乘个4,但是时间除以个10,总耗能还是降低。

选择:说不上那个好,各有优点。我感觉硬解更好一些。过于占用CPU太耗性能。

三、播放器

目前播放器比较火热的有Android系统自带的MediaPalyer,还有google的ExoPlayer

3.1我们先说一下MediaPlayer

MediaPlayer处于Android多媒体包下”android.media.MediaPlayer”,仅有一个无参的构造函数,虽然Android平台仅为我们提供了一个无参的构造函数,但是为了方便我们初始化,还为我们提供了几个静态的`create()方法用于完成MediaPlayer初始化的工作。(常用的两个)

static MediaPlayer create(Context context,int resid):通过给定的Id来创建一个MediaPlayer实例。
static MediaPlayer create(Context context,Uri uri):通过给定的Uri来创建一个MediaPlayer实例。
还有一些重载的create方法。

MediaPlayer具体方法介绍:

void setDataSource(String path) 通过一个具体的路径来设置MediaPlayer的数据源,path可以是本地的一个路径,也可以是一个网络路径
void setDataSource(Context context, Uri uri) 通过给定的Uri来设置MediaPlayer的数据源,这里的Uri可以是网络路径或是一个ContentProvider的Uri。
void setDataSource(MediaDataSource dataSource) 通过提供的MediaDataSource来设置数据源
void setDataSource(FileDescriptor fd) 通过文件描述符FileDescriptor来设置数据源
int getCurrentPosition() 获取当前播放的位置
int getAudioSessionId() 返回音频的session ID
int getDuration() 得到文件的时间
TrackInfo[] getTrackInfo() 返回一个track信息的数组
boolean isLooping () 是否循环播放
boolean isPlaying() 是否正在播放
void pause () 暂停
void start () 开始
void stop () 停止
void prepare() 同步的方式装载流媒体文件。
void prepareAsync() 异步的方式装载流媒体文件。
void reset() 重置MediaPlayer至未初始化状态。
void release () 回收流媒体资源。
void seekTo(int msec) 指定播放的位置(以毫秒为单位的时间)
void setAudioStreamType(int streamtype) 指定流媒体类型
void setLooping(boolean looping) 设置是否单曲循环
void setNextMediaPlayer(MediaPlayer next) 当 当前这个MediaPlayer播放完毕后,MediaPlayer next开始播放
void setWakeMode(Context context, int mode):设置CPU唤醒的状态。
setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener) 网络流媒体的缓冲变化时回调
setOnCompletionListener(MediaPlayer.OnCompletionListener listener) 网络流媒体播放结束时回调
setOnErrorListener(MediaPlayer.OnErrorListener listener) 发生错误时回调
setOnPreparedListener(MediaPlayer.OnPreparedListener listener):当装载流媒体完毕的时候回调。

在使用MediaPlayer播放一段流媒体的时候,需要使用prepare()或prepareAsync()方法把流媒体装载进MediaPlayer,才可以调用start()方法播放流媒体。

在使用start()播放流媒体之前,需要装载流媒体资源。这里最好使用prepareAsync()用异步的方式装载流媒体资源。因为流媒体资源的装载是会消耗系统资源的,在一些硬件不理想的设备上,如果使用prepare()同步的方式装载资源,可能会造成UI界面的卡顿,这是非常影响用于体验的。因为推荐使用异步装载的方式,为了避免还没有装载完成就调用start()而报错的问题,需要绑定MediaPlayer.setOnPreparedListener()事件,它将在异步装载完成之后回调。异步装载还有一个好处就是避免装载超时引发ANR((Application Not Responding)错误。

            mediaPlayer = new MediaPlayer();
            mediaPlayer.setDataSource(path);
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            
            // 通过异步的方式装载媒体资源
            mediaPlayer.prepareAsync();
            mediaPlayer.setOnPreparedListener(new OnPreparedListener() {                    
                @Override
                public void onPrepared(MediaPlayer mp) {
                    // 装载完毕回调
                    mediaPlayer.start();
                }
            });

使用完MediaPlayer需要回收资源。MediaPlayer是很消耗系统资源的,所以在使用完MediaPlayer,不要等待系统自动回收,最好是主动回收资源。

          mediaPlayer.stop();
          mediaPlayer.release();
          mediaPlayer = null;
      }

3.2 ExoPalyer
简要说明:

ExoPlayer is an application level media player for Android. It provides an alternative to Android’s MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Android’s MediaPlayer API, including DASH and SmoothStreaming adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and can be updated through Play Store application updates.
来自google翻译:
ExoPlayer是Android的应用程序级媒体播放器。 它提供了Android的MediaPlayer API的替代品,用于在本地和互联网上播放音频和视频。 ExoPlayer支持Android MediaPlayer API目前不支持的功能,包括DASH和SmoothStreaming自适应回放。 与MediaPlayer API不同,ExoPlayer易于定制和扩展,并可通过Play Store应用程序更新进行更新。
综上所述,大概就是MediaPlayer不如ExoPlayer,google推荐使用ExoPlayer。

使用

3.2.1.添加依赖

implementation 'com.google.android.exoplayer:exoplayer:2.X.X'

整个ExoPlayer库包括5个子库,依赖了整个ExoPlayer库和依赖5个子库是等效的。
– exoplayer-core:核心功能 (必要)
– exoplayer-dash:支持DASH内容
– exoplayer-ui:用于ExoPlayer的UI组件和相关的资源。
– exoplayer-hls:支持HLS内容
– exoplayer-smoothstreaming:支持SmoothStreaming内容

3.2.2.创建一个SimpleExoPlayer实例,SimpleExoPlayer是ExoPlayer接口的一个默认的通用实现。


private void initializePlayer() {
        if (player==null){
            player = ExoPlayerFactory.newSimpleInstance(
                    new DefaultRenderersFactory(this),
                    new DefaultTrackSelector(), new DefaultLoadControl());

            playerView.setPlayer(player);

            player.setPlayWhenReady(playWhenReady);
            player.seekTo(currentWindow, playbackPosition);
        }

        Uri uri = Uri.parse(getString(R.string.media_url_mp4));
        MediaSource mediaSource = buildMediaSource(uri);
        player.prepare(mediaSource, false, true);
    }

3.2.3.最后我们要做资源释放

ExoPlayer相较于MediaPlayer有很多很多的优点:
支持动态的自适应流HTTP(DASH) 和 平滑流,任何目前MediaPlayer支持的视频格式(同时它还支持HTTP直播了(HLS),MP4,MP3,WebM,M4A,MPEG-TS 和 AAC).
支持高级的HLS特性
支持自定义和扩治你的使用场景。ExoPlayer专门为此设计;
便于随着App的升级而升级。因为ExoPlayer是一个包含在你的应用中的库,对于你使用哪个版本有完全的控制权,并且你可以简单的跟随应用的升级而升级;
更少的适配性问题。

缺点:
ExoPlayer的音频和视频组件依赖Android的 MediaCodec接口,该接口发布于Android4.1(API 等级16)。因此它不能工作于之前的Android版本。

3.3 MediaPlayer和TextureView

使用MediaPlayer 要配合SurfaceView或者TextureView来使用。来用他们渲染视图。
打个比方来说的话 MediaPlayer就像电脑的主机一样,而SurfaceView和TextureView就如同显示器一样。

TextureView优缺点:

  • 优点

动画支持良好,可以获取视频截图

视图不可见时可以保留当前帧不黑屏

  • 缺点

必须开启硬件加速,否则无画面,占用内存比SurfaceView高,在5.0以前在主线程渲染,5.0以后有单独的渲染线程。

SurfaceView优缺点:

  • 优点

可以在一个独立的线程中进行绘制,不会影响主线程使用双缓冲机制,播放视频时画面更流畅

  • 缺点

由于是独立的一层View,更像是独立的一个Window,不能加上动画、平移、缩放;

四、一些思考
之前我们接入过腾讯视频
直接用腾讯写好的Demo 叫SuperBasePlayer 使用过程中是非常痛苦,因为他的功能比较多。但是没有按照业务功能进行拆分。所有全部蹂在一起。这样如果做功能修改或者解码器更换。或者我们不接腾讯接别的sdk改动就会很大。所以就想因为我们现在做的项目都是组件化。所以视频这块是否可以组件化开发。把每个功能单独开发,然后自由拼接。将解码器与视图分离。

 
3.png

实现

4.1 接收者组管理(ReceiverGroup)

ReceiverGroup的目的就是对众多接收者进行统一的管理,事件下发。

在ReceiverGroup中包含Cover和Receiver,提供了Receiver的添加、移除、销毁等操作。

public interface IReceiverGroup {
    void setOnReceiverGroupChangeListener(OnReceiverGroupChangeListener onReceiverGroupChangeListener);

    void addReceiver(String key, IReceiver receiver);

    void removeReceiver(String key);

    void clearReceivers();
}


**4.2 BaseCover **
定义一个视图基类

//请求暂停
void requestPause(Bundle bundle);
//请求恢复播放
void requestResume(Bundle bundle);
//请求seek
void requestSeek(Bundle bundle);
//请求停止
void requestStop(Bundle bundle);
//请求重置
void requestReset(Bundle bundle);
//请求重试
void requestRetry(Bundle bundle);
//请求重播
void requestReplay(Bundle bundle);

------------------------------------------------------------

//必须实现的方法。用于初始化视图布局。
abstract View onCreateCoverView(Context context);

------------------------------------------------------------

//设置视图的显示或隐藏
void setCoverVisibility(int visibility); 

4.2.1 CoVer
写各种CoVer
LoadingCover
ErrorCover
ControllerCover
GestureCover
·················
根据自己的需要些一写Cover

通过ReceiverGroup 我们把Cover添加到视图上

ReceiverGroup receiverGroup = new ReceiverGroup(groupValue);
receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context));
receiverGroup.addReceiver(KEY_CONTROLLER_COVER, new ControllerCover(context));
receiverGroup.addReceiver(KEY_GESTURE_COVER, new GestureCover(context));
receiverGroup.addReceiver(KEY_ERROR_COVER, new ErrorCover(context));
videoView.setReceiverGroup(receiverGroup);

我们也可以移除不想要的Cover

4.3事件生产者(EventProducer)
顾名思义,就是它是产生事件的源。比如系统网络状态发生了变化,发出了通知,然后各个应用根据自己的情况来调整显示或设置等。又或者电池电量的变化和低电量预警通知事件等。

网络变化事件

public class NetworkEventProducer extends BaseEventProducer {
    //...
    private Handler mHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case MSG_CODE_NETWORK_CHANGE:
                    int state = (int) msg.obj;
                    //...将网络状态发送出去
                    getSender().sendInt(InterKey.KEY_NETWORK_STATE, state);
                    PLog.d(TAG,"onNetworkChange : " + state);
                    break;
            }
        }
    };
    //...
    public NetworkEventProducer(Context context){
        //...
    }
    //...
    public static class NetChangeBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //...
            //post state message
        }
        //...
    }
}

4.4 BaseVideoView
写一个基类 来实现我们写的功能。

  • 初始化BaseVideoView
    BaseVideoView可以写在在布局中

 <com.kk.taurus.playerbase.widget.BaseVideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_margin="2dp"
        android:background="#000000"
        />

我们也还可以通过java代码创建

BaseVideoView videoView = new BaseVideoView(context);

  • 为BaseVideoView设置一个ReceiverGroup

ReceiverGroup receiverGroup = new ReceiverGroup(groupValue);
receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context));
receiverGroup.addReceiver(KEY_CONTROLLER_COVER, new ControllerCover(context));
receiverGroup.addReceiver(KEY_ERROR_COVER, new ErrorCover(context));
videoView.setReceiverGroup(receiverGroup);
  • 设置事件监听器、事件处理器

videoView.setOnPlayerEventListener(new OnPlayerEventListener() {
    @Override
    public void onPlayerEvent(int eventCode, Bundle bundle) {
        //...
    }
});
videoView.setOnReceiverEventListener(new OnReceiverEventListener() {
    @Override
    public void onReceiverEvent(int eventCode, Bundle bundle) {
        //...
    }
});
videoView.setOnErrorEventListener(new OnErrorEventListener() {
    @Override
    public void onErrorEvent(int eventCode, Bundle bundle) {
        //...
    }
});
videoView.setEventHandler(new OnVideoViewEventHandler(){
    @Override
    public void onAssistHandle(BaseVideoView assist, int eventCode, Bundle bundle) {
        super.onAssistHandle(assist, eventCode, bundle);
        //...
    }
});
  • 设置数据启动播放

videoView.setDataSource(new DataSource("http://url..."));
videoView.start();
//如果需要定点播放,请调用下面的方法并传入时间点(毫秒值)
//如下,从15秒处起播
videoView.start(15000);
  • 也可以设置 倍速播放 需要看解码器是否支持

  • 暂停与恢复播放

//暂停
videoView.pause();
//恢复
videoView.resume();
  • 销毁播放器

videoView.stopPlayback();

 
 
 
 

链接:https://www.jianshu.com/p/2ea9572cf36c