【0123】【项目实战】-【组件化封装思想实战Android App】-【6】视频播放SDK模块开发


1.实现的效果及思路

1.1 实现的效果

【功能】

【1】在视频部分的高度不足50%不播放,在下拉达到50%再播放;

【2】可以全屏播放;

【3】播放完之后跳转到原来的界面,并向服务上传该视频播放的次数;

1.2  开发的流程

2.播放器的封装讲解

2.1 实现视频播放器的方案选择

 

2.2 视频播放器的生命周期

【摘自博客】

1、方法调用及异常说明

1)当一个MediaPlayer对象被刚刚用new操作符创建或是调用了reset()方法后,它就处于Idle状态。当调用了release()方法后,它就处于End状态。这两种状态之间是MediaPlayer对象的生命周期。

1.1)在一个新构建的MediaPlayer对象和一个调用了reset()方法的MediaPlayer对象之间有一个微小的但是十分重要的差别。在处于Idle状态时,调用getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setAudioStreamType(int), setLooping(boolean),setVolume(float, float), pause(), start(), stop(), seekTo(int), prepare() 或者 prepareAsync() 方法都是编程错误。当一个MediaPlayer对象刚被构建的时候,内部的播放引擎和对象的状态都没有改变,在这个时候调用以上的那些方法,框架将无法回调客户端程序注册的OnErrorListener.onError()方法;但若这个MediaPlayer对象调用了reset()方法之后,再调用以上的那些方法,内部的播放引擎就会回调客户端程序注册的OnErrorListener.onError()方法了,并将错误的状态传入。
1.2) 我们建议,一旦一个MediaPlayer对象不再被使用,应立即调用release()方法来释放在内部的播放引擎中与这个MediaPlayer对象关联的资源。资源可能包括如硬件加速组件的单态组件,若没有调用release()方法可能会导致之后的MediaPlayer对象实例无法使用这种单态硬件资源,从而退回到软件实现或运行失败。一旦MediaPlayer对象进入了End状态,它不能再被使用,也没有办法再迁移到其它状态。
1.3) 此外,使用new操作符创建的MediaPlayer对象处于Idle状态,而那些通过重载的create()便利方法创建的MediaPlayer对象却不是处于Idle状态。事实上,如果成功调用了重载的create()方法,那么这些对象已经是Prepare状态了。

2) 在一般情况下,由于种种原因一些播放控制操作可能会失败,如不支持的音频/视频格式,缺少隔行扫描的音频/视频,分辨率太高,流超时等原因,等等。因此,错误报告和恢复在这种情况下是非常重要的。有时,由于编程错误,在处于无效状态的情况下调用了一个播放控制操作可能发生。在所有这些错误条件下,内部的播放引擎会调用一个由客户端程序员提供的OnErrorListener.onError()方法。客户端程序员可以通过调用MediaPlayer.setOnErrorListener(android.media.MediaPlayer.OnErrorListener)方法来注册OnErrorListener.
2.1) 一旦发生错误,MediaPlayer对象会进入到Error状态。
2.2) 为了重用一个处于Error状态的MediaPlayer对象,可以调用reset()方法来把这个对象恢复成Idle状态。
2.3) 注册一个OnErrorListener来获知内部播放引擎发生的错误是好的编程习惯。
2.4) 在不合法的状态下调用一些方法,如prepare(),prepareAsync()和setDataSource()方法会抛出IllegalStateException异常。

3) 调用setDataSource(FileDescriptor)方法,或setDataSource(String)方法,或setDataSource(Context,Uri)方法,或setDataSource(FileDescriptor,long,long)方法会使处于Idle状态的对象迁移到Initialized状态。
3.1) 若当此MediaPlayer处于其它的状态下,调用setDataSource()方法,会抛出IllegalStateException异常。
3.2) 好的编程习惯是不要疏忽了调用setDataSource()方法的时候可能会抛出的IllegalArgumentException异常和IOException异常。

