Android VR Player(全景視頻播放器) [7]:視頻列表的實現-網絡視頻

Android VR Player(全景視頻播放器) [7]:視頻列表的實現-網絡視頻

前期準備

在之前的博文,Android VR Player(全景視頻播放器) [6]:視頻列表的實現-本地視頻 中, 和大家分享瞭如何使用AsnycTask來實現讀取本地媒體庫中的視頻信息,並用RecyclerView來實現以列表的方式進行展示。本篇博文以本地視頻列表的這篇博文爲基礎,繼續和大家分享如何實現“網絡視頻列表”。

這裏需要說明一下爲什麼每篇博文的代碼都是分開的,而不是一步步迭代,最後得到一個完整的播放器的。因爲自己也是剛開始Android開發,水平還很low,還不敢寫“跟着我一步步做全景視頻播放器”這樣的博客。寫這個系列的目的也主要是想分享自己在這個全景視頻播放器實現過程中遇到的一些問題的解決思路,方法,希望能給正好有這方面需要的同學一點點幫助。每部分單獨分開,也可以方便大家“各取所需”。


讀取網絡視頻列表

老朋友:doInBackground

前一篇博客中已近和大家分享過異步任務AsyncTask類的使用方法,我們把讀取本地媒體庫這個耗時的任務放在了doInBackground方法中。要實現網絡列表,就要讀取網絡視頻列表信息,而這個也是一個耗時的任務,也是要放到doInBackground中的。那麼我們的網絡視頻列表信息從何而來,又怎麼讀取呢?這就涉及到數據傳輸的格式的選擇和Android網絡流的相關操作了。


讀取JSON數據

在之前的博客 Linux ffmpeg視頻截圖,C中操作JSON數據 中我分享瞭如何在C中操作JSON數據,順便在那篇博客裏說了自己對於JSON數據的一些理解,有興趣的同學可以去看看,這裏就不再重複說明JSON的格式等內容,而是直接介紹如何在Android中解析服務器上的JSON數據。

既然談到服務器,首先我們得有個服務器啊!爲了方便,我們直接在本地用MyEclipse創建一個web項目TestOnlineList,然後把準備好的JSON數據和視頻縮略圖一起放到項目的WebRoot目錄下。如果你有遠程的服務器的話,也可以將JSON文件和其他一些必要的文件放到web服務器的目錄下。

我們準備的視頻列表的JSON文件爲:

{
    "status":   1,
    "data": [{
            "name": "dubai.mp4",
            "videoThumb":   "http://10.0.2.2:8080/TestOnlineList/dubai.mp4.jpg",
            "duration": "00:01:20.60",
            "createTime":   "2017-5-9 13:34",
            "path": "http://10.0.2.2:8080/TestOnlineList/dubai.mp4"
        }, {
            "name": "360test.mp4",
            "videoThumb":   "http://10.0.2.2:8080/TestOnlineList/360test.mp4.jpg",
            "duration": "00:00:42.63",
            "createTime":   "2017-5-9 14:1",
            "path": "http://10.0.2.2:8080/TestOnlineList/360test.mp4"
        }, {
            "name": "panda.mp4",
            "videoThumb":   "http://10.0.2.2:8080/TestOnlineList/panda.mp4.jpg",
            "duration": "00:01:56.82",
            "createTime":   "2017-5-9 14:1",
            "path": "http://10.0.2.2:8080/TestOnlineList/panda.mp4"
        }],
    "msg":  "SUCCESS"
}

包含三個視頻的信息,一個視頻對象有name,videoThumb(存放的視頻截圖的地址),duration等鍵值對。然後把這個項目添加MyEclipse自帶的Tomcat服務器中,並啓動服務器。現在我們的服務器端就準備好了,再來看Android端。

大家應該很容易想到,要讀取服務器數據,總得有個地址吧,但是這個地址應該怎麼寫呢?

 private static String jsonURL = "http://10.0.2.2:8080/TestOnlineList/videolist.json";

10.0.2.2是一個A類的局域網地址,用來表示本地局域網,我們在Android模擬器中需要使用這個地址來訪問本地局域網,注意不要用127.0.0.1。Tomcat默認的端口爲8080,所以我們還需要加上該端口,後面是我們的項目名,之後是json文件名。地址也有了,下一步就是讀取和解析。

