封裝bilibili播放器,自定義邊下邊播和緩存功能

源碼下載,歡迎starjava

演示Demo下載android

image

本項目使用播放器是ijkplay, 而且進行封裝和修改

主要功能:
1.從新編輯ijkplay的so庫, 使其更精簡和支持https協議
2.自定義MediaDataSource, 使用okhttp重寫網絡框架, 網絡播放更流暢
3.實現視頻緩存, 而且自定義LRUCache算法管理緩存文件
4.全局使用一個播放器, 實現視頻在多個Activity以前無縫切換, 流暢播放
5.加入更多兼容性判斷, 適配絕大數機型
複製代碼

①導入ijkplay:

image

//須要的權限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

首先將lib文件夾下的so庫粘貼過來, (由於官方自帶的so庫是不支持https的, 我從新編譯的這個so庫支持https協議, 
而且使用的是精簡版的配置, 網上關於ijkplay編譯的流程和配置挺多的, 能夠根據本身的需求自定義)

而後在module的build中加入 "implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'"
複製代碼

②使用播放器的方法:

1.我封裝了一個MediaPlayerTool工具類包含的初始化so庫和一些回調等等git

//經過單例獲得媒體播放工具
mMediaPlayerTool = MediaPlayerTool.getInstance();
//這裏會自動初始化so庫 有些手機會找不到so, 會自動使用系統的播放器
private MediaPlayerTool(){
       try {
           IjkMediaPlayer.loadLibrariesOnce(null);
           IjkMediaPlayer.native_profileBegin("libijkplayer.so");
           loadIjkSucc = true;
       }catch (UnsatisfiedLinkError e){
           e.printStackTrace();
           loadIjkSucc = false;
       }
}
   
//一些生命週期回調
public static abstract class VideoListener {
       //視頻開始播放
       public void onStart(){};
       //視頻被中止播放
       public void onStop(){};
       //視頻播放完成
       public void onCompletion(){};
       //視頻旋轉角度參數初始化完成
       public void onRotationInfo(int rotation){};
       //播放進度 0-1
       public void onPlayProgress(long currentPosition){};
       //緩存速度 1-100
       public void onBufferProgress(int progress){};
   }
複製代碼

2.由於我使用的是RecyclerView,因此先找到當前屏幕中 處於能夠播放範圍的itemgithub

//首先循環RecyclerView中全部itemView, 找到在屏幕可見範圍內的item
    private void checkPlayVideo(){
        currentPlayIndex = 0;
        videoPositionList.clear();

        int childCount = rv_video.getChildCount();
        for (int x = 0; x < childCount; x++) {
            View childView = rv_video.getChildAt(x);
            //isPlayRange()這個方法很重要
            boolean playRange = isPlayRange(childView.findViewById(R.id.rl_video), rv_video);
            if(playRange){
                int position = rv_video.getChildAdapterPosition(childView);
                if(position>=0 && !videoPositionList.contains(position)){
                    videoPositionList.add(position);
                }
            }
        }
    }
    
    //檢查當前item是否在RecyclerView可見的範圍內
    private boolean isPlayRange(View childView, View parentView){

        if(childView==null || parentView==null){
            return false;
        }

        int[] childLocal = new int[2];
        childView.getLocationOnScreen(childLocal);

        int[] parentLocal = new int[2];
        parentView.getLocationOnScreen(parentLocal);

        boolean playRange = childLocal[1]>=parentLocal[1] &&
                childLocal[1]<=parentLocal[1]+parentView.getHeight()-childView.getHeight();

        return playRange;
    }
複製代碼

3.我還封裝了一個TextureView, 裏面包含一些初始化SurfaceTexture和視頻裁剪播放的方法算法

//視頻居中播放
    private void setVideoCenter(float viewWidth, float viewHeight, float videoWidth, float videoHeight){

        Matrix matrix = new Matrix();
        float sx = viewWidth/videoWidth;
        float sy = viewHeight/videoHeight;
        float maxScale = Math.max(sx, sy);

        matrix.preTranslate((viewWidth - videoWidth) / 2, (viewHeight - videoHeight) / 2);
        matrix.preScale(videoWidth/viewWidth, videoHeight/viewHeight);
        matrix.postScale(maxScale, maxScale, viewWidth/2, viewHeight/2);

        mTextureView.setTransform(matrix);
        mTextureView.postInvalidate();
    }

    //初始化SurfaceTexture
    public SurfaceTexture newSurfaceTexture(){

        int[] textures = new int[1];
        GLES20.glGenTextures(1, textures, 0);
        int texName = textures[0];
        SurfaceTexture surfaceTexture = new SurfaceTexture(texName);
        surfaceTexture.detachFromGLContext();
        return surfaceTexture;
    }