4) 在开始播放之前,MediaPlayer对象必须要进入Prepared状态。
4.1) 有两种方法(同步和异步)可以使MediaPlayer对象进入Prepared状态:要么调用prepare()方法(同步),此方法返回就表示该MediaPlayer对象已经进入了Prepared状态;要么调用prepareAsync()方法(异步),此方法会使此MediaPlayer对象进入Preparing状态并返回,而内部的播放引擎会继续未完成的准备工作。当同步版本返回时或异步版本的准备工作完全完成时就会调用客户端程序员提供的OnPreparedListener.onPrepared()监听方法。可以调用MediaPlayer.setOnPreparedListener(android.media.MediaPlayer.OnPreparedListener)方法来注册OnPreparedListener.
4.2) Preparing是一个中间状态,在此状态下调用任何具备边影响的方法的结果都是未知的!
4.3) 在不合适的状态下调用prepare()和prepareAsync()方法会抛出IllegalStateException异常。当MediaPlayer对象处于Prepared状态的时候,可以调整音频/视频的属性,如音量,播放时是否一直亮屏,循环播放等。

5) 要开始播放,必须调用start()方法。当此方法成功返回时,MediaPlayer的对象处于Started状态。isPlaying()方法可以被调用来测试某个MediaPlayer对象是否在Started状态。
5.1) 当处于Started状态时,内部播放引擎会调用客户端程序员提供的OnBufferingUpdateListener.onBufferingUpdate()回调方法,此回调方法允许应用程序追踪流播放的缓冲的状态。
5.2) 对一个已经处于Started 状态的MediaPlayer对象调用start()方法没有影响。

6) 播放可以被暂停,停止,以及调整当前播放位置。当调用pause()方法并返回时,会使MediaPlayer对象进入Paused状态。注意Started与Paused状态的相互转换在内部的播放引擎中是异步的。所以可能需要一点时间在isPlaying()方法中更新状态,若在播放流内容,这段时间可能会有几秒钟。
6.1) 调用start()方法会让一个处于Paused状态的MediaPlayer对象从之前暂停的地方恢复播放。当调用start()方法返回的时候,MediaPlayer对象的状态会又变成Started状态。
6.2) 对一个已经处于Paused状态的MediaPlayer对象pause()方法没有影响。

7) 调用stop()方法会停止播放,并且还会让一个处于Started,Paused,Prepared或PlaybackCompleted状态的MediaPlayer进入Stopped状态。
7.1) 对一个已经处于Stopped状态的MediaPlayer对象stop()方法没有影响。

8) 调用seekTo()方法可以调整播放的位置。
8.1) seekTo(int)方法是异步执行的,所以它可以马上返回,但是实际的定位播放操作可能需要一段时间才能完成,尤其是播放流形式的音频/视频。当实际的定位播放操作完成之后,内部的播放引擎会调用客户端程序员提供的OnSeekComplete.onSeekComplete()回调方法。可以通过setOnSeekCompleteListener(OnSeekCompleteListener)方法注册。
8.2) 注意,seekTo(int)方法也可以在其它状态下调用,比如Prepared,Paused和PlaybackCompleted状态。此外,目前的播放位置,实际可以调用getCurrentPosition()方法得到,它可以帮助如音乐播放器的应用程序不断更新播放进度

9) 当播放到流的末尾,播放就完成了。
9.1) 如果调用了setLooping(boolean)方法开启了循环模式,那么这个MediaPlayer对象会重新进入Started状态。
9.2) 若没有开启循环模式,那么内部的播放引擎会调用客户端程序员提供的OnCompletion.onCompletion()回调方法。可以通过调用MediaPlayer.setOnCompletionListener(OnCompletionListener)方法来设置。内部的播放引擎一旦调用了OnCompletion.onCompletion()回调方法,说明这个MediaPlayer对象进入了PlaybackCompleted状态。
9.3) 当处于PlaybackCompleted状态的时候,可以再调用start()方法来让这个MediaPlayer对象再进入Started状态。

2、生命周期状态说明
  这张状态转换图清晰的描述了MediaPlayer的各个状态,也列举了主要的方法的调用时序,每种方法只能在一些特定的状态下使用,如果使用时MediaPlayer的状态不正确则会引发IllegalStateException异常。

Idle 状态:当使用new()方法创建一个MediaPlayer对象或者调用了其reset()方法时,该MediaPlayer对象处于idle状态。这两种方法的一个重要差别就是:如果在这个状态下调用了getDuration()等方法(相当于调用时机不正确),通过reset()方法进入idle状态的话会触发OnErrorListener.onError(),并且MediaPlayer会进入Error状态;如果是新创建的MediaPlayer对象,则并不会触发onError(),也不会进入Error状态。

