Android視頻客戶端的設計與實現

1.前言
筆者最近正在給網站視頻模塊開發android手機客戶端,通過手機客戶端可以很方便的瀏覽網站的視頻內容,網站的視頻內容大部分是flv和mp4格式,以下為手機客戶端的部分截圖:

下面記錄下筆者的開發過程和註意事項

2.開發工具vc3Ryb25nPjxicj4Kz+7Ev7v509pBbmRyb2lkIFN0dWRpbyBJREW5ub2oo6xBbmRyb2lkIFN0dWRpb8rHMjAxMyBnb29nbGUgSS9Pv6q3otXftPO74c3Gs/a1xKOsu/nT2kludGVsbGlKIGlkZWG5ub2oo6xhbmRyb2lkIHN0dWRpb9K71rHU2rj80MLN6snGo6y98czs0tG+rbW9wcswLjQuNtSkwMCw5qOsztK5wLzGtb3By73xxOq1xDIwMTQgZ29vZ2xlIEkvT7Tzu+G74bW9MS4wzsi2qLDmoaPT0MjLtaPQxLTTRWNsaXBzZceo0sa1vUFuZHJvaWQgU3R1ZGlvsrvKytOmo6yyu87ItqijrNOwz+y/qreivfi2yKOs1eLA77TTscrV37XEx9fJ7czl0em45svftPO80kFuZHJvaWQgU3R1ZGlv08PG8MC01ea1xLrcyN3S18nPytajrLb4x9K087TzzOG437+qt6K9+LbIo6xBbmRyb2lkIFN0dWRpb8rHzrTAtLXEt73P8qOhQW5kcm9pZAogU3R1ZGlvu7m8r7PJwcvPyL34tcRHcmFkbGW5ub2oz7XNs6Ossb7P7sS/0rLKx7v509pHcmFkbGXP7sS/ubm9qKOsttTT2kFuZHJvaWTP7sS/1tC+rbOj0qrSwMC1TGlicmFyeSBwcm9qZWN0c7rct72x46OsudjT2kdyYWRsZaOstPO80r/J0tSyzr+0Z29vZ2xludm3vc7EtbVodHRwOi8vdG9vbHMuYW5kcm9pZC5jb20vdGVjaC1kb2NzL25ldy1idWlsZC1zeXN0ZW0vdXNlci1ndWlkZaOsQW5kcm9pZAogU3R1ZGlvu7m8r7PJwctWQ1Ow5rG+v9jWxs+1zbOjrLHK1d+/ydLUuty3vbHjtcS9q9S0wuvM4b27tb1naXRodWLJz6GjPGJyPgo8YnI+CjxzdHJvbmc+My5BbmRyb2lkv827p7bLz+7Ev7XEubm9qDwvc3Ryb25nPjxicj4Ksb7P7sS/tcS9qMGiss6/vMHLtPrC67zSyei8xrXEQW5pbWVUYXN0ZaOsuNDQu7T6wuu80rXEv6rUtKOhz8LD5rzytaW96cncz8LKtc/Wy7zCt6O6yta7+r/Nu6e2y82ouf3P8rf+zvHG97bLt6LLzWh0dHDH68fzo6y3/s7xxve2y2Fwab3Tv9q3tbvYanNvbsr9vt2jrMi7uvPK1rv6v827p7bLveLO9mpzb27K/b7do6zIu7rzvavK/b7d1bnKvtTabGlzdHZpZXfW0KGjPGJyPgooMSnP7sS/tcTEv8K8veG5uTxicj4KPHA+PGltZyBzcmM9″/uploadfile/Collfiles/20140307/20140307151018112.jpg” alt=”\”>

(2)項目的LoadActivity為app的main Activity啟動界面,init()方法主要是從服務器端獲取數據進行數據的初始化,服務器端返回的數據為JSONArray格式,即變量response,通過intent.putExtra(“LoadData”,response.toString())將數據放在intent中以便將數據傳遞到StartActivity.

package com.zyy360.app;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.support.v7.app.ActionBarActivity;
import com.loopj.android.http.JsonHttpResponseHandler;
import com.zyy360.app.core.DataVideoFetcher;
import com.zyy360.app.ui.StartActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import org.json.JSONArray;

/**
 * @author daimajia
 * @modified Foxhu
 * @version 1.0
 */
