請以Conviva開發(fā)者社區(qū)作為主要參考源。
一 前言
Conviva在android播放器中的集成需要兩個jar包青柄,一個是core包(Conviva_SDK_Android)阵漏,一個是每個播放器獨(dú)有的包(在下文中我們稱之為Proxy)。
Proxy在我們的開發(fā)者社區(qū)播放器庫里已經(jīng)有了很多的播放器類型绽左,按照相關(guān)的步驟集成就可以悼嫉,但是也有很多播放器我們播放器庫里是沒有的,Proxy就需要由我們進(jìn)行定制開發(fā)拼窥,以提供給開發(fā)者使用戏蔑。
但是,有很多的公司播放器的SDK可能涉及到保密鲁纠,我們無法直接提供定制方案总棵,就開放了自定義播放器Proxy的開發(fā)流程,以供開發(fā)者使用改含。
對于conviva集成自定義播放器而言情龄,Proxy是開發(fā)工作的核心。
二 概念介紹
很多類封裝在ConvivaSessionManager類中,開發(fā)者可以靈活處理骤视。
??Session
Conviva對一段視頻數(shù)據(jù)的檢測是以Session為單位鞍爱,從創(chuàng)建Session開始(mSessionKey = mClient.creaateSession)到銷毀Session結(jié)束(mClient.cleanupSession(mSessionKey))。對這段視頻評估的數(shù)據(jù)专酗,都是從這個過程中獲取的睹逃。
??PlayStateManager
在創(chuàng)建Session開始到銷毀Session結(jié)束的過程中,需要傳遞視頻的播放狀態(tài)(mStateManager.setPlayerState(…))祷肯,上傳播放的錯誤(mStateManager.sendError(…))等等沉填。
Conviva自身分為以下幾個必須的標(biāo)準(zhǔn)狀態(tài):SOPPED(停止),PLAYING(播放)佑笋,BUFFERING(緩沖)翼闹,PAUSED(暫停),UNKNOW(未知)允青,開發(fā)者需要根據(jù)播放器的實(shí)際情況分別對應(yīng)Conviva的這五個狀態(tài)(后面會對比兩種自定義播放器Proxy的開發(fā)方案橄碾,幫助開發(fā)者理解)。
??ContentMetadata
我們提供了ContentMetadata這個類傳遞一些視頻信息(又叫元數(shù)據(jù))颠锉,比如視頻url法牲,視頻類型,ViewId等等琼掠。
??Client
主要用于創(chuàng)建Session拒垃,關(guān)聯(lián)Player和銷毀Session,發(fā)送事件(包括廣告事件瓷蛙、自定義事件)悼瓮。
三 集成流程
??導(dǎo)入SDK
到官網(wǎng)下載最新Conviva_SDK_Android,復(fù)制Conviva_SDK_Android到app的libs文件夾艰猬,Add As Liraty横堡。
找到Demo的helper文件夾,復(fù)制ConvivaSessionManager文件到項(xiàng)目中合適位置(建議復(fù)制冠桃,可以根據(jù)需求適當(dāng)修改命贴,也可以自己寫)。
??替換key
確定mGateWayUrl食听,測試階段的mGateWayUrl一般是https://testonly.conviva.com或ClientSettings.defaultDevelpomentGatewayUrl胸蛛,測試沒問題后替換成pulse的url;
??初始化
app啟動第一次調(diào)用即可(可以寫在Application)
ConvivaSessionManager.initClient(this,?mGateWayUrl);
??傳遞視頻基礎(chǔ)信息(配置元數(shù)據(jù))
創(chuàng)建Session,表示對一段視頻檢測的開始(位置在播放器開始請求視頻數(shù)據(jù)之前即可樱报,進(jìn)行到這一步Touchstone是有數(shù)據(jù)的葬项,盡管可能狀態(tài)不準(zhǔn)確),可以放在ConvivaSessionManager類中迹蛤,也可以根據(jù)需求調(diào)整位置民珍。如下:
//mStateManager需要在createSession和創(chuàng)建Proxy之前創(chuàng)建
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";
/*這里表示視頻類型是點(diǎn)播襟士,conviva有三個標(biāo)準(zhǔn):VOD,LIVE,UNKNOEN,分別表示直播,點(diǎn)播和未知類型嚷量。*/
convivaMetaData.streamType?=?ContentMetadata.StreamType.VOD;
convivaMetaData.duration?=?0;
convivaMetaData.encodedFrameRate?=?-1;
//自定義tag敌蜂,根據(jù)用戶實(shí)際需求,設(shè)置一些tag
Map?tags?=?new?HashMap<>();
Tags.put("key",?"value");
//創(chuàng)建Session津肛,后面的參數(shù)就是視頻的url
ConvivaSessionManager.createConvivaSession(convivaMetaData);
通過convivaMetaData信息都會在Touchstone上顯示,如下圖:
??實(shí)例化Proxy對象
監(jiān)聽Player的播放狀態(tài)等信息,這是對視頻進(jìn)行監(jiān)控的過程汗贫,Proxy對象要在mStateManager和mPlayer不為null的地方進(jìn)行實(shí)例化(后面詳細(xì)介紹這個Proxy的寫法)身坐。
//實(shí)例化Proxy,需要傳遞參數(shù)PlayerStateManager和播放器Player對象落包。
mPlayerInterface?=?new?CVXXXPlayerInterface(mStateManager,?mPlayer);
??對廣告部蛇,比特率,自定義錯誤咐蝇,自定義事件的操作
A:在廣告的監(jiān)聽中設(shè)置:
//廣告開始
ConvivaSessionManager.adStart();
//廣告開始自定義事件(可選涯鲁,作為廣告開始的補(bǔ)充,可傳遞一些信息)
ConvivaSessionManager.podEvent(ConvivaSessionManager.POS_EVENT_POD_START);
//廣告結(jié)束
ConvivaSessionManager.adEnd();
//廣告結(jié)束自定義事件(可選有序,作為廣告結(jié)束的補(bǔ)充抹腿,可傳遞一些信息)
ConvivaSessionManager.podEvent(ConvivaSessionManager.POS_EVENT_POD_END);
B:在可監(jiān)測比特率變化的監(jiān)聽中設(shè)置:
mPlayeStateManager.setBitrateKbps(…);
C:在系統(tǒng)檢測不到的錯誤,需要自定義的時候調(diào)用:
mPlayeStateManager.sendError(…);
D:在分辨變化的監(jiān)聽中設(shè)置:
mPlayeStateManager.setVideoWidth(…);
mPlayeStateManager.setVideoHeight(…);
??銷毀Session
表示對一段視頻的檢測結(jié)束旭寿。一般是在back時警绩,進(jìn)入下個視頻等結(jié)束該視頻時調(diào)用。包括三部分:
//清除Proxy
mPlayerInterface.cleanup();
//釋放mPlayeStateManager
ConvivaSessionManager.releasePlayerStateManager();
//清除Session
ConvivaSessionManager.cleanupConvivaSession();
?釋放Client
退出app時銷毀即可
ConvivaSessionManager.deinitClient();
四 測試
上述集成步驟進(jìn)行完以后盅称,conviva就可以對視頻進(jìn)行檢測肩祥,經(jīng)過后臺大數(shù)據(jù)分析返回有用的數(shù)據(jù)信息,初步集成之后需要用TouchStone進(jìn)行測試(測試流程詳見《TouchStone的使用及測試流程》)缩膝。
五 Proxy文件寫法
參考Demo的Proxy文件混狠,對照稍加修改即可。
在android開發(fā)中疾层,多次注冊Linister會被覆蓋掉将饺,只有最后一次注冊的Linister會生效。我們官方的Proxy文件都做了處理云芦,可參考我們的Proxy方案俯逾,按部就班的進(jìn)行,以免監(jiān)聽事件失效導(dǎo)致conviva檢測失敗舅逸。不同播放器的方案不盡相同桌肴,以下是兩種播放器的方案,一般來講琉历,第一種方案是自定義播放器常用的坠七,如果播放器的API不能滿足需求水醋,就采用第二種方案,開發(fā)者要靈活制定開發(fā)方案彪置,如果都不滿足需求拄踪,請登陸傳視網(wǎng)站咨詢我們。
ExoPlayer2 + Proxy:
Exoplayer的Proxy方案是依賴于各種Linister監(jiān)聽事件維護(hù)視頻播放狀態(tài)和獲取相關(guān)信息拳魁,因?yàn)橐曨l的四個狀態(tài)Buffering惶桐,Playing,Paused潘懊,Stopped以及其他信息都可以獲取到姚糊。
??在onPlayerStateChanges()回調(diào)方法中,可以更新四種必須的視頻播放狀態(tài)和視頻的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()回調(diào)方法中調(diào)用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()回調(diào)方法中調(diào)用mStateManager.sendError(errorCode, Client.ErrorSeverity.FATAL)提交錯誤救恨,ctrl點(diǎn)開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結(jié)束的回調(diào)方法onPositionDiscontinuity()中調(diào)用mStateManager.setPlayerSeekEnd(),告知后臺
@Override
public?void?onPositionDiscontinuity()?{
????try?{
????????mStateManager.setPlayerSeekEnd();
????}?catch?(ConvivaException?e)?{
????????e.printStackTrace();
????}
}
MediaPlayer Proxy:
MediaPlayer的Proxy方案是依賴于各種Linister監(jiān)聽事件和播放器Position結(jié)合定時任務(wù)維護(hù)視頻播放狀態(tài)和獲取相關(guān)信息释树。
A 依賴于監(jiān)聽事件
??在onPrepare()回調(diào)方法中更新視頻播放狀態(tài)為Buffering肠槽,更新播放器的Height,Width, 提交了視頻的Duration
@Override
public?void?onPrepared(MediaPlayer?mp)?{
????Log("OnPrepared",?SystemSettings.LogLevel.DEBUG);
????if?(_inListener)?return;
????//執(zhí)行該回調(diào)方法時奢啥,認(rèn)為player處于Buffering狀態(tài)
????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()的回調(diào)中,更新播放狀態(tài)為Stopped扫尺。
@Override
public?void?onCompletion(MediaPlayer?mp)?{
????Log("onCompletion",?SystemSettings.LogLevel.DEBUG);
????if?(_inListener)?return;
????_mIsPlayerActive?=?false;
????updateState(PlayerState.STOPPED);
????//可以觀察到筋栋,每個回調(diào)都會有這種寫法,目的是不影響外面對監(jiān)聽事件的使用正驻。如果不理解弊攘,對照寫就行。
????if?(_onCompListenerOrig?!=?null)?{
????????_inListener?=?true;
????????try?{
????????????_onCompListenerOrig.onCompletion(mp);
????????}?finally?{
????????????_inListener?=?false;
????????}
????}
}
??在onVideoSizeChanged()回調(diào)方法中調(diào)用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()回調(diào)方法中調(diào)用mStateManager.sendError(errorCode, Client.ErrorSeverity.FATAL)提交錯誤襟交,ctrl點(diǎn)開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
????//可以觀察到伤靠,每個回掉都會有這種寫法捣域,如果不理解,對照寫就行宴合,目的是不影響外面對監(jiān)聽事件的使用焕梅。
????if?(_onErrorListenerOrig?!=?null)?{
????????_inListener?=?true;
????????try?{
????????????return?_onErrorListenerOrig.onError(mp,?what,?extra);
????????}?finally?{
????????????_inListener?=?false;
????????}
????}
????return?true;
}
??在Seek結(jié)束的回調(diào)方法onSeekComplete()中調(diào)用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 依賴于定時任務(wù)
??除了onPrepare()和onCompeletion(),已經(jīng)沒有監(jiān)聽方法可以提供給我們更新Playing和Paused的狀態(tài)了贞言,Buffering在這里就調(diào)用一次,后面的Buffering狀態(tài)也沒辦法拿到阀蒂,于是就有了定時器去判斷這些狀態(tài)(依賴于判斷position變化確定播放狀態(tài))该窗。通過對上一次position和本次position的對比弟蚀,判斷Buffering,Playing,Paused狀態(tài)并提交給conviva。
//定時器200ms執(zhí)行一次任務(wù)
ITimerInterface?iTimerInterface?=?new?AndroidTimerInterface();
_mCancelTimer?=?iTimerInterface.createTimer(_pollStreamerTask,?200,?"CVMediaPlayerInterface");?
//根據(jù)position酗失,判斷狀態(tài)的定時任務(wù)
private?Runnable?_pollStreamerTask?=?new?Runnable()?{
????@Override
????public?void?run()?{
????????GetPlayheadTimeMs();
????}
};
//定時任務(wù)執(zhí)行的方法??
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狀態(tài)為true??????????????
????????????if(_mPlayer.isPlaying())?{???????????????????
????????????????//i如果當(dāng)前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之后才實(shí)例化PlayStateManager對象拖刃。
解決辦法:需要在CreateSession之前實(shí)例化PlayStateManager對象。