Android自定義音視頻播放器

隨著快手空盼,抖音,西瓜視頻等視頻APP的崛起象迎,視頻播放已經(jīng)成為主流,此時(shí)做為Android研發(fā)的你呛踊,想要提升本身的能力還不知道怎么開(kāi)發(fā)視頻播放器怎么行砾淌?
視頻播放器有原生的VideoView、開(kāi)源的Ijkplayer?谭网、ExoPlayer汪厨、JieCaoVideoPlayer等等,這些部分的播放器我們?cè)陂_(kāi)發(fā)過(guò)程中蜻底,使用過(guò)這些視頻播放框架來(lái)播放本地視頻或者網(wǎng)絡(luò)視頻骄崩,但不一定會(huì)滿足業(yè)務(wù)需求的。因此薄辅,我們可以去自定義一個(gè)播放器要拂,在開(kāi)發(fā)Android應(yīng)用的過(guò)程中,難免需要自定義View站楚,其實(shí)自定義View不難脱惰,只要了解原理,實(shí)現(xiàn)起來(lái)就沒(méi)有那么難窿春,從而滿足業(yè)務(wù)需求拉一。
先看效果圖采盒。


player.gif

我自定義一個(gè)VideoPlayerView類(lèi),主要是由MediaPlayer與SurfaceView相結(jié)合組成一個(gè)視頻播放器蔚润,從Android原生的VideoView視頻框架磅氨,也是基于實(shí)現(xiàn)的。


1.png
2.png

1.首先編寫(xiě)布局文件layout_video_player.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
   <RelativeLayout
       android:layout_width="match_parent"
       android:layout_height="250dp"
       android:orientation="vertical"
       >
       <SurfaceView
           android:id="@+id/surfaceview"
           android:layout_width="match_parent"
           android:layout_height="match_parent"/>
       <LinearLayout
           android:layout_width="match_parent"
           android:layout_height="50dp"
           android:orientation="horizontal"
           android:layout_alignParentBottom="true"
           >
           <TextView
               android:id="@+id/tvCurrentTime"
               android:layout_width="0dp"
               android:layout_height="match_parent"
               android:layout_weight="0.1"
               android:gravity="center|right"
               android:text="00:00"
               android:textSize="12dp"
               android:textColor="#FFFFFFFF"
               />
           <SeekBar
               android:id="@+id/seekBar"
               android:layout_width="0dp"
               android:layout_height="10dp"
               android:layout_weight="0.9"
               android:progressDrawable="@drawable/seekbar_progress_drawable"
               android:maxHeight="2dp"
               android:minHeight="1dp"
               android:thumb="@drawable/round_bg"
               android:layout_gravity="center"
               />
           <TextView
               android:id="@+id/tvTotalTime"
               android:layout_width="0dp"
               android:layout_height="match_parent"
               android:layout_weight="0.1"
               android:gravity="center|left"
               android:text="00:00"
               android:textSize="12dp"
               android:textColor="#FFFFFFFF"
               />
       </LinearLayout>
       <Button
           android:id="@+id/player"
           android:layout_width="45dp"
           android:layout_height="45dp"
           android:background="@drawable/ic_play_24"
           android:layout_centerInParent="true"
           />
   </RelativeLayout>
</FrameLayout>

2.編寫(xiě)VideoPlayerView.java文件

public class VideoPlayerView extends FrameLayout implements View.OnClickListener {
   private static final String TAG = "VideoPlayerView";

   private SurfaceView mSurfaceView;

   private MediaPlayer mMediaPlayer = null;

   private int currentPosition = 0;

   private SurfaceHolder mSurfaceHolder;

   private boolean isPlaying;

   private SeekBar mSeekBar;

   private Button mPlyer;

   private String mUrl;

   private TextView tvCurrentTime, tvTotalTime;

   private Handler handler = new Handler(Looper.getMainLooper());

   public VideoPlayerView(Context context) {
       this(context, null);
   }

   public VideoPlayerView(Context context, AttributeSet attrs) {
       this(context, attrs, 0);
   }