End 状态:通过release()方法可以进入End状态,只要MediaPlayer对象不再被使用,就应当尽快将其通过release()方法释放掉,以释放相关的软硬件组件资源,这其中有些资源是只有一份的(相当于临界资源)。如果MediaPlayer对象进入了End状态,则不会在进入任何其他状态了。

Initialized 状态:这个状态比较简单,MediaPlayer调用setDataSource()方法就进入Initialized状态,表示此时要播放的文件已经设置好了。

Prepared 状态:初始化完成之后还需要通过调用prepare()或prepareAsync()方法,这两个方法一个是同步的一个是异步的,只有进入Prepared状态,才表明MediaPlayer到目前为止都没有错误,可以进行文件播放。

Preparing 状态:这个状态比较好理解,主要是和prepareAsync()配合,如果异步准备完成,会触发OnPreparedListener.onPrepared(),进而进入Prepared状态。

Started 状态:显然,MediaPlayer一旦准备好,就可以调用start()方法,这样MediaPlayer就处于Started状态,这表明MediaPlayer正在播放文件过程中。可以使用isPlaying()测试MediaPlayer是否处于了Started状态。如果播放完毕,而又设置了循环播放,则MediaPlayer仍然会处于Started状态,类似的,如果在该状态下MediaPlayer调用了seekTo()或者start()方法均可以让MediaPlayer停留在Started状态。

Paused 状态:Started状态下MediaPlayer调用pause()方法可以暂停MediaPlayer,从而进入Paused状态,MediaPlayer暂停后再次调用start()则可以继续MediaPlayer的播放,转到Started状态,暂停状态时可以调用seekTo()方法,这是不会改变状态的。

Stop 状态:Started或者Paused状态下均可调用stop()停止MediaPlayer,而处于Stop状态的MediaPlayer要想重新播放,需要通过prepareAsync()和prepare()回到先前的Prepared状态重新开始才可以。

PlaybackCompleted状态:文件正常播放完毕,而又没有设置循环播放的话就进入该状态,并会触发OnCompletionListener的onCompletion()方法。此时可以调用start()方法重新从头播放文件,也可以stop()停止MediaPlayer,或者也可以seekTo()来重新定位播放位置。

Error状态:如果由于某种原因MediaPlayer出现了错误,会触发OnErrorListener.onError()事件,此时MediaPlayer即进入Error状态,及时捕捉并妥善处理这些错误是很重要的,可以帮助我们及时释放相关的软硬件资源,也可以改善用户体验。通过setOnErrorListener(android.media.MediaPlayer.OnErrorListener)可以设置该监听器。如果MediaPlayer进入了Error状态,可以通过调用reset()来恢复,使得MediaPlayer重新返回到Idle状态。

 2.3 踏过的坑

【说明】播放属于后台的服务,android 6.0 之后对后台内存的回收管理加强了;

2.4 核心的类

3.视频播放器接口功能描述

【接口的继承】

 

【变量的定义】

【构造方法】

【数据的初始化】

【布局】q0pwzp\Client_Code\vuandroidadsdk\src\main\res\layout\xadsdk_video_player.xml

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <RelativeLayout
 3     android:id="@+id/mraid_content_layout"
 4     xmlns:android="http://schemas.android.com/apk/res/android"
 5     android:layout_width="match_parent"
 6     android:layout_height="match_parent"
 7     android:background="@android:color/black"
 8 >
 9 
