【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