请以Conviva开发者社区作为主要参考源。
一 前言
Conviva在android播放器中的集成需要两个jar包,一个是core包(Conviva_SDK_Android),一个是每个播放器独有的包(在下文中我们称之为Proxy)。
Proxy在我们的开发者社区播放器库里已经有了很多的播放器类型,按照相关的步骤集成就可以,但是也有很多播放器我们播放器库里是没有的,Proxy就需要由我们进行定制开发,以提供给开发者使用。
但是,有很多的公司播放器的SDK可能涉及到保密,我们无法直接提供定制方案,就开放了自定义播放器Proxy的开发流程,以供开发者使用。
对于conviva集成自定义播放器而言,Proxy是开发工作的核心。
二 概念介绍
很多类封装在ConvivaSessionManager类中,开发者可以灵活处理。
• Session
Conviva对一段视频数据的检测是以Session为单位,从创建Session开始(mSessionKey = mClient.creaateSession)到销毁Session结束(mClient.cleanupSession(mSessionKey))。对这段视频评估的数据,都是从这个过程中获取的。
• PlayStateManager
在创建Session开始到销毁Session结束的过程中,需要传递视频的播放状态(mStateManager.setPlayerState(…)),上传播放的错误(mStateManager.sendError(…))等等。
Conviva自身分为以下几个必须的标准状态:SOPPED(停止),PLAYING(播放),BUFFERING(缓冲),PAUSED(暂停),UNKNOW(未知),开发者需要根据播放器的实际情况分别对应Conviva的这五个状态(后面会对比两种自定义播放器Proxy的开发方案,帮助开发者理解)。
• ContentMetadata
我们提供了ContentMetadata这个类传递一些视频信息(又叫元数据),比如视频url,视频类型,ViewId等等。
• Client
主要用于创建Session,关联Player和销毁Session,发送事件(包括广告事件、自定义事件)。
三 集成流程
• 导入SDK
到官网下载最新Conviva_SDK_Android,复制Conviva_SDK_Android到app的libs文件夹,Add As Liraty。
找到Demo的helper文件夹,复制ConvivaSessionManager文件到项目中合适位置(建议复制,可以根据需求适当修改,也可以自己写)。
• 替换key
确定mGateWayUrl,测试阶段的mGateWayUrl一般是https://testonly.conviva.com或ClientSettings.defaultDevelpomentGatewayUrl,测试没问题后替换成pulse的url;
• 初始化
app启动第一次调用即可(可以写在Application)
ConvivaSessionManager.initClient(this, mGateWayUrl);
• 传递视频基础信息(配置元数据)
创建Session,表示对一段视频检测的开始(位置在播放器开始请求视频数据之前即可,进行到这一步Touchstone是有数据的,尽管可能状态不准确),可以放在ConvivaSessionManager类中,也可以根据需求调整位置。如下:
//mStateManager需要在createSession和创建Proxy之前创建
mStateManager = ConvivaSessionManager.getPlayerManager();
//视频基本信息
mStateManager.setBitrateKbps(-1);
ContentMetadata convivaMetaData = new ContentMetadata();
convivaMetaData.defaultResource = "AKAMAI";
//app的name
convivaMetaData.applicationName = "ConvivaSdk Demo";
//视频别名
convivaMetaData.assetName = "mediaplayer test video";
convivaMetaData.streamUrl = "www.sdadas.mp4";
/*这里表示视频类型是点播,conviva有三个标准:VOD,LIVE,UNKNOEN,分别表示直播,点播和未知类型。*/
convivaMetaData.streamType = ContentMetadata.StreamType.VOD;
convivaMetaData.duration = 0;
convivaMetaData.encodedFrameRate = -1;
//自定义tag,根据用户实际需求,设置一些tag
Map tags = new HashMap<>();
Tags.put("key", "value");
//创建Session,后面的参数就是视频的url
ConvivaSessionManager.createConvivaSession(convivaMetaData);
通过convivaMetaData信息都会在Touchstone上显示,如下图:
• 实例化Proxy对象
监听Player的播放状态等信息,这是对视频进行监控的过程,Proxy对象要在mStateManager和mPlayer不为null的地方进行实例化(后面详细介绍这个Proxy的写法)。
//实例化Proxy,需要传递参数PlayerStateManager和播放器Player对象。
mPlayerInterface = new CVXXXPlayerInterface(mStateManager, mPlayer);
• 对广告,比特率,自定义错误,自定义事件的操作
A:在广告的监听中设置:
//广告开始
ConvivaSessionManager.adStart();
//广告开始自定义事件(可选,作为广告开始的补充,可传递一些信息)
ConvivaSessionManager.podEvent(ConvivaSessionManager.POS_EVENT_POD_START);
//广告结束
ConvivaSessionManager.adEnd();
//广告结束自定义事件(可选,作为广告结束的补充,可传递一些信息)
ConvivaSessionManager.podEvent(ConvivaSessionManager.POS_EVENT_POD_END);
B:在可监测比特率变化的监听中设置:
mPlayeStateManager.setBitrateKbps(…);
C:在系统检测不到的错误,需要自定义的时候调用:
mPlayeStateManager.sendError(…);
D:在分辨变化的监听中设置:
mPlayeStateManager.setVideoWidth(…);
mPlayeStateManager.setVideoHeight(…);
• 销毁Session
表示对一段视频的检测结束。一般是在back时,进入下个视频等结束该视频时调用。包括三部分:
//清除Proxy
mPlayerInterface.cleanup();
//释放mPlayeStateManager
ConvivaSessionManager.releasePlayerStateManager();
//清除Session
ConvivaSessionManager.cleanupConvivaSession();
•释放Client
退出app时销毁即可
ConvivaSessionManager.deinitClient();
四 测试
上述集成步骤进行完以后,conviva就可以对视频进行检测,经过后台大数据分析返回有用的数据信息,初步集成之后需要用TouchStone进行测试(测试流程详见《TouchStone的使用及测试流程》)。
五 Proxy文件写法
参考Demo的Proxy文件,对照稍加修改即可。
在android开发中,多次注册Linister会被覆盖掉,只有最后一次注册的Linister会生效。我们官方的Proxy文件都做了处理,可参考我们的Proxy方案,按部就班的进行,以免监听事件失效导致conviva检测失败。不同播放器的方案不尽相同,以下是两种播放器的方案,一般来讲,第一种方案是自定义播放器常用的,如果播放器的API不能满足需求,就采用第二种方案,开发者要灵活制定开发方案,如果都不满足需求,请登陆传视网站咨询我们。
ExoPlayer2 + Proxy:
Exoplayer的Proxy方案是依赖于各种Linister监听事件维护视频播放状态和获取相关信息,因为视频的四个状态Buffering,Playing,Paused,Stopped以及其他信息都可以获取到。
• 在onPlayerStateChanges()回调方法中,可以更新四种必须的视频播放状态和视频的Duration
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (_inListener){
return;
}
stateChanged(playWhenReady,playbackState);
if (eventListener != null){
_inListener = true;
eventListener.onPlayerStateChanged(playWhenReady, playbackState);
_inListener = false;
}
}
public void stateChanged(boolean playWhenReady, int playbackState) {
try {
switch (playbackState) {
case ExoPlayer.STATE_BUFFERING:
mStateManager.setPlayerState(PlayerState.BUFFERING);
break;
case ExoPlayer.STATE_ENDED:
mStateManager.setPlayerState(PlayerState.STOPPED);
break;
case ExoPlayer.STATE_IDLE:
mStateManager.setPlayerState(PlayerState.STOPPED);
break;
case ExoPlayer.STATE_READY:
if (playWhenReady) {
mStateManager.setPlayerState(PlayerState.PLAYING);
if (!isContentSet) {
//content length is available only after preparing state
//mStateManager.setDuration(((int) mPlayer.getDuration() / 1000));
ContentMetadata metadata = new ContentMetadata();
metadata.duration = (int) mPlayer.getDuration() / 1000;
mStateManager.updateContentMetadata(metadata);
isContentSet = true;
}
} else {
mStateManager.setPlayerState(PlayerState.PAUSED);
}
break;
default:
break;
}
} catch (Exception e) {
Log("Player state exception", SystemSettings.LogLevel.DEBUG);
}
}
• 在onViewSizeChanged()回调方法中调用updateResolution (width, height),更新分辨率
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (_viListener) return;
updateResolution(width,height,unappliedRotationDegrees,pixelWidthHeightRatio);
if (videoListener != null) {
_viListener = true;
videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,pixelWidthHeightRatio);
_viListener = false;
}
}
• 在onPlayerError()回调方法中调用mStateManager.sendError(errorCode, Client.ErrorSeverity.FATAL)提交错误,ctrl点开ExoPlaybackException.TYPE_SOURCE可以看到源码中所有errorCode
@Override
public void onPlayerError(ExoPlaybackException error) {
if (_inListener) return;
try {
updateError(error);
if (eventListener != null){
_inListener = true;
eventListener.onPlayerError(error);
_inListener = false;
}
} catch (ConvivaException e) {
e.printStackTrace();
}
}
public void updateError(ExoPlaybackException errorMsg) throws ConvivaException {
String errorCode = null;
if (errorMsg.type == ExoPlaybackException.TYPE_SOURCE) {
errorCode = TYPE_SOURCE+":"+errorMsg.getSourceException();
}
else if (errorMsg.type == ExoPlaybackException.TYPE_RENDERER) {
errorCode = TYPE_RENDERER+":"+errorMsg.getRendererException();
}
else if (errorMsg.type == ExoPlaybackException.TYPE_UNEXPECTED) {
errorCode = TYPE_RUNTIME+":"+errorMsg.getUnexpectedException();
}
else {
errorCode = UNKONW_EXPECTION+":"+errorMsg.getMessage();
}
if (mStateManager!= null) {
mStateManager.setPlayerState(PlayerState.STOPPED);
mStateManager.sendError(errorCode, Client.ErrorSeverity.FATAL);
}
}
• 在Seek结束的回调方法onPositionDiscontinuity()中调用mStateManager.setPlayerSeekEnd(),告知后台
@Override
public void onPositionDiscontinuity() {
try {
mStateManager.setPlayerSeekEnd();
} catch (ConvivaException e) {
e.printStackTrace();
}
}
MediaPlayer Proxy:
MediaPlayer的Proxy方案是依赖于各种Linister监听事件和播放器Position结合定时任务维护视频播放状态和获取相关信息。
A 依赖于监听事件
• 在onPrepare()回调方法中更新视频播放状态为Buffering,更新播放器的Height,Width, 提交了视频的Duration
@Override
public void onPrepared(MediaPlayer mp) {
Log("OnPrepared", SystemSettings.LogLevel.DEBUG);
if (_inListener) return;
//执行该回调方法时,认为player处于Buffering状态
updateState(PlayerState.BUFFERING);
//传递此时player的宽高或者说是分辨率
updateResolution(_mPlayer.getVideoHeight(), _mPlayer.getVideoWidth());
_mIsPlayerActive = true;
if(mp != null) {
int duration = mp.getDuration();
if (mStateManager != null && duration > 0) {
try {
//mStateManager.setDuration(duration / 1000);废弃的api,下面是最新的
//获取视频时长传递给conviva
ContentMetadata metadata = new ContentMetadata();
metadata.duration = duration / 1000;
Log.e("time",metadata.duration+"");
mStateManager.updateContentMetadata(metadata);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
• 在OnCompletion()的回调中,更新播放状态为Stopped。
@Override
public void onCompletion(MediaPlayer mp) {
Log("onCompletion", SystemSettings.LogLevel.DEBUG);
if (_inListener) return;
_mIsPlayerActive = false;
updateState(PlayerState.STOPPED);
//可以观察到,每个回调都会有这种写法,目的是不影响外面对监听事件的使用。如果不理解,对照写就行。
if (_onCompListenerOrig != null) {
_inListener = true;
try {
_onCompListenerOrig.onCompletion(mp);
} finally {
_inListener = false;
}
}
}
• 在onVideoSizeChanged()回调方法中调用updateResolution (width, height),更新分辨率;
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
updateResolution(width, height);
}
//更新分辨率
public void updateResolution(int width, int height) {
if(mStateManager != null) {
try {
mStateManager.setVideoWidth(width);
mStateManager.setVideoHeight(height);
} catch (ConvivaException e) {
e.printStackTrace();
}
}
}
• 在onError()回调方法中调用mStateManager.sendError(errorCode, Client.ErrorSeverity.FATAL)提交错误,ctrl点开Mediaplayer.MEDIA_ERROR_UNKNOWN可以看到源码中所有errorCode。
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
//Log("OnError : Error occurred", SystemSettings.LogLevel.DEBUG);
if (_inListener) return true;
if(mStateManager != null) {
Log("Proxy: onError (" + what + ", " + extra + ")", SystemSettings.LogLevel.DEBUG);
String errorCode = null;
if (what == MediaPlayer.MEDIA_ERROR_UNKNOWN) {
errorCode = ERR_UNKNOWN;
}
else if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) {
errorCode = ERR_SERVERDIED;
}
else if (what == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
errorCode = ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK;
}
else {
errorCode = ERR_UNKNOWN;
}
try {
mStateManager.sendError(errorCode, Client.ErrorSeverity.FATAL);
} catch (Exception e) {
e.printStackTrace();
}
}
//clean up session if the error causes video start failure
//可以观察到,每个回掉都会有这种写法,如果不理解,对照写就行,目的是不影响外面对监听事件的使用。
if (_onErrorListenerOrig != null) {
_inListener = true;
try {
return _onErrorListenerOrig.onError(mp, what, extra);
} finally {
_inListener = false;
}
}
return true;
}
• 在Seek结束的回调方法onSeekComplete()中调用mStateManager.setPlayerSeekEnd(),告知后台。
@Override
public void onSeekComplete(MediaPlayer mp) {
if(mStateManager != null) {
try {
mStateManager.setPlayerSeekEnd();
} catch (ConvivaException e) {
Log("Exception occurred during Seek End", SystemSettings.LogLevel.ERROR);
}
}
if(_onSeekListenerOrig != null) {
_onSeekListenerOrig.onSeekComplete(mp);
}
}
B 依赖于定时任务
• 除了onPrepare()和onCompeletion(),已经没有监听方法可以提供给我们更新Playing和Paused的状态了,Buffering在这里就调用一次,后面的Buffering状态也没办法拿到,于是就有了定时器去判断这些状态(依赖于判断position变化确定播放状态)。通过对上一次position和本次position的对比,判断Buffering,Playing,Paused状态并提交给conviva。
//定时器200ms执行一次任务
ITimerInterface iTimerInterface = new AndroidTimerInterface();
_mCancelTimer = iTimerInterface.createTimer(_pollStreamerTask, 200, "CVMediaPlayerInterface");
//根据position,判断状态的定时任务
private Runnable _pollStreamerTask = new Runnable() {
@Override
public void run() {
GetPlayheadTimeMs();
}
};
//定时任务执行的方法
public int GetPlayheadTimeMs() {
int currPos = -1;
try {
//Check if player is not null and player is prepared before pht values are queried.
if(_mPlayer != null && _mIsPlayerActive) {
currPos = _mPlayer.getCurrentPosition();
//isPlaying 在Playing或Buffering状态为true
if(_mPlayer.isPlaying()) {
//i如果当前position和前一次position相同
if(currPos == _previousPosition) {
updateState(PlayerState.BUFFERING);
} else {
updateState(PlayerState.PLAYING);
}
_previousPosition = currPos;
} else {
//If isPlaying is false, player is paused.
updateState(PlayerState.PAUSED);
}
}
} catch (IllegalStateException e) {
e.printStackTrace();
}
return currPos;
}
六 常见错误
Touchstone报以下错误:
原因:在attachPlayer()时传入的PlayerStateManager对象为null。可能是在CreateSession之后才实例化PlayStateManager对象。
解决办法:需要在CreateSession之前实例化PlayStateManager对象。