public class LoadActivity extends ActionBarActivity {
    private Context mContext;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if(getSupportActionBar() != null){
            getSupportActionBar().hide();
        }
        mContext = this;

        setContentView(R.layout.activity_load);

        if (NetworkUtils.isWifi(mContext) == false){
            AlertDialog.Builder builder = new AlertDialog.Builder(mContext)
                    .setTitle(R.string.only_wifi_title).setMessage(R.string.only_wifi_body);
            builder.setCancelable(false);
            //if user click ok then init data
            builder.setPositiveButton(R.string.only_wifi_ok,
                    new OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                            init();
                        }
                    });
            //if user click quit then finish()
            builder.setNegativeButton(R.string.only_wifi_cancel,
                    new OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            finish();
                        }
                    });
            builder.create().show();
        }else{
            init();
        }
    }

    /**
     * init data
     */
    private void init(){
        DataVideoFetcher.instance().getList(0,new JsonHttpResponseHandler(){
            /**
             * The server returns data format like
             * [{"name":"冬蟲夏草","path":"2013/10/25_152747_61dP.flv","video_pic":"20131025/IMG_9La6_25_b.jpg","video_thumbpic":"20131025/IMG_BjTA_25_s.jpg","introduce":"冬蟲夏草多種功效。","___key_id":25},
             * {"name":"防風?","path":"2013/10/25_152557_pmYc.flv","video_pic":"20131025/IMG_H0zO_24_b.jpg","video_thumbpic":"20131025/IMG_3b76_24_s.jpg","introduce":"解表藥、祛風藥","___key_id":24}]
             * reference documnets
             * https://loopj.com/android-async-http/doc/com/loopj/android/http/JsonHttpResponseHandler.html
             * @param statusCode
             * @param response
             */
            @Override
            public void onSuccess(int statusCode, JSONArray response) {

                super.onSuccess(statusCode, response);
                System.out.println("jsonArray->>"+response);
                Intent intent = new Intent(LoadActivity.this,StartActivity.class);

                if (statusCode == 200 && response.length()>0){
                    try {
                        intent.putExtra("LoadData",response.toString());
                        startActivity(intent);
                        finish();
                    }catch (Exception e) {
                        e.printStackTrace();
                    }

                }else{}
            }

            @Override
            public void onFailure(Throwable e, JSONArray errorResponse) {
                super.onFailure(e, errorResponse);
                System.out.println("jsonArray->>"+errorResponse);
                Toast.makeText(getApplicationContext(), R.string.error_load,
                        Toast.LENGTH_SHORT).show();
                startActivity(new Intent(mContext, StartActivity.class));
                finish();
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
    }



}

(3)DataVideoFetcher是通過使用android-async-http這個庫實現想服務器端發送post或get請求,關於android-async-http的使用,請參考我之前的博文https://blog.csdn.net/hil2000/article/details/13949513

package com.zyy360.app.core;

import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.JsonHttpResponseHandler;

/**
 * @author daimajia
 * @modified Foxhu
 * @version 1.0
 */
public class DataVideoFetcher {
    private static DataVideoFetcher mInstance;
    //request url with parameter page
    private static final String mRequestListUrl = "https://192.168.0.101:8080/action/api/videoList?page=%d";

    private DataVideoFetcher() {
    }
    public static DataVideoFetcher instance() {
        if (mInstance == null) {
            mInstance = new DataVideoFetcher();
        }
        return mInstance;
    }

    /**
     * get data from server by AsyncHttpClient
     * @param page
     * @param handler
     */
    public void getList(int page,JsonHttpResponseHandler handler){
        AsyncHttpClient client = new AsyncHttpClient();
        String request = String.format(mRequestListUrl,page);
        //get json data from server
        client.get(request,null,handler);
    }
}

而LoadActivity的 DataVideoFetcher.instance().getList()中的new JsonHttpResponseHandler()對onSuccess和onFailure進行瞭重寫.
(4)StartActivity獲得getIntent().hasExtra(“LoadData”)獲得傳遞來的數據

if (getIntent().hasExtra("LoadData")) {
            init(getIntent().getStringExtra("LoadData"));
        } else {
            init();
        }

其中init為初始化數據

public void init(String data) {
        try {
            JSONArray videoList = new JSONArray(data);
            if (videoList != null) {
                new AddToDBThread(videoList).start();
            }
            mVideoAdapter = VideoListAdapter.build(mContext, videoList, true);
            mVideoList.setAdapter(mVideoAdapter);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

其中mVideoAdapter = VideoListAdapter.build(mContext, videoList, true);基於JsonArray數據構建,我們看下VideoListAdapter的builde方法build(Context context, JSONArray data,Boolean checkIsWatched)

public static VideoListAdapter build(Context context, JSONArray data,
                                         Boolean checkIsWatched) throws JSONException {
        ArrayList videos = new ArrayList();
        for (int i = 0; i < data.length(); i++) {
            videos.add(VideoDataFormat.build(data.getJSONObject(i)));
        }
        return new VideoListAdapter(context, videos, checkIsWatched);
    }

其中videos.add(VideoDataFormat.build(data.getJSONObject(i)));通過VideoDataFormat的build方法解析JSONObjec對象,VideoDataFormat類如下

package com.zyy360.app.model;

import android.database.Cursor;

import org.json.JSONObject;
import org.json.JSONException;
import java.io.Serializable;

/**
 * @author Foxhu
 * @version 1.0
 */
public class VideoDataFormat implements Serializable {
    public  Integer id;
    public  String name;//視頻名稱
    public  String path;//視頻地址
    public  String video_pic;//視頻圖片
    public  String video_thumbpic; //視頻縮略圖
    public  String introduce;//視頻簡介
    public  String create_time;

    //縮略圖地址 https://192.168.0.101:8080/uploads/videopics/20131025/IMG_BjTA_25_s.jpg
    //大圖地址 https://192.168.0.101:8080/uploads/videopics/20131025/IMG_BjTA_25_b.jpg
    //視頻地址 https://192.168.0.101:8080/uploads/videofiles/2013/10/25_152747_61dP.flv
    private boolean IsWatched;
    private final String VideoUrlFormat = "https://192.168.0.101:8080/uploads/videofiles/%s";
    private final String PicUrlFormat = "https://192.168.0.101:8080/uploads/videopics/%s";


    public static final String NONE_VALUE = "-1";
    private VideoDataFormat(Integer id, String name, String path,String video_pic,
                            String video_thumbPic,String introduce,String create_time)
    {
        super();
        this.id = id;
        this.name = name;
        this.path = String.format(VideoUrlFormat, path);//根據原始地址構建完整url地址
        this.video_pic = String.format(PicUrlFormat, video_pic);
        this.video_thumbpic = String.format(PicUrlFormat, video_thumbPic);
        this.introduce = introduce;
        this.create_time = create_time;
    }

    private VideoDataFormat(JSONObject object){
        id = Integer.valueOf(getValue(object,"___key_id"));
        name = getValue(object, "name");
        path = String.format(VideoUrlFormat, getValue(object, "path"));
        video_pic = String.format(PicUrlFormat, getValue(object,"video_pic"));
        video_thumbpic = String.format(PicUrlFormat, getValue(object,"video_thumbpic"));
        introduce = getValue(object,"introduce");
        create_time = getValue(object,"create_time");
        IsWatched = false;
    }

    private static String getValue(JSONObject object, String key) {
        try {
            return object.getString(key);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return NONE_VALUE;
    }

    public boolean isWatched() {
        return IsWatched;
    }

    public void setWatched(Boolean watch) {
        IsWatched = watch;
    }

    public static VideoDataFormat build(JSONObject object) {
        return new VideoDataFormat(object);
    }
    public static VideoDataFormat build(Cursor cursor) {
        int id = cursor.getInt(cursor.getColumnIndex("id"));
        String name = cursor.getString(cursor.getColumnIndex("name"));
        String path = cursor.getString(cursor.getColumnIndex("path"));
        String video_pic = cursor.getString(cursor.getColumnIndex("video_pic"));
        String video_thumbPic = cursor.getString(cursor.getColumnIndex("video_thumbpic"));
        String introduce = cursor.getString(cursor.getColumnIndex("introduce"));
        String create_time = cursor.getString(cursor.getColumnIndex("create_time"));

        return new VideoDataFormat(id, name, path,video_pic,video_thumbPic,introduce,create_time);
    }
}

(5)VideoListAdapter中getView()方法

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        TextView titleTextView;
        TextView contentTextView;
        ImageView thumbImageView;
        ViewHolder holder;
        if (convertView == null) {
            convertView = mLayoutInflater.inflate(R.layout.video_item, parent,
                    false);
            titleTextView = (TextView) convertView.findViewById(R.id.title);
            contentTextView = (TextView) convertView.findViewById(R.id.content);
            thumbImageView = (ImageView) convertView.findViewById(R.id.thumb);
            holder = new ViewHolder(titleTextView, contentTextView,
                    thumbImageView);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
            titleTextView = holder.titleText;
            contentTextView = holder.contentText;
            thumbImageView = holder.thumbImageView;
        }
        VideoDataFormat video = (VideoDataFormat) getItem(position);
        Picasso.with(mContext).load(video.video_thumbpic)
                .placeholder(R.drawable.placeholder_thumb)
                .error(R.drawable.placeholder_fail).into(thumbImageView);
        titleTextView.setText(video.name);
        contentTextView.setText(video.introduce);
        convertView.setOnClickListener(new VideoListItemListener(mContext,
                this, video));
        convertView.setOnLongClickListener(new View.OnLongClickListener() {
            // 保證長按事件傳遞
            @Override
            public boolean onLongClick(View v) {
                return false;
            }
        });
        if (video.isWatched() == true) {
            titleTextView.setTextColor(mWatchedTitleColor);
        } else {
            titleTextView.setTextColor(mUnWatchedTitleColor);
        }
        return convertView;
    }

用於向視圖控件裝載數據,其中圖片數據的加載采用第三方圖片緩存庫Picasso(picasso是Square公司開源的一個Android圖形緩存庫,地址https://square.github.io/picasso/,可以實現圖片下載和緩存功能),並對view條目設置監聽convertView.setOnClickListener(new
VideoListItemListener(mContext,this, video));以便啟動播放界面PlayActivity
(6)VideoListItemListener單擊監聽類,當用戶單擊條目時啟動PlayActivity。

@Override
    public void onClick(View v) {
        Intent intent = new Intent(mContext, PlayActivity.class);
        intent.putExtra("VideoInfo", mData);
        mContext.startActivity(intent);
        mVideoDB.insertWatched(mData);
        if (mAdapter != null) {
            if (mData.isWatched() == false)
                mAdapter.setWatched(mData);
        }
    }

(7)PlayActivity類,該類主要是利用第三方視頻播放庫vitamio實現視頻播放,關於vitamio,請參考https://github.com/yixia/VitamioBundle,關於PlayActivity請參考筆者的github源碼。這裡涉及到Android項目如何引入第三方library project。Android Studio的項目由於采用Gradle構建,所以引入library project與Eclipse不同。主要步驟如下,這裡以vitamio為例:
①根目錄新建 libraries文件夾
②將vitamio拷貝到libraries文件夾
③修改settings.gradle

include ':app'
include(':libraries:vitamio')

④.修改app的build.gradle文件

dependencies {
    compile 'com.android.support:support-v4:19.0.+'
    compile 'com.android.support:appcompat-v7:+'
    compile fileTree(dir: 'libs', include: '*.jar')
    compile project(':libraries:vitamio')
}

以上修改完後記得Sync project with Gradle Files

4.服務器端API接口設計
服務器端接收用戶的http請求,通過ctx.param獲取參數,然後從數據庫查詢數據,利用google 的GSON庫,將list數據轉成JSONArray數據返回給客戶端

package com.cmsis.action;

import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import com.cmsis.beans.Video;
import com.google.gson.Gson;

/**
 * 
 * @author Foxhu
 *
 */
public class ApiAction extends BaseAction {
	private static final String homeIds = "order by id desc";
	/**
	 * 網站視頻客戶端api,返回數據格式為JsonArray
	 * @param ctx
	 * @throws IOException
	 */
	public void videoList(RequestContext ctx) throws IOException{
		int pageno = ctx.param("page", 1);//獲取手機客戶端請求頁碼
		pageno = pageno <= 0 ? 1 : pageno;
		List ids = Video.INSTANCE.IDs(homeIds);//從緩存中獲取加載數據id
		int size = ids.size();
		int beginIndex = (pageno - 1) * 10;//每頁記錄10條
		int toIndex = pageno * 10;
		List returnIds = ids.subList((beginIndex > size ? size : beginIndex), (toIndex > size ? size : toIndex));
		List

github源碼地址:https://github.com/puma007/Zyy360

發佈留言