1. 程式人生 > >android 視訊+音訊播放器Demo

android 視訊+音訊播放器Demo

程式主介面

  • MainActivity.java

    1.主介面,頭部是兩個TextView(自定義類似指標效果),底部是ViewPager。ViewPager中每個頁面對應的是一個Fragment.這樣就搭起了首頁。

    xml檔案程式碼:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@mipmap/base_bg"
    tools:context=".activity.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="55dp"
        android:orientation="vertical"
        android:background="@mipmap/base_titlebar_bg"
        >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:orientation="horizontal"
            android:gravity="center_vertical"
            >

            <TextView
                android:id="@+id/tv_audio"
                style="@style/mainActivity_indicator"
                android:text="@string/audio"
            />

            <TextView
                android:id="@+id/music"
                style="@style/mainActivity_indicator"
                android:text="@string/music" />

        </LinearLayout>

        <View
            android:id="@+id/indicator"
            android:layout_width="30dp"
            android:layout_height="3dp"
            android:background="@color/green"
            />

    </LinearLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>
  2.當選中音訊或者視訊時,TextView的文字顏色和大小都改變。
/**
     * 選擇:音訊 / 音樂
     * @param isAudio 是選擇了音訊
     */
    public void changeSelectedIndicator(boolean isAudio){

        mAudio.setSelected(isAudio);
        mMusic.setSelected(!isAudio);

        float ascale = isAudio ? 1.2f : 1.0f;
        float mscale = isAudio ? 1.0f : 1.2f;


        ViewPropertyAnimator.animate(mAudio).scaleX(ascale);
        ViewPropertyAnimator.animate(mAudio).scaleY(ascale);

        ViewPropertyAnimator.animate(mMusic).scaleX(mscale);
        ViewPropertyAnimator.animate(mMusic).scaleY(mscale);
    }
 3.滑動指示先與ViewPager的結合。
<span style="color:#000000;">/**
     * 滑動指示線
     * @param position
     * @param positionOffsetPixels
     */
    protected void scrollIndicator(int position, int positionOffsetPixels) {
        int translationX = mIndicatorWidth * position + positionOffsetPixels / pageSize ;
        LogUtil.i("qd","translationX =="+translationX );
        ViewHelper.setTranslationX(mIndicator, translationX);

    }
 
</span><pre name="code" class="java"><span style="color:#000000;"> mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                LogUtil.i("qd", "position===" + position + "  positionOffset=" + positionOffset + "  positionOffsetPixels=" + positionOffsetPixels);
                scrollIndicator(position, positionOffsetPixels);
            }

            @Override
            public void onPageSelected(int position) {
                changeSelectedIndicator(position == 0);
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });</span>

4,給ViewPager設定FragmentStatePagerAdapter,他的構造需要FragmentManager,所以MainActivity需要繼承自FragmentActivity
 fragmentAdapter.addItem(new VideoFragment());
 fragmentAdapter.addItem(new MusicFragment());
  • VideoFragment.java(視訊模組)
    新建了BaseFragment這樣一個基類。設計思想,1.視訊和音訊的列表頁面其實大致相同,只是顯示的具體資料不同,所以可以他他們共同的東西抽取出來成為一個
  public abstract class BaseFragment<T> extends Fragment implements BaseInterface
    2.每個類都會涉及到,初始化view,初始化data,設定監聽等。所以,把這三個方法涉及成一個Interface,由每個需要的類來進行實現。

    3.每個Fragment都會執行onCreateView,只是他們的佈局不同,抽取出getLayoutId(),返回每個子類具體的佈局。並且在onViewCreated方法中,呼叫initView,initData.

      這樣子類只需要實現這幾個方法即可。

   4.如何查詢手機當中的視訊? 音訊資源???

     android將這些的資訊都封裝進了本地資料庫中,我們只需要按照指定的格式查詢資料庫即可。

     android 提供了非同步查詢資料庫的一個類,AsyncQueryHandler,他使用了內容觀察者模式,每次資料庫發生變動時,他返回的Cursor都會變化。