複製代碼

4.接下來就是播放代碼了數據庫

private void playVideoByPosition(int position){
       //根據傳進來的position找到對應的ViewHolder
       final MainAdapter.MyViewHolder vh = (MainAdapter.MyViewHolder)       
       rv_video.findViewHolderForAdapterPosition(position);
       if(vh == null){
           return ;
       }

       currentPlayView = vh.rl_video;

       //初始化一些播放狀態, 如進度條,播放按鈕,加載框等
       //顯示正在加載的界面
       vh.iv_play_icon.setVisibility(View.GONE);
       vh.pb_video.setVisibility(View.VISIBLE);
       vh.iv_cover.setVisibility(View.VISIBLE);
       vh.tv_play_time.setText("");

       //初始化播放器
       mMediaPlayerTool.initMediaPLayer();
       mMediaPlayerTool.setVolume(0);

       //設置視頻url
       String videoUrl = dataList.get(position).getVideoUrl();
       mMediaPlayerTool.setDataSource(videoUrl);

       myVideoListener = new MediaPlayerTool.VideoListener() {
           @Override
           public void onStart() {
               //將播放圖標和封面隱藏
               vh.iv_play_icon.setVisibility(View.GONE);
               vh.pb_video.setVisibility(View.GONE);
               //防止閃屏
               vh.iv_cover.postDelayed(new Runnable() {
                   @Override
                   public void run() {
                       vh.iv_cover.setVisibility(View.GONE);
                   }
               }, 300);
           }
           @Override
           public void onStop() {
               //播放中止
               vh.pb_video.setVisibility(View.GONE);
               vh.iv_cover.setVisibility(View.VISIBLE);
               vh.iv_play_icon.setVisibility(View.VISIBLE);
               vh.tv_play_time.setText("");
               currentPlayView = null;
           }
           @Override
           public void onCompletion() {
               //播放下一個
               currentPlayIndex++;
               playVideoByPosition(-1);
           }
           @Override
           public void onRotationInfo(int rotation) {
               //設置旋轉播放
               vh.playTextureView.setRotation(rotation);
           }
           @Override
           public void onPlayProgress(long currentPosition) {
               //顯示播放時長
               String date = MyUtil.fromMMss(mMediaPlayerTool.getDuration() - currentPosition);
               vh.tv_play_time.setText(date);
           }
       };
       mMediaPlayerTool.setVideoListener(myVideoListener);

       //這裏重置一下TextureView
       vh.playTextureView.resetTextureView();
       mMediaPlayerTool.setPlayTextureView(vh.playTextureView);
       mMediaPlayerTool.setSurfaceTexture(vh.playTextureView.getSurfaceTexture());
       //準備播放
       mMediaPlayerTool.prepare();
   }
複製代碼

③重寫MediaDataSource, 使用okhttp實現邊下邊播和視頻緩存

1.一共須要重寫3個方法getSize(),close()和readAt(); 先說getSize()數組

public long getSize() throws IOException {
        //開始播放時, 播放器會調用一下getSize()來初始化視頻大小, 這時咱們就要初始化一條視頻播放流
        if(networkInPutStream == null) {
            initInputStream();
        }
        return contentLength;
    }

    //初始化一個視頻流出來, 多是本地或網絡
    private void initInputStream() throws IOException{

        File file = checkCache(mMd5);
        if(file != null){
            //更新一下緩存文件
            VideoLRUCacheUtil.updateVideoCacheBean(mMd5, file.getAbsolutePath(), file.length());
            //讀取的本地緩存文件
            isCacheVideo = true;
            localVideoFile = file;
            //開啓一個本地視頻流
            localStream = new RandomAccessFile(localVideoFile, "rw");
            contentLength = file.length();
        }else {
            //沒有緩存 開啓一個網絡流, 而且開啓一個緩存流, 實現視頻緩存
            isCacheVideo = false;
            //開啓一個網絡視頻流
            networkInPutStream = openHttpClient(0);
            //要寫入的本地緩存文件
            localVideoFile = VideoLRUCacheUtil.createCacheFile(MyApplication.mContext, mMd5, contentLength);
            //要寫入的本地緩存視頻流
            localStream = new RandomAccessFile(localVideoFile, "rw");
        }
    }
複製代碼

2.而後是readAt()方法, 也是最重要的一個方法緩存