   public VideoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
       init(context);
   }

   private void init(Context context) {
       initView(context);
       // 為SurfaceHolder添加回調(diào)
       mSurfaceHolder = mSurfaceView.getHolder();
       mSurfaceHolder.addCallback(callback);
       //設(shè)置Surface不維護(hù)自己的緩沖區(qū)嫡纠,而是等待屏幕的渲染引擎將內(nèi)容推送到界面
       mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
   }

   private void initView(Context context) {
       View view = LayoutInflater.from(context).inflate(R.layout.layout_video_player,null,false);
       mSurfaceView = view.findViewById(R.id.surfaceview);
       mSeekBar = view.findViewById(R.id.seekBar);
       mPlyer = view.findViewById(R.id.player);
       tvCurrentTime = view.findViewById(R.id.tvCurrentTime);
       tvTotalTime = view.findViewById(R.id.tvTotalTime);
       addView(view);
       mPlyer.setOnClickListener(this);
       mSeekBar.setOnSeekBarChangeListener(change);
   }

   private SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
       @Override
       public void surfaceCreated(@NonNull SurfaceHolder holder) {
           LogUtil.info(TAG, "surfaceCreated被創(chuàng)建");
           if (currentPosition > 0) {
               // 創(chuàng)建SurfaceHolder的時(shí)候烦租,如果存在上次播放的位置,則按照上次播放位置進(jìn)行播放
               play(currentPosition);
               currentPosition = 0;
           }
       }

       @Override
       public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
           LogUtil.info(TAG, "surfaceChanged大小改變");
       }

       @Override
       public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
           LogUtil.info(TAG, "surfaceDestroyed被銷(xiāo)毀");
           if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
               currentPosition = mMediaPlayer.getCurrentPosition();
               mMediaPlayer.stop();
               mMediaPlayer.release();
           }
       }
   };

   private void play(int currentPosition) {
       if (mMediaPlayer != null) {
           mMediaPlayer.seekTo(currentPosition);
       }
   }

   /**
    * 播放本地視頻
    * @param mUrl
    */
   public void setPlayerVideo(String mUrl) {
       this.mUrl = mUrl;
       LogUtil.info(TAG, "setPlayerVideo mUrl: " + mUrl);
       if (mUrl.contains("http")) {
           showRemoteVideo(0);
       } else {
           playerLocalView(0);
       }
   }

   // 播放本地視頻
   private void playerLocalView(final int msec) {
       LogUtil.info(TAG, " 獲取視頻文件地址");
       String path = mUrl;
       File file = new File(path);
       if (!file.exists()) {
           Toast.makeText(AppContextUtil.getContext(), "視頻文件路徑錯(cuò)誤", Toast.LENGTH_SHORT).show();
           return;
       }
       LogUtil.info(TAG, "指定視頻源路徑");
       try {
           mMediaPlayer = new MediaPlayer();
           mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
           // 設(shè)置播放的視頻源
           mMediaPlayer.setDataSource(file.getAbsolutePath());
           // 設(shè)置顯示視頻的SurfaceHolder
           mMediaPlayer.setDisplay(mSurfaceHolder);
           LogUtil.info(TAG, "開(kāi)始裝載");
           mMediaPlayer.prepareAsync();
           mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {

               @Override
               public void onPrepared(MediaPlayer mp) {
                   LogUtil.info(TAG, "裝載完成");
                   mMediaPlayer.start();
                   // 按照初始位置播放
                   mMediaPlayer.seekTo(msec);
                   // 設(shè)置進(jìn)度條的最大進(jìn)度為視頻流的最大播放時(shí)長(zhǎng)
                   mSeekBar.setMax(mMediaPlayer.getDuration());
                   tvCurrentTime.setText(DateUtil.getDate(msec, DateUtil.PARAMETER));
                   tvTotalTime.setText(DateUtil.getDate(mMediaPlayer.getDuration(), DateUtil.PARAMETER));
                   // 開(kāi)始線程除盏,更新進(jìn)度條的刻度
                   new Thread() {
                       @Override
                       public void run() {
                           try {
                               isPlaying = true;
                               while (isPlaying) {
                                   // 如果正在播放叉橱,沒(méi)0.5.毫秒更新一次進(jìn)度條
                                   int current = mMediaPlayer.getCurrentPosition();
                                   mSeekBar.setProgress(current);
                                   handler.post(new Runnable() {
                                       @Override
                                       public void run() {
                                           tvCurrentTime.setText(DateUtil.getDate(current, DateUtil.PARAMETER));
                                       }
                                   });

                                   sleep(500);
                               }
                           } catch (Exception e) {
                               e.printStackTrace();
                           }
                       }
                   }.start();

               }
           });
           mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {

               @Override
               public void onCompletion(MediaPlayer mp) {
                   // 在播放完畢被回調(diào)
                   mPlyer.setEnabled(true);
               }
           });

           mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {

               @Override
               public boolean onError(MediaPlayer mp, int what, int extra) {
                   // 發(fā)生錯(cuò)誤重新播放
                   play(0);
                   isPlaying = false;
                   return false;
               }
           });
       } catch (Exception e) {
           e.printStackTrace();
       }
   }

   // 播放遠(yuǎn)程視頻
   private void showRemoteVideo(final int msec) {
       String videoUrl2 = mUrl;
       Uri uri = Uri.parse(videoUrl2);
       try {
           mMediaPlayer = new MediaPlayer();
           mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
           // 設(shè)置播放的視頻源
           mMediaPlayer.setDataSource(AppContextUtil.getContext(), uri);
           // 設(shè)置顯示視頻的SurfaceHolder
           mMediaPlayer.setDisplay(mSurfaceHolder);
           LogUtil.info(TAG, "開(kāi)始裝載");
           mMediaPlayer.prepareAsync();
           mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
               @Override
               public void onPrepared(MediaPlayer mp) {
                   LogUtil.info(TAG, "裝載完成");
                   mMediaPlayer.start();
                   // 按照初始位置播放
                   mMediaPlayer.seekTo(msec);
                   // 設(shè)置進(jìn)度條的最大進(jìn)度為視頻流的最大播放時(shí)長(zhǎng)
                   mSeekBar.setMax(mMediaPlayer.getDuration());
                   tvCurrentTime.setText(DateUtil.getDate(msec, DateUtil.PARAMETER));
                   tvTotalTime.setText(DateUtil.getDate(mMediaPlayer.getDuration(), DateUtil.PARAMETER));
                   // 開(kāi)始線程,更新進(jìn)度條的刻度
                   new Thread() {
                       @Override
                       public void run() {
                           try {
                               isPlaying = true;
                               while (isPlaying) {
                                   // 如果正在播放者蠕,沒(méi)0.5.毫秒更新一次進(jìn)度條
                                   int current = mMediaPlayer.getCurrentPosition();
                                   mSeekBar.setProgress(current);
                                   handler.post(new Runnable() {
                                       @Override
                                       public void run() {
                                           tvCurrentTime.setText(DateUtil.getDate(current, DateUtil.PARAMETER));
                                       }
                                   });

                                   sleep(500);
                               }
                           } catch (Exception e) {
                               e.printStackTrace();
                           }
                       }
                   }.start();
               }
           });
           mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {

               @Override
               public void onCompletion(MediaPlayer mp) {
                   mp.release();
                   mp = null;
                   // 在播放完畢被回調(diào)
                   mPlyer.setEnabled(true);
               }
           });

           mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {

               @Override
               public boolean onError(MediaPlayer mp, int what, int extra) {
                   // 發(fā)生錯(cuò)誤重新播放
                   play(0);
                   isPlaying = false;
                   return false;
               }
           });
       } catch (Exception e) {
           e.printStackTrace();
       }
   }

   private SeekBar.OnSeekBarChangeListener change = new SeekBar.OnSeekBarChangeListener() {

       @Override
       public void onStopTrackingTouch(SeekBar seekBar) {
           // 當(dāng)進(jìn)度條停止修改的時(shí)候觸發(fā)
           // 取得當(dāng)前進(jìn)度條的刻度
           int progress = seekBar.getProgress();
//            if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
               // 設(shè)置當(dāng)前播放的位置
               mMediaPlayer.seekTo(progress);
               tvCurrentTime.setText(DateUtil.getDate(progress, DateUtil.PARAMETER));
//            }
       }

       @Override
       public void onStartTrackingTouch(SeekBar seekBar) {

       }

       @Override
       public void onProgressChanged(SeekBar seekBar, int progress,
                                     boolean fromUser) {

       }
   };

   @Override
   public void onClick(View v) {
       if (v.getId() == R.id.player) {
           if (mMediaPlayer == null) {
               return;
           }
           if (mMediaPlayer.isPlaying()) {
               mMediaPlayer.pause();
               mPlyer.setBackgroundResource(R.drawable.ic_play_24);
           } else {
               mMediaPlayer.start();
               mPlyer.setBackgroundResource(R.drawable.ic_pause_24);
           }
       }
   }

   public void pause () {
       if (mMediaPlayer == null) {
           return;
       }
       if (mMediaPlayer.isPlaying()) {
           mMediaPlayer.pause();
           mPlyer.setBackgroundResource(R.drawable.ic_pause_24);
       }
   }

   public void destroy() {
       if (mMediaPlayer != null) {
           mMediaPlayer.stop();
           mMediaPlayer.reset();
           mMediaPlayer.release();
           mMediaPlayer = null;
       }
       isPlaying = false;
       LogUtil.info(TAG, "MediaPlayer is release");
   }
}