10     <TextureView
11         android:id="@+id/xadsdk_player_video_textureView"
12         android:layout_width="match_parent"
13         android:layout_height="wrap_content"
14         android:layout_centerInParent="true"
15         android:duplicateParentState="true"
16     />
17 
18     <ImageView
19         android:id="@+id/framing_view"
20         android:layout_width="match_parent"
21         android:layout_height="match_parent"
22         android:background="@color/xadsdk_frame_bg"
23         android:scaleType="fitCenter"
24         android:src="@drawable/xadsdk_img_error"
25         android:visibility="gone"
26     />
27 
28     <ImageView
29         android:id="@+id/xadsdk_to_full_view"
30         android:layout_width="wrap_content"
31         android:layout_height="wrap_content"
32         android:layout_alignParentRight="true"
33         android:layout_marginRight="14dp"
34         android:layout_marginTop="14dp"
35         android:padding="10dp"
36         android:scaleType="center"
37         android:src="@drawable/xadsdk_ad_mini"
38 
39     />
40 
41     <Button
42         android:id="@+id/xadsdk_small_play_btn"
43         android:layout_width="80dp"
44         android:layout_height="80dp"
45         android:layout_centerInParent="true"
46         android:background="@drawable/xadsdk_ad_play"
47         android:visibility="gone"
48     />
49 
50     <ImageView
51         android:id="@+id/loading_bar"
52         android:layout_width="wrap_content"
53         android:layout_height="wrap_content"
54         android:layout_centerInParent="true"
55         android:background="@drawable/xadsdk_ad_loading_anim"
56     />
57     <!--<ViewStub-->
58     <!--android:id="@+id/small_view_stub"-->
59     <!--android:layout_width="match_parent"-->
60     <!--android:layout_height="match_parent"-->
61     <!--android:layout_centerInParent="true"-->
62     <!--android:layout="@layout/xadsdk_video_player_small_layout"-->
63     <!--/>-->
64 
65     <!--<ViewStub-->
66     <!--android:id="@+id/big_view_stub"-->
67     <!--android:layout_width="match_parent"-->
68     <!--android:layout_height="match_parent"-->
69     <!--android:layout_centerInParent="true"-->
70     <!--android:layout="@layout/xadsdk_video_player_big_layout"-->
71     <!--/>-->
72 </RelativeLayout>

 

【核心的接口】【1】一种是状态的处理;【2】功能方法的实现;

 

4.实现视频播放器的生命周期的方法

 

4.1 load加载方法的实现

【核心方法】

4.2 onPrepared 方法的实现

 

4.3 resume()方法的实现

【核心】

4.4 onCommplete()方法的实现

4.5 playBack()方法

【说明】与stop方法有些类似,但是不会将播放器置为空,如果用户需要再次播放的话,可以直接播放;不需要再次load;

可以节省流量,也可以节省时间;

 

4.6 onError()的处理

 

4.7 onPause()方法的实现

4.8 onLoad方法的调用的时机

【说明】只有在Texture准备好之后才可以对视频进行加载;

 

5.视频播放器的工具类的实现

 5.1 锁屏和解锁的处理

 5.2 当前播放页面显示与不显示的处理

【说明】当前页面切换到另外一个页面的时候,该页面就会被挂载到后台;播放视频也需要处理;此时系统会提供方法进行回调;

5.3 测试播放器

6.业务逻辑封装概述

6.1  概述

 

6.2 接口回调的讲解

 

 

 

 

 

【定义接口】

 

 【实现接口】

 

【接口的调用】 

 

6.3 业务逻辑的实现-监听播放器产生的各种事件并实现逻辑

【说明】封装具有两层,一层是从customVideoview到VedioAdslot层;一层是从VedioAdslot到最外层的API层;

 

【视频的点击事件】

【视频加载的成功和失败】

【视频播放结束】

【跳转到指定的位置】

6.4 完成滑出屏幕自动暂停、滑入屏幕自动播放的效果

【说明】此时的是customVideoView的自定义控件,是视频播放区域,并非是listView或者是GridView,如何知道外界出现了滑动呢?

 答:自己是无法知道外界是否发生了滑动的;其实很简单,只要我们将功能实现了,调用方是否使用,比如在GridView中使用ScrollerListener,然后调用该方法(在api层再封装)即可;

【计算在屏幕中占有的百分比】

【逻辑书写】

【测试效果】下拉不到50%不会播放;在切换到别的fragment时候,会暂停原来的播放;

7. 小屏幕到全屏的开发

7.1 方案的选择

 

7.2 逻辑的实现

【监听注入的方式】【1】构造方法中;【2】setListener();【3】第三方注解框架;

 

 

【在dialog获取焦点的时候调用该方法】

【dismiss的复写】

7.3 调用该方法

【逻辑包含】从小屏到全屏;从全屏到小屏;

 

 

 8.API层的封装

【外观模式】参考友盟的封装方式;