/**
     * @param position 視頻流讀取進度
     * @param buffer 要把讀取到的數據存到這個數組
     * @param offset 數據開始寫入的座標
     * @param size 本次一共讀取數據的大小
     * @throws IOException
     */
    //記錄當前讀取流的索引
    long mPosition = 0;
    @Override
    public int readAt(long position, byte[] buffer, int offset, int size) throws IOException {

        if(position>=contentLength || localStream==null){
            return -1;
        }

        //是否將此字節緩存到本地
        boolean isWriteVideo = syncInputStream(position);

        //讀取的流的長度不能大於contentLength
        if (position+size > contentLength) {
            size -= position+size-contentLength;
        }

        //讀取指定大小的視頻數據
        byte[] bytes;
        if(isCacheVideo){
            //從本地讀取
            bytes = readByteBySize(localStream, size);
        }else{
            //從網絡讀取
            bytes = readByteBySize(networkInPutStream, size);
        }
        if(bytes != null) {
            //寫入到播放器的數組中
            System.arraycopy(bytes, 0, buffer, offset, size);
            if (isWriteVideo && !isCacheVideo) {
                //將視頻緩存到本地
                localStream.write(bytes);
            }
            //記錄數據流讀取到哪步了
            mPosition += size;
        }

        return size;
    }

     /**
     * 從inputStream裏讀取size大小的數據
     */
    private byte[] readByteBySize(InputStream inputStream, int size) throws IOException{

        ByteArrayOutputStream out = new ByteArrayOutputStream();

        byte[] buf = new byte[size];
        int len;
        while ((len = inputStream.read(buf)) != -1) {
            out.write(buf, 0, len);
            if (out.size() == size) {
                return out.toByteArray();
            } else {
                buf = new byte[size - out.size()];
            }
        }
        return null;
    }

     /**
     * 刪除file一部分字節, 從position到file.size
     */
    private void deleteFileByPosition(long position) throws IOException{

        FileInputStream in = new FileInputStream(localVideoFile);

        File tempFile = VideoLRUCacheUtil.createTempFile(MyApplication.mContext);
        FileOutputStream out = new FileOutputStream(tempFile);

        byte[] buf = new byte[8192];
        int len;
        while ((len = in.read(buf)) != -1) {
            if(position <= len){
                out.write(buf, 0, (int) position);
                out.close();

                in.close();
                localVideoFile.delete();
                tempFile.renameTo(localVideoFile);
                localStream = new RandomAccessFile(localVideoFile, "rw");
                return ;
            }else{
                position -= len;
                out.write(buf, 0, len);
            }
        }
        tempFile.delete();
    }
複製代碼

3.主要說一下syncInputStream(), 由於有可能出現一種狀況, 好比一個視頻長度100, 播放器首先讀取視頻的1到10之間的數據, 而後在讀取90到100之間的數據, 而後在從1播放到100; 因此這時咱們須要同步視頻流, 和播放進度保持一致這時就須要從新開啓一個IO流(若是在讀取本地緩存時能夠直接使用RandomAccessFile.seek()方法跳轉)bash

//同步數據流
    private boolean syncInputStream(long position) throws IOException{
        boolean isWriteVideo = true;
        //判斷兩次讀取數據是否連續
        if(mPosition != position){
            if(isCacheVideo){
                //若是是本地緩存, 直接跳轉到該索引
                localStream.seek(position);
            }else{
                if(mPosition > position){
                    //同步本地緩存流
                    localStream.close();
                    deleteFileByPosition(position);
                    localStream.seek(position);
                }else{
                    isWriteVideo = false;
                }
                networkInPutStream.close();
                //從新開啓一個網絡流
                networkInPutStream = openHttpClient((int) position);
            }
            mPosition = position;
        }
        return isWriteVideo;
    }
複製代碼

4.最後一個是close()方法, 主要播放中止後釋放一些資源網絡

public void close() throws IOException {
        if(networkInPutStream != null){
            networkInPutStream.close();
            networkInPutStream = null;
        }
        if(localStream != null){
            localStream.close();
            localStream = null;
        }
        if(localVideoFile.length()!=contentLength){
            localVideoFile.delete();
        }
    }
複製代碼

④視頻緩存和LRUCache管理

1.首先建立緩存文件, 在剛纔的MediaDataSource.getSize()方法裏有一句代碼

localVideoFile = VideoLRUCacheUtil.createCacheFile(MyApplication.mContext, mMd5, contentLength);