該類(lèi)主要做了如下工作:
1).初始化控件窃祝;
2).SurfaceHolder回調(diào)監(jiān)聽(tīng);
3).播放本地視頻踱侣;
4).播放網(wǎng)絡(luò)視頻粪小;
5).常用SeekBar顯示進(jìn)度及改變進(jìn)度;
6).額外工作泻仙,播放糕再、暫停、總時(shí)長(zhǎng)玉转、當(dāng)前時(shí)長(zhǎng)的View控件突想。

3.編寫(xiě)Activity的布局文件activity_player_video.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/player_layout"
    />

從上面可以看出,里面沒(méi)有任何子view究抓,僅僅聲明FrameLayout和id即可猾担。

4.PlayerVideoActivity.java

public class PlayerVideoActivity extends Activity {
    private FrameLayout playerLayout;

    private VideoPlayerView videoPlayerView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_player_video);
        initPlayer();
    }

    private void initPlayer() {
        String localPath = Environment.getExternalStorageDirectory() +"/xlk-player.mp4";
        playerLayout = findViewById(R.id.player_layout);
        videoPlayerView = new VideoPlayerView(this);
        playerLayout.addView(videoPlayerView);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                //這里傳入本地視頻的路徑或者網(wǎng)絡(luò)視頻的url
                videoPlayerView.setPlayerVideo(localPath);
            }
        },500);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (videoPlayerView != null) {
            videoPlayerView.pause();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (videoPlayerView != null) {
            videoPlayerView.destroy();
            videoPlayerView = null;
        }
    }
}