<span style="font-size:18px;"> AsyncQueryHandler task = new AsyncQueryHandler(getActivity().getContentResolver()) {
                @Override
                protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
                    super.onQueryComplete(token, cookie, cursor);
                    if(cursor != null){
                        mListView.setAdapter(getAdapter(getActivity(), cursor));
                    }else{
                        LogUtil.i("qd","audioFragment cursor == null");
                    }
                }
            };
task.startQuery(0,null, getUri(), getProjection(),null,null,getOrderBy());
</span>
  音訊和視訊的區別就在於這些引數上
<span style="font-size:18px;"> task.startQuery(0,null, getUri(), getProjection(),null,null,getOrderBy());</span>
  所以,我們抽取成abstract方法,讓具體的子類來實現。

  音訊的地址:

<span style="font-size:18px;">MediaStore.Audio.Media.EXTERNAL_CONTENT_URI</span>
  視訊的地址:  
<span style="font-size:18px;">MediaStore.Video.Media.EXTERNAL_CONTENT_URI</span>
 5.查詢成功後,會返回一個cursor,其中包括了資料,將資料設定到adapter中,而且這個adapter需要繼承自CursorAdapter
<span style="font-size:18px;">public class VideoAdapter extends CursorAdapter {
    public VideoAdapter(Context context, Cursor c) {
        super(context, c);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        View view = LayoutInflater.from(context).inflate(R.layout.video_list_item, null);
        ViewHolder holder = new ViewHolder();
        holder.title = (TextView) view.findViewById(R.id.title);
        holder.duration = (TextView) view.findViewById(R.id.duration);
        holder.size = (TextView) view.findViewById(R.id.size);
        view.setTag(holder);
        return view;
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        ViewHolder holder = (ViewHolder) view.getTag();
        VideoItem videoItem = VideoItem.fromCursor(cursor);
        holder.title.setText(videoItem.getTitle());
        holder.duration.setText(TimeUtil.formatLong(videoItem.getDuration()));
        holder.size.setText(Formatter.formatFileSize(context, videoItem.getSize()));
    }

    class ViewHolder{
        public TextView title;
        public TextView duration;
        public TextView size;
    }
}
</span>
 6.最後設定當點選條目的時候,頁面進行跳轉。將條目的位置和查詢到的所有資料全都傳遞過去。
  •  音訊播放頁面

    利用了android中的VideoView來進行播放視訊。

    主要涉及到了一下幾個方法。 

<span style="font-size:18px;">//1.設定播放路徑
mVideoView.setVideoPath(item.getPath());
</span>
<span style="font-size:18px;">//2.視訊準備好的監聽
mVideoView.setOnPreparedListener(preparedListener);
</span><pre name="code" class="java"><span style="font-size:18px;">MediaPlayer.OnPreparedListener preparedListener = new MediaPlayer.OnPreparedListener(){

        @Override
        public void onPrepared(MediaPlayer mp) {
            //3.切記需要,先啟動,因為接下來需要獲取他播放的位置,否在的話會報異常。
            mVideoView.start();
            if(item != null){
                mTitle.setText(item.getTitle());
            }

            mDuration.setText(TimeUtil.formatLong(mVideoView.getDuration()));
            mSeekBarVideo.setMax((int) </span><pre name="code" class="java"><span style="font-size:18px;">mVideoView.getDuration()</span>

); updateVideoCurrentPosition(); LogUtil.i("qd", "preparedListener selectPosition===" + selectPosition); loading.setVisibility(View.GONE); } };
4.獲取視訊的播放時長和當前時長
<span style="font-size:18px;">mVideoView.getDuration()</span>
<span style="font-size:18px;">mVideoView.getCurrentPosition()</span>
5.設定視訊播放前的監聽
<span style="font-size:18px;">//緩衝視訊前的準備
mVideoView.setOnInfoListener(onInfoListener);
</span><pre name="code" class="java"><span style="font-size:18px;">MediaPlayer.OnInfoListener onInfoListener = new MediaPlayer.OnInfoListener(){

        @Override
        public boolean onInfo(MediaPlayer mp, int what, int extra) {
            switch (what){
                case MediaPlayer.MEDIA_INFO_BUFFERING_START:  //緩衝開始,設定載入頁面可見
                    loading.setVisibility(View.VISIBLE);
                    break;
                case MediaPlayer.MEDIA_INFO_BUFFERING_END: //緩衝結束,設定載入頁面不可見
                    loading.setVisibility(View.GONE);
                    break;
            }
            return false;
        }
    };</span>