先讀取,再解析。讀取其實就是從服務器拿到json數據流,並把它轉成String;解析則是把這個String按照JSON的組織格式把對象信息給提取出來。我們創建一個readStream方法,它用來讀取輸入流,返回String對象。

  private String readStream(InputStream is) {
            InputStreamReader isReader;
            String result = "";
            String line = "";
            try {
                isReader = new InputStreamReader(is, "utf-8");
                BufferedReader buffReader = new BufferedReader(isReader);
                while ((line = buffReader.readLine()) != null) {
                    result += line;
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            return result;
        }

完整的 doInBackground方法的代碼如下,但是我們只需留意其中的部分代碼:如何獲得JSONString,JSONObject的使用, JSONArray的使用,以及如何由地址讀取網絡圖片。

 @Override
        protected Void doInBackground(Void... params) {
            try {
                String jsonString = readStream(new URL(jsonURL).openStream());

                String path = "";
                String name = "";
                String createdTime = "";
                String strDuration = "";
                int duration2second = 0;
                boolean isLocal  = FALSE;
                String imageUrl = "";

                JSONObject jsonObject;
                jsonObject = new JSONObject(jsonString);
                JSONArray jsonArray = jsonObject.getJSONArray("data");

                Log.d(TAG, "doInBackground: jsonArray:"+ jsonArray.toString());

                for (int i = 0; i < jsonArray.length(); i++) {
                    jsonObject = jsonArray.getJSONObject(i);

                    imageUrl = jsonObject.getString("videoThumb");
                    name = jsonObject.getString("name");//
                    strDuration = jsonObject.getString("duration");
                    String subStrDuration = "";
                    try {
                        //change xx:xx:xx to int
                        subStrDuration = strDuration.substring(0,8);
                        duration2second = 0;
                    } catch (NumberFormatException e) {
                        e.printStackTrace();
                    }
                    createdTime = jsonObject.getString("createTime");
                    path = jsonObject.getString("path");

                    VideoItem data = new VideoItem(path, name, createdTime,duration2second,subStrDuration,isLocal,imageUrl);  
                    data.createThumb();
                    publishProgress(data);
                    mDataList.add(data);
                }//for
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (JSONException e) {
                e.printStackTrace();
            }
            return null;
        }

JSONString使用我們之前的readStream即可獲取。JsonObject,也就是JSON對象,JSONArray也就是JSON數組,JSON數組由JSON對象組成。先用

jsonObject = new JSONObject(jsonString);

把從網絡流中讀到的jsonString中的JSON對象解析出來。在我們服務器的videolist.json中,視頻信息對象數組的名字是“data”,於是我們用JSONObject的getJSONArray方法,獲得JSON數組。

  JSONArray jsonArray = jsonObject.getJSONArray("data");

大家可能很迷糊了,一會兒對象,一會兒數組的,難道不應該是先把數組解析出來,再一個個得到數組中的對象嗎?爲什麼是用JSONObject的getJSONArray方法去獲得數組呢?簡單地說,JSON中,{}括起來的是對象,[]括起來的是數組,所以我們先用new JSONObject(jsonString)獲取的對象是包含了data這個數組的一個對象,然後再從這個對象中用getJSONArray去獲得JSON數組。所以會看到,在循環 for (int i = 0; i < jsonArray.length(); i++) 中,我們有 jsonArray.getJSONObject(i),這時獲取的對象顯然就是JSON數組中的對象了。

可能有點繞,不過這點清楚了的話,後面就沒什麼難題了,針對每個數組中的每個object,用getString(“key”)去獲取key對應的value就可以了。然後再把獲得的值給VideoItem對應的屬性。

我們把每個視頻的縮略圖的地址也通過JSON傳輸過來了,要在列表中展示縮略圖就涉及到如何從得到的圖片地址去讀取網絡圖片。我們在VideoItem中寫了一個getBitmapFromUrl,

  //get online video thumb by url
    public Bitmap getBitmapFromUrl(String urlString) {
        InputStream is = null;
        Bitmap bitmap;
        try {
            URL url = new URL(urlString);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            is = new BufferedInputStream(connection.getInputStream());
            bitmap = BitmapFactory.decodeStream(is);
            connection.disconnect();
            return bitmap;
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally
        {
            try {
                is.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return null;
    }

主要用到HttpURLConnection來獲取網絡輸入流,並利用工廠方法BitmapFactory.decodeStream來解析輸入流,獲得bitmap。

準備好需要的數據後,具體怎麼展示到RecyclerView中就和本地視頻列表一樣了。


監聽網絡變化

有的時候我們的網絡並不是可用的,這時就沒法加載我們的網絡視頻列表,我們需要在網絡不可用時給用戶相應的反饋,比如彈出個Toast,提示“當前網絡不可用”。

監聽網絡變化需要用到Android的廣播機制,廣播接收器和前一篇博客中提到的內容提供器一樣,是Android四大組件之一。

    class NetworkChangeReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent){
            ConnectivityManager mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
            if(mNetworkInfo != null && mNetworkInfo.isAvailable()){
                Log.d(TAG, "onReceive: NETWORK AVAILABLE");
            }else{
                Toast.makeText(context,"network is unavailable",Toast.LENGTH_SHORT).show();
            }
        }
    }

我們在MainActivity中創建一個內部類,NetworkChangeReceiver,它繼承自 BroadcastReceiver,然後我們重寫了onReceive方法,並且在網絡不可用時,Toast一條消息“network is unavailable”。代碼很簡單,方法名,類名都很規範,所以應該很容易看懂其中的意思。根據《第一行代碼》書中講的(沒有實踐驗證,大家有興趣可以試一下),因爲廣播接收器中不允許開啓線程,如果onReceive方法很久都沒有結束的話,程序會報錯,所以儘量只在onReceive中執行耗時較短的簡單的代碼。

然後在MainActivity的onCreate方法中動態地註冊網絡變化監聽,當然也可以靜態地在Androidmanifest中註冊,兩者的區別是動態方式更加靈活,但必須要程序啓動後才能開始監聽,而靜態的方式在程序未啓動時也能接收到廣播。

IntentFilter mIntentFilter = new IntentFilter();
mIntentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
NetworkChangeReceiver mNetworkChangeReceiver = new NetworkChangeReceiver();
this.registerReceiver(mNetworkChangeReceiver,mIntentFilter);

mIntentFilter用來指定我們意圖的動作是監聽“CONNECTIVITY_CHANGE”(連通變化,或者連接變化),實例化一個NetworkChangeReceiver,然後動態地註冊這個監聽。注意別忘了在MainActivity中onDestroy使用

this.unregisterReceiver(mNetworkChangeReceiver);

來註銷監聽。

需要注意的是,我們就算在模擬器上開了飛行模式,還是可以正常訪問到本地服務器的數據的,但網絡監聽還是會提示網絡狀態不可用,真正使用外網服務器時,網絡不可用的情況下是不能讀取到網絡服務器數據的。

最後是權限問題,在Androidmanifest中添加以下權限,第一個權限是用來訪問網絡狀態,第二個權限是訪問WIFI網絡狀態(後期網絡視頻播放時會用到,比如用戶在非WIFI模式下播放網絡視頻,可以提供相應的提示),第三個權限是網絡訪問權限,這是我們訪問網絡服務器數據時需要的。這幾個權限都不是敏感權限,所以靜態註冊即可。

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>

與本地視頻列表的整合

可以看到,本地視頻列表的實現和網絡視頻列表的實現其實沒有太大差異,只是數據的獲取方式不同,一個讀本地,一個讀網絡。所以,如果要做一個包含本地和網絡視頻列表的app時,VideoItem.java,VideoItemAdapter.java,videoitem.xml等等都是可以用同一個的,只是需要做一些判斷。比如在VideoItem的構造函數中傳入指定這個video是網絡視頻還是本地視頻,然後在VideoItem中分別針對網絡和本地視頻提供相應的方法來生成視頻縮略圖。

網絡視頻列表的解析,視頻縮略圖的創建等都是比較耗時的操作,再聯繫前面WelcomeActivity的作用,我們就可以把這部分比較耗時的操作放在WelcomeActivity中,需要用的時候直接拿過來用就可以了,用戶就不用進入app後再等待加載資源,從而提升了用戶體驗。

至此,全景視頻播放器大致的UI設計已經完成,基本有了 Android VR Player(全景視頻播放器) [1]:項目介紹 中展示的效果。“千米長跑”,我們已經跑完了頭一百米,開了個好頭,後面就是更大的挑戰。視頻播放控制,OpenGL ES的使用,網絡視頻壓縮傳輸,服務器搭建等等都比界面設計更加複雜,更加困難。下一篇博客會分享一下Android中視頻播放相關的問題,主要內容是一個簡單的視頻播放控制的實現。


測試結果

這裏寫圖片描述
網絡視頻列表的展示效果

這裏寫圖片描述
網絡狀態監聽,可以看到開了飛行模式的情況下,彈出網絡不可用的提示“network is unavailable”,說明網絡監聽監聽生效了。需要注意的是,因爲我們使用的是本地局域網,所以沒有網絡情況下仍然是可以加載本地tomcat服務器上的視頻列表數據的。


Reference

Android之JSON格式數據解析
Android異步加載訪問網絡圖片-解析json
Linux ffmpeg視頻截圖,C中操作JSON數據
JSON的三種解析方式


參考源碼

鏈接: https://pan.baidu.com/s/1i4S83hN 密碼: qfcu
源碼中service壓縮包爲web工程源碼,Android壓縮包爲Android工程源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章