上面實(shí)例化一個(gè)VideoPlayerView,然后通過(guò)VideoPlayerView對(duì)象來(lái)調(diào)用setPlayerVideo的方法傳入本地視頻的路徑或者網(wǎng)絡(luò)視頻的url,實(shí)現(xiàn)音視頻播放器刺下,運(yùn)行此項(xiàng)目绑嘹,效果如開(kāi)頭顯示的一幕。
自定義View播放器已經(jīng)實(shí)現(xiàn)了橘茉,但我們需要掌握MediaPlayer的初始化過(guò)程是怎么樣的工腋,MediaPlayer做了哪些工作,音視頻數(shù)據(jù)如何渲染到SurfaceView上面的畅卓,這需要把MediaPlayer框架原理搞清楚擅腰。就像下面的。


3.png

每個(gè)方法調(diào)用過(guò)程是怎么樣子的翁潘,做了哪些工作趁冈,而不是簡(jiǎn)單寫(xiě)幾行代碼就可以實(shí)現(xiàn)播放視頻或者音頻。
小伙伴們有興趣的話,搜索并關(guān)注公眾號(hào)“Android技術(shù)迷”關(guān)注后可閱讀更多文章渗勘,感謝各位關(guān)注沐绒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市旺坠,隨后出現(xiàn)的幾起案子乔遮,更是在濱河造成了極大的恐慌,老刑警劉巖价淌,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件申眼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蝉衣,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)巷蚪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)病毡,“玉大人,你說(shuō)我怎么就攤上這事屁柏±材ぃ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵淌喻,是天一觀的道長(zhǎng)僧家。 經(jīng)常有香客問(wèn)我,道長(zhǎng)裸删,這世上最難降的妖魔是什么八拱? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮涯塔,結(jié)果婚禮上肌稻,老公的妹妹穿的比我還像新娘。我一直安慰自己匕荸,他們只是感情好爹谭,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著榛搔,像睡著了一般诺凡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上践惑,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天腹泌,我揣著相機(jī)與錄音,去河邊找鬼童本。 笑死真屯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的穷娱。 我是一名探鬼主播绑蔫,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼运沦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了配深?” 一聲冷哼從身側(cè)響起携添,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎篓叶,沒(méi)想到半個(gè)月后烈掠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缸托,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年左敌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片俐镐。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡矫限,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出佩抹,到底是詐尸還是另有隱情叼风,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布棍苹,位于F島的核電站无宿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏枢里。R本人自食惡果不足惜孽鸡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坡垫。 院中可真熱鬧梭灿,春花似錦、人聲如沸冰悠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)溉卓。三九已至皮迟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間桑寨,已是汗流浹背伏尼。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留尉尾,地道東北人爆阶。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親辨图。 傳聞我的和親對(duì)象是個(gè)殘疾皇子班套,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容