6.設定視訊快取(針對網路視訊,這個方法是用來更新第二進度條的)
<span style="font-size:18px;"> //視訊緩衝  ----本地視訊不存在緩衝一說
mVideoView.setOnBufferingUpdateListener(onBufferingUpdateListener);
</span><pre name="code" class="java"><span style="font-size:18px;">MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener(){

        @Override
        public void onBufferingUpdate(MediaPlayer mp, int percent) {
            float f = (float)percent / 100;
            int secondaryProgress = (int) (f * mVideoView.getDuration());
            mSeekBarVideo.setSecondaryProgress(secondaryProgress);
        }
    };</span>
7.設定視訊播放完的監聽
<span style="font-size:18px;"> //視訊播放完的監聽
 mVideoView.setOnCompletionListener(completionListener);
</span><pre name="code" class="java"><span style="font-size:18px;"> MediaPlayer.OnCompletionListener completionListener = new MediaPlayer.OnCompletionListener(){
        @Override
        public void onCompletion(MediaPlayer mp) {
            mhandler.removeMessages(UPDATE_VIDEO_CURRENT_POSITION);
        }
    };</span>
8.當對螢幕進行手勢識別的時候,用GestureDetector,在onTouch事件時,交給GestureDetector的onTouchEvent來處理。
  • 萬能視訊播放器
   主要用開源的Vitamio來實現。

    1.首先,在清單檔案中加入

<span style="font-size:18px;"> <activity
            android:name="io.vov.vitamio.activity.InitActivity"
            android:configChanges="orientation|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
            android:launchMode="singleTop"
            android:theme="@android:style/Theme.NoTitleBar"
            android:windowSoftInputMode="stateAlwaysHidden" />

  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</span>
  2.在VideoPlayerActivity.java(視訊播放頁面)初始化資料時,加入
<span style="font-size:18px;">  // 初始化Vitamio SDK
        if (!LibsChecker.checkVitamioLibs(this))
            return;</span>
 3.替換VideoView,成Vitamio的中的VideoView,在個類的路徑
<span style="font-size:18px;">io.vov.vitamio.widget.VideoView</span>
這樣就可以播放基本上所有型別的視訊檔案了,android預設只支援mp4格式的視訊。
  • 音訊模組(AudioPlayerActivity.java  和 AudioPlayerService.java) 播放音樂需要服務。

   Activity與Service如何互動?

   1.使用AIDL

   2.使用Messenger   這裡主要講解下次方式。

   有點像,TCP/IP通訊,需要3此握手,才能建立連線、

   第一步:

<span style="font-size:18px;">//開啟服務
 startService(service);
//繫結服務,這樣才能與其互動
bindService(service, conn, BIND_AUTO_CREATE);
</span>
 第二步:
<span style="font-size:18px;"> //上面的開啟服務方式,首先呼叫onStartCommand,然後呼叫onBind()
 @Override
 public IBinder onBind(Intent intent) {
       return serviceMessenger.getBinder();
 }
 //在service中創建出一個信使,用來與Activity通訊。</span><pre name="code" class="java"><span style="font-size:18px;">private Messenger serviceMessenger = new Messenger(mHandler);</span>
//建立handler,用來處理此信使獲取到的資訊
<span style="font-size:18px;">private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
        }
    };</span>