【封装的类】q0pwzp\Client_Code\vuandroidadsdk\src\main\java\com\youdu\core\video\VideoAdContext.java 本节视频差6min;

  1 package com.youdu.core.video;
  2 
  3 import android.content.Intent;
  4 import android.view.ViewGroup;
  5 
  6 import com.youdu.activity.AdBrowserActivity;
  7 import com.youdu.core.AdContextInterface;
  8 import com.youdu.core.video.VideoAdSlot.AdSDKSlotListener;
  9 import com.youdu.module.AdValue;
 10 import com.youdu.okhttp.HttpConstant;
 11 import com.youdu.okhttp.HttpConstant.Params;
 12 import com.youdu.report.ReportManager;
 13 import com.youdu.adutil.ResponseEntityToModule;
 14 import com.youdu.adutil.Utils;
 15 import com.youdu.widget.CustomVideoView.ADFrameImageLoadListener;
 16 
 17 /**
 18  * @author: qndroid
 19  * @function: 管理slot, 与外界进行通信
 20  * @date: 16/6/1
 21  */
 22 public class VideoAdContext implements AdSDKSlotListener {
 23 
 24     //the ad container
 25     private ViewGroup mParentView;
 26 
 27     private VideoAdSlot mAdSlot;
 28     private AdValue mInstance = null;
 29     //the listener to the app layer
 30     private AdContextInterface mListener;
 31     private ADFrameImageLoadListener mFrameLoadListener;
 32 
 33     public VideoAdContext(ViewGroup parentView, String instance,
 34                           ADFrameImageLoadListener frameLoadListener) {
 35         this.mParentView = parentView;
 36         this.mInstance = (AdValue) ResponseEntityToModule.
 37                 parseJsonToModule(instance, AdValue.class);
 38         this.mFrameLoadListener = frameLoadListener;
 39         load();
 40     }
 41 
 42     /**
 43      * init the ad,不调用则不会创建videoview
 44      */
 45     public void load() {
 46         if (mInstance != null && mInstance.resource != null) {
 47             mAdSlot = new VideoAdSlot(mInstance, this, mFrameLoadListener);
 48             //发送解析成功事件
 49             sendAnalizeReport(Params.ad_analize, HttpConstant.AD_DATA_SUCCESS);
 50         } else {
 51             mAdSlot = new VideoAdSlot(null, this, mFrameLoadListener); //创建空的slot,不响应任何事件
 52             if (mListener != null) {
 53                 mListener.onAdFailed();
 54             }
 55             sendAnalizeReport(Params.ad_analize, HttpConstant.AD_DATA_FAILED);
 56         }
 57     }
 58 
 59     /**
 60      * release the ad
 61      */
 62     public void destroy() {
 63         mAdSlot.destroy();
 64     }
 65 
 66     public void setAdResultListener(AdContextInterface listener) {
 67         this.mListener = listener;
 68     }
 69 
 70     /**
 71      * 根据滑动距离来判断是否可以自动播放, 出现超过50%自动播放,离开超过50%,自动暂停
 72      */
 73     public void updateAdInScrollView() {
 74         if (mAdSlot != null) {
 75             mAdSlot.updateAdInScrollView();
 76         }
 77     }
 78 
 79     @Override
 80     public ViewGroup getAdParent() {
 81         return mParentView;
 82     }
 83 
 84     @Override
 85     public void onAdVideoLoadSuccess() {
 86         if (mListener != null) {
 87             mListener.onAdSuccess();
 88         }
 89         sendAnalizeReport(Params.ad_load, HttpConstant.AD_PLAY_SUCCESS);
 90     }
 91 
 92     @Override
 93     public void onAdVideoLoadFailed() {
 94         if (mListener != null) {
 95             mListener.onAdFailed();
 96         }
 97         sendAnalizeReport(Params.ad_load, HttpConstant.AD_PLAY_FAILED);
 98     }
 99 
100     @Override
101     public void onAdVideoLoadComplete() {
102     }
103 
104     @Override
105     public void onClickVideo(String url) {
106         if (mListener != null) {
107             mListener.onClickVideo(url);
108         } else {
109             Intent intent = new Intent(mParentView.getContext(), AdBrowserActivity.class);
110             intent.putExtra(AdBrowserActivity.KEY_URL, url);
111             mParentView.getContext().startActivity(intent);
112         }
113     }
114 
115     /**
116      * 发送广告数据解析成功监测
117      */
118     private void sendAnalizeReport(Params step, String result) {
119         try {
120             ReportManager.sendAdMonitor(Utils.isPad(mParentView.getContext().
121                             getApplicationContext()), mInstance == null ? "" : mInstance.resourceID,
122                     (mInstance == null ? null : mInstance.adid), Utils.getAppVersion(mParentView.getContext()
123                             .getApplicationContext()), step, result);
124         } catch (Exception e) {
125 
126         }
127     }
128 }

9.使用

【三步走】

 

 【总结】


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM