Android:自定义播放器传视集成文档(java,AS)

请以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对象要在mStateManagermPlayer不为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对象。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 请以Conviva开发者社区作为主要参考源。 集成之前,请前往官网下载对应播放器类型的Demo,参考进行集成。 一...
    繁天涯阅读 446评论 0 0
  • 早晨还没起来,就听到一阵一阵的鞭炮声,不知道是在庆祝什么。 原本打算出行的,也拖到了明天,具体原因不明。 今天早上...
    云栗阅读 213评论 0 1
  • “激情”和“友谊”有所平衡,就是永久和谐最有效的处方。 现在,请拿起笔填下列的表格,一起评估你们的感情在“激情指数...
    红豆印迹阅读 512评论 0 0