第三步:
<span style="font-size:18px;">//在Activty中,當service與Activty連線上時會回撥此方法。 
ServiceConnection conn = new ServiceConnection(){

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //獲取service傳遞過來的信使
            Messenger serviceMessenger = new Messenger(service);
            //建立一條訊息
            Message message = Message.obtain(null, AudioPlayService.WHAT_UI_INTEFACE);
            //攜帶UI類過去
            message.obj = AudioPlayerActivity.this;
            //告訴service,此UI的信使是哪個(這樣,service就能拿到ui的信使,並用此信使,傳送訊息)
            message.replyTo = uiMessenger;
            //用service的信使,給service傳送訊息
            try {
                serviceMessenger.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };</span>
第四步:
<span style="font-size:18px;">//service中,處理自己的信使收到的訊息
private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case WHAT_UI_INTEFACE:
                    //拿到,傳遞過來的UI物件
                    audioUI = (AudioUI) msg.obj;
                    //獲取ui物件的信使
                    Messenger uiMessenger = msg.replyTo;
                    //建立訊息
                    Message message = Message.obtain(null, WHAT_PLAYSERVICE_INTERFACCE);
                    //把service傳遞過去
                    message.obj = AudioPlayService.this;
                    //
                    message.arg1 = flag ;
                    //用UI類的信使,傳送訊息
                    try {
                        uiMessenger.send(message);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
            }
            super.handleMessage(msg);
        }
    };
</span>
第五步:

service中拿到了Activity中的信使,所以用此信使傳遞訊息。

<span style="font-size:18px;">//這樣在Ativity中又收到了service的訊息,這樣即確定了Service與Activity建立了可靠的連線。
private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case AudioPlayService.WHAT_PLAYSERVICE_INTERFACCE:
                    //Activity 進行了三次握手,建立了連線
                    audioPlayService = (IPlayAudio) msg.obj;

                    if(msg.arg1 == -1){
                        //開啟音訊
                        audioPlayService.openAudio();
                    }else{
                        //從通知欄,點選進來額,機選重新整理activity並不做其他處理
                        updateUI(audioPlayService.getCurrentMusicItem());
                    }

                    break;
                case UPDATE_PLAY_TIME:
                    updatePlayTime();
                    break;
                case UPDATE_LYRICS:
                    updateLyrics();
                    break;
            }
            super.handleMessage(msg);
        }
    };</span>

這樣就可以在service,activity中拿上對方的引用,來操作對方的方法,

視訊  / 音訊效果:


我開發中遇到的坑:

1.音訊,視訊的開發過程,基本不怎麼報錯,直接奔潰,要麼是ANR,所以一定要細心,在加上Debug進行除錯

2.控制檯報錯:client not yet。。。
        Connected to the target VM, address: 'localhost:8603', transport: 'socket'
解決方案:其實還是模稜兩可,碰出來的。
      重新設定斷點,然後在進行除錯。
      注意檢視執行時是否是app(有時候是Activity)

3.studio開發當涉及到.so檔案的正確匯入方式

在gradle檔案中加入下面這句話:
 sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

4.int型別的兩個數,想出得到一個float型別的數值時,需要將除數,或者被除數轉換為

float型別,否在得到的除數將== 0;

5.service生命週期
1.startService --> oncreate  ---> onStartCommand  --->onDestroy(必須呼叫stopService!!!!!此服務才能結束,否則再次開啟應用的時候,可能出現ANR)

2.bindService -->  onCreate ---> onBind  -- > unUnBind() --> onDestroy
  在activity銷燬的時候,必須顯示的呼叫unBindServce來接觸繫結,銷燬service

3.混合啟動service   startService --> bindService
  ---> 在activity銷燬的時候,必須呼叫stopService()來停止服務,首先會呼叫onUnBind,在呼叫onDestroy
  ---> 只調用onUnbind()並不會關閉服務。 startService,必須stopService才能關閉掉
  ---> 如果unBindService,stopService都呼叫的話onUnbind ,onDestroy會按順序呼叫一次。

我在開發音樂播放器的時候,在activity銷燬的時候沒有關閉service,下次再啟動應用的時候出現白屏(也就是應用不能開啟,最後會報ANR)

下載地址:

https://github.com/QDqiaodong/VideoAndAudio