public static File createCacheFile(Context context, String md5, long fileSize){
        //建立一個視頻緩存文件, 在data/data目錄下
        File filesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);

        File cacheFile = new File(filesDir, md5);
        if(!cacheFile.exists()) {
            cacheFile.createNewFile();
        }
        //將緩存信息存到數據庫
        VideoLRUCacheUtil.updateVideoCacheBean(md5, cacheFile.getAbsolutePath(), fileSize);
        return cacheFile;
    }
複製代碼

2.而後是讀取緩存文件, 在剛纔的MediaDataSource.getSize()方法裏還有一句代碼

//檢查本地是否有緩存, 2步確認, 數據庫中是否存在, 本地文件是否存在
    private File checkCache(String md5){
        //查詢數據庫
        VideoCacheBean bean = VideoCacheDBUtil.query(md5);
        if(bean != null){
            File file = new File(bean.getVideoPath());
            if(file.exists()){
                return file;
            }
        }
        return null;
    }
複製代碼

3.LRUCache的實現

//清理超過大小和存儲時間的視頻緩存文件
VideoLRUCacheUtil.checkCacheSize(mContext);

public static void checkCacheSize(Context context){

        ArrayList<VideoCacheBean> videoCacheList = VideoCacheDBUtil.query();

        //檢查一下數據庫裏面的緩存文件是否存在
        for (VideoCacheBean bean : videoCacheList){
            if(bean.getFileSize() == 0){
                File videoFile = new File(bean.getVideoPath());
                //若是文件不存在或者文件大小不匹配, 那麼刪除
                if(!videoFile.exists() && videoFile.length()!=bean.getFileSize()){
                    VideoCacheDBUtil.delete(bean);
                }
            }
        }

        long currentSize = 0;
        long currentTime = System.currentTimeMillis();
        for (VideoCacheBean bean : videoCacheList){
            //過久遠的文件刪除
            if(currentTime-bean.getPlayTime() > maxCacheTime){
                VideoCacheDBUtil.delete(bean);
            }else {
                //大於存儲空間的刪除
                if (currentSize + bean.getFileSize() > maxDirSize) {
                    VideoCacheDBUtil.delete(bean);
                } else {
                    currentSize += bean.getFileSize();
                }
            }
        }

        //刪除不符合規則的緩存
        deleteDirRoom(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), VideoCacheDBUtil.query());
    }

    //更新緩存文件的播放次數和最後播放時間
    public static void updateVideoCacheBean(String md5, String videoPath, long fileSize){

        VideoCacheBean videoCacheBean = VideoCacheDBUtil.query(md5);
        if(videoCacheBean == null){
            videoCacheBean = new VideoCacheBean();
            videoCacheBean.setKey(md5);
            videoCacheBean.setVideoPath(videoPath);
            videoCacheBean.setFileSize(fileSize);
        }
        videoCacheBean.setPlayCount(videoCacheBean.getPlayCount()+1);
        videoCacheBean.setPlayTime(System.currentTimeMillis());

        VideoCacheDBUtil.save(videoCacheBean);
    }
複製代碼

⑤關於多個Activity同步播放狀態, 無縫切換

1.首先在跳轉時, 通知被覆蓋的activity不關閉播放器

//首先跳轉時通知一下activity
 mainActivity.jumpNotCloseMediaPlay(position);

//而後在onPause裏
protected void onPause() {
        super.onPause();
        //若是要跳轉播放, 那麼不關閉播放器
        if (videoPositionList.size()>currentPlayIndex && jumpVideoPosition==videoPositionList.get(currentPlayIndex)) {
              ...這裏就不關閉播放器
        }else{
            //若是不要求跳轉播放, 那麼就重置播放器
            mMediaPlayerTool.reset();
      }
}
複製代碼

2.而後在新頁面初始化播放器

private void playVideoByPosition(int position){
    ......一切初始化代碼照舊(注意不要重置播放器), 這裏省略不提

     //把播放器當前綁定的SurfaceTexture取出起來, 設置給當前界面的TextureView
     vh.playTextureView.resetTextureView(mMediaPlayerTool.getAvailableSurfaceTexture());
     mMediaPlayerTool.setPlayTextureView(vh.playTextureView);
     //最後刷新一下view
     vh.playTextureView.postInvalidate();
}
複製代碼

至此代碼講解完畢, 親測在4g網絡下視頻初始化速度毫秒級, 而且在低性能手機下, 頁面來回切換無卡頓.

你們若是有不解, 能夠查看源碼瞭解更多, 有bug或優化思路 也能夠提issues

相關文章
相關標籤/搜索