Android videoview实现视频下载缓存播放前的思考

 

        最近在处理一个业务需求,其实也是视频播放普遍存在的一个问题,我们总不可能无休止去url不断去开请求下载一个视频资源的,顺便把这个实现的过程思路和一下问题记录下来,方便以后自己学习改进,我这是需求不是做一个完整视频播放器(不集现成网络下的缓存开源库实现)。而是Android 里面的常用控件videoview, 一个Fragment 页面应用VideoView用全屏居中展示播放出来,效果如下图:

 

           第一步:这个布局xml的代码:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    tools:context=".testDemo.Fragment_one">

    <!-- TODO: Update blank fragment layout -->
    <VideoView
        android:id="@+id/video_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"/>


</FrameLayout>

 

        有了布局,我们就可以方便实现接下来的功能了,回到问题上,我们的要做的目的是什么,不就是app 里面在我上面的页面去播放我想要播放的视频资源嘛,我们随便开个线程,引用okhttp的下载文件,然后播放出来不就得咧,的确是!

        第二步,我这边用到的是一个网上流行的okhttp下载文件工具类(DownloadUtil ):

package 填写属于你自己的项目包名;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
 * 文件下载工具类(单例模式)
 */

public class DownloadUtil {

    private static DownloadUtil downloadUtil;
    private final OkHttpClient okHttpClient;

    public static DownloadUtil get() {
        if (downloadUtil == null) {
            downloadUtil = new DownloadUtil();
        }
        return downloadUtil;
    }

    public DownloadUtil() {
        okHttpClient = new OkHttpClient();
    }


    /**
     * @param url          下载连接
     * @param destFileDir  下载的文件储存目录
     * @param destFileName 下载文件名称,后面记得拼接后缀,否则手机没法识别文件类型
     * @param listener     下载监听
     */

    public void download(final String url, final String destFileDir, final String destFileName, final OnDownloadListener listener) {

        Request request = new Request.Builder()
                .url(url)
                .build();

        OkHttpClient client = new OkHttpClient();

        try {
            Response response = client.newCall(request).execute();
        } catch (IOException e) {
            e.printStackTrace();
        }

        //异步请求
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                // 下载失败监听回调
                listener.onDownloadFailed(e);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

                InputStream is = null;
                byte[] buf = new byte[2048];
                int len = 0;
                FileOutputStream fos = null;

                //储存下载文件的目录
                File dir = new File(destFileDir);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                File file = new File(dir, destFileName);

                try {

                    is = response.body().byteStream();
                    long total = response.body().contentLength();
                    fos = new FileOutputStream(file);
                    long sum = 0;
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);
                        sum += len;
                        int progress = (int) (sum * 1.0f / total * 100);
                        //下载中更新进度条
                        listener.onDownloading(progress);
                    }
                    fos.flush();
                    //下载完成
                    listener.onDownloadSuccess(file);
                } catch (Exception e) {
                    listener.onDownloadFailed(e);
                }finally {

                    try {
                        if (is != null) {
                            is.close();
                        }
                        if (fos != null) {
                            fos.close();
                        }
                    } catch (IOException e) {

                    }

                }


            }
        });
    }


    public interface OnDownloadListener{

        /**
         * 下载成功之后的文件
         */
        void onDownloadSuccess(File file);

        /**
         * 下载进度
         */
        void onDownloading(int progress);

        /**
         * 下载异常信息
         */

        void onDownloadFailed(Exception e);
    }
}

 

       我们看看他的调用 是download()方法,里面传递的参数,可以看代码。当然我们在调用这个方法的时候,不能直接调用,而是放到子线程去调用,至于为什么?应该是每个安卓开发者都要注意的线程使用问题,自己度娘吧。我这边是new Thread ()的方式去开启下载的: 

            new Thread(new Runnable() {
                            @Override
                            public void run() {
                            //uri2:下载链接 namepath: 自己保持的路径 filename:文件名
                                downFile(uri2, namepath, filename);  
                            }
                        }).start();
    /*@
     * 文件下载 url:地址 filepateh: 存放文件路径  filename: 文件名字
     */
    private void downFile(String url, String filepath, String filename) {
        DownloadUtil.get().download(url, filepath, filename,
                new DownloadUtil.OnDownloadListener() {
                    @Override
                    public void onDownloadSuccess(File file) {
                        //下载成功
                        Log.i(TAG, "onDownloadSuccess:....... ..................文件下载成功..............................");
                    }

                    @Override
                    public void onDownloading(int progress) {
                        //下载中
                        Log.i(TAG, "onDownloading:..............................文件下载中................................");
                    }

                    @Override
                    public void onDownloadFailed(Exception e) {
                        //下载异常进行相关提示操作
                        Log.e(TAG, "onDownloadFailed:..........................下载文件失败...............................");
                    //文件下载失败之后的处理
                   
                    }
                });
    }

 

        这里简单说一下downFile()传递参数问题吧,传递链接直接丢进来就行了,自己保存视频文件的路径也是自己定好就行,文件名的这里就要注意了,由于接口返回来的,你也可以用后台返回来的文件名,但是这样后台文件名更改了,我接下来的方式会出现一些bug,无论如何,我都不采用后台返回的字段,而是采用自己的一个正则表达式来提取相应文件名:

 //保留文件名及后缀
    public String getFileNameWithSuffix(String pathandname) {
        int start = pathandname.lastIndexOf("/");
        if (start != -1) {
            return pathandname.substring(start + 1);
        } else {
            return null;
        }
    }

         举例子,我传个url 进去=http://zexbrb.natappfree.cc/video/2019/10/21/92e172e5-8e5b-42c0-bf1e-6163457fd45d.mp4

getFileNameWithSuffix()提取到文件名及后缀是 92e172e5-8e5b-42c0-bf1e-6163457fd45d.mp4 ,这个名字就会用来保存在我自己的文件夹下面,这是我成功下载的文件,也是我最终想要的效果

 

 

        第三步,我们把视频文件下载完成之后,接下来是找到自己定义好的文件夹保存的路径,去查询该文件夹下面的资源文件,是否跟后台接口返回的文件一样。我这边是这样做的,用了一个getFiles(final String namepath, final String filename, long cont_length, final String uri2)方法,这里 namepath :传入本地下文件路径,filename:传入的是文件名,cont_length:传入的是url的url视频文件的大小, uri2:传入的是后台接口返回的url 访问路径

    private void getFiles(final String namepath, final String filename, long cont_length, final String uri2) {
        File file = new File(namepath);
        File[] files = file.listFiles();
        for (int i = 0; i < files.length; i++) {
            File childFile = files[i];
            String childName = childFile.getName();
            String fileSizeString = formetFileSize(childFile);
            Log.e(TAG, "本地文件名getFiles: " + childName);

            long filesize = Long.parseLong(fileSizeString);
            Log.e(TAG, "服务器文件长度fileLength=" + cont_length);
            Log.e(TAG, "本地文件长度fileLength=" + filesize);
            //如果文件名相等,就开始验证
            if (childName.equals(filename)) {
                if (cont_length == filesize) {
                    Log.i(TAG, "getFiles: 文件的大小跟服务器的大小一样,完整无损");
                    Log.e(TAG, "getFiles: 文件的链接1==" + uri2);
                } else {
                    Log.e(TAG, "getFiles: 文件的大小跟服务器的大小不一样,破损丢失");
                    Log.e(TAG, "getFiles: 文件的链接2==" + uri2);

                    if (childFile != null || childFile.exists()) {
                        childFile.delete();
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                downFile(uri2, namepath, filename);
                            }
                        }).start();
                    }

                }
            } else {
                Log.i(TAG, "getFiles:文件名不相等 ");
            }
        }
    }

    //获取本地上视频文件名与后台接口返回视频文件名相同,方法返回该视频的文件大小
    public String formetFileSize(File file) {
        String fileSize = "0";
        if (file == null) {
            fileSize = "文件不存在";
            return fileSize;
        }
        long fileLength = file.length();
        DecimalFormat df = new DecimalFormat("#");
        fileSize = df.format((double) fileLength);

        return fileSize;
    }

        如果你调用上面的getFiles()方法,你会纳闷上面的cont_length是从哪里拿的,这里也可是后台接口定义的字段取,如果没有的话,你就要用我下面的套路了,就是一个http协议的headers 去取了,用的是下面的方法:(我这种方式是在postman 调试工具上的) Content-Length这里看到是 文件的字节大小 

package 你的项目包名;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;

public class Headers_jc {
    private URL u;
    private URLConnection uc;

    public int headers_info(String usl) {
        try {
            u = new URL(usl);
            uc = u.openConnection();
            System.out.println("Content-type: " + uc.getContentType());
            if (uc.getContentEncoding() != null) {
                System.out.println("Content-encoding: " + uc.getContentEncoding());
            }
            if (uc.getDate() != 0) {
                System.out.println("Date: " + new Date(uc.getDate()));
            }
            if (uc.getLastModified() != 0) {
                System.out.println("Last Modified: " + new Date(uc.getLastModified()));
            }
            if (uc.getExpiration() != 0) {
                System.out.println("Expiration Date: " + uc.getExpiration());
            }
            if (uc.getContentLength() != 0) {
                System.out.println("Content-length: " + uc.getContentLength());
            }
        } catch (
                MalformedURLException e) {
            // TODO: handle exception
            System.err.println(e);
        } catch (IOException ex) {
            // TODO: handle exception
            System.err.println(ex);
        }
        return uc.getContentLength();
    }


}
//检测文件已经存在时,对比他的文件大小情况
因为我在activity里面写的,直接写没问题,但是你要想再fragment里面这样new 就不行了,

  Headers_jc headers_jc = new Headers_jc();
  int content_length = headers_jc.headers_info(uri); 
  Log.i(TAG,"拿到的url的文件大小是"+content_length );

fragment页面使用,必须开个子线程:

new Thread(new Runnable() {
    @Override
    public void run() {
        Headers_jc headers_jc = new Headers_jc();
        int content_length = headers_jc.headers_info(content);
      Log.i(TAG,"拿到的url的文件大小是"+content_length );
    }
}).start();

        上面是我自己踩到的问题,及解决方式。如果仅仅靠这个方法去url 的文件大小就会出现一个bug,当这个链接不能访问的时候,他也可以取出 Content-Length 值 为 38 

 

        那这值直接用上面的那些方法就比较判断来调用downFiles()下载的话就会破坏了我们原本保存视频文件完整性了,也不能说上面做了这么多就没有用,我在按思路一步步写好之后,我们必须确保url为正常访问的前提下, 具体调用可以参考我下面的方法:

 //增加对视频url的请求状态验证
                        OkHttpClient okHttpClient_url = new OkHttpClient.Builder()
                                .connectTimeout(10 * 60 * 1000, TimeUnit.MILLISECONDS)
                                .readTimeout(10 * 60 * 1000, TimeUnit.MILLISECONDS)
                                .writeTimeout(10 * 60 * 1000, TimeUnit.MILLISECONDS)
                                .build();

                        Request request_url = new Request.Builder().url(uri).get().build(); //uri:把后台接口的访问链接传递进来判断
                        okHttpClient_url.newCall(request_url).enqueue(new Callback() {
                            @Override
                            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                                Log.i(TAG, "onFailure:视频链接状态失败 ");
                            }

                            @Override
                            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                                Log.i(TAG, "onResponse: 视频链接状态成功 ");
                                int url_code = response.code();
                                Log.i(TAG, "onResponse: 视频链接成功内容 " + url_code);
                               // 状态码判断!!!!
                                if (url_code != 404 && url_code == 200) {
                                    final String url_names = getFileNameWithSuffix(uri);
                                    //具体文件路径引用
                                    if (isSDCardMounted()) {
                                        rootfilepath = Environment.getExternalStorageDirectory().getPath() + UitlData.MEADIA_VIDEO;
                                    } else {
                                        rootfilepath = Environment.getDataDirectory().getPath() + UitlData.MEADIA_VIDEO;
                                    }
                                    List<String> videpath = GetVideoFileName(rootfilepath);
                                    boolean list_filecount = videpath.contains(url_names);
                                    if (!videpath.isEmpty() && list_filecount) {
                                        Log.i(TAG, "init_test_json: 本地视频文件不为空/已存在,不下载");
                                        //检测文件已经存在时,对比他的文件大小情况
                                        Headers_jc headers_jc = new Headers_jc();
                                        int content_length = headers_jc.headers_info(uri);
                                        //开始验证
                                        getFiles(rootfilepath, url_names, content_length, uri);

                                    } else {
                                        Log.i(TAG, "init_test_json: 本地视频文件为空/不存在,开启下载。。。。");
                                        new Thread(new Runnable() {
                                            @Override
                                            public void run() {
                                                downFile(uri, rootfilepath, url_names);
                                            }
                                        }).start();
                                    }
                                } else {
                                    Log.i(TAG, "onResponse: get 请求失败");
                                }
                            }
                        });

          上面用到方法都是在activity 做的逻辑判断处理,至于我为什么这样做,是我在activity+多fragment布局应用,核心代码和思想是在加载指定fragment页面前,先去获取url下载,接着去处理下载相应的资源文件,判断本地已存在的资源完整性,确保videoview控件 去加载文件播放是完整无损的状态,最后在fragment页面中的videoview控件引用之前判断一下,在本地的文件状态是还在下载中或者下载错误丢失,不完整的情况下我们最好用一个默认的视频播放,当下次进这个fragment页面时,去加载缓存好的视频就可以避免播放错误

          实现上面的功能后,我们可以简单做个视频缓存和页面展示播放了,还有一个问题就是,我们不可能源源不断下载文件保存在自己的手机上,我们还与考虑机身的实际容量,所有要去设定他的文件大小,超过后就要去删掉,再去下载。

        我这边有两种方式删除,第一种如下,原理是根据我传进去的指定文件夹,获取该文件夹下面的文件数,如果超过了15个文件(自己定义的),我这边就会掉用方法去把改文件夹的文件全部清空,并且删除它的目录文件夹,重启程序。(注:我这里的确可以实现清空缓存问题,但是这样会把所有文件都清空,我这边想留下最后缓存的文件,来来播放就要重新去下载回来,造成不必要的操作,然后就是,方式二改进了这个问题)

   if (isSDCardMounted()) {
                                    rootfilepath = Environment.getExternalStorageDirectory().getPath() + UitlData.MEADIA_VIDEO;
                                } else {
                                    rootfilepath = Environment.getDataDirectory().getPath() + UitlData.MEADIA_VIDEO;
                                }
                                List<String> videpath2 = GetVideoFileName(rootfilepath);
                                Log.i(TAG, "onResponse: 数据不相同 当前下文件数" + videpath2);
                                if (videpath2.size() >= 15) {
                                    Log.i(TAG, "onResponse: 数据不相同 当前下文件数大于等于15个清空文件夹");
                                    deleteDir(UitlData.MEADIA_VIDEO);
                                }
                                Log.i(TAG, "onResponse: 数据不相同 当前下文件数小15个不清空文件夹");

 

/**
     * 删除文件夹
     *
     * @param path
     */
    public static void deleteDir(String path) {
        File dir = new File(path);
        deleteDirWihtFile(dir);
    }

    //删除文件及文件夹
    public static void deleteDirWihtFile(File dir) {
        if (dir == null || !dir.exists() || !dir.isDirectory())
            return;
        for (File file : dir.listFiles()) {
            if (file.isFile())
                file.delete(); // 删除所有文件
            else if (file.isDirectory())
                deleteDirWihtFile(file); // 递规的方式删除文件夹
        }
        dir.delete();// 删除目录本身
    }

      方式二,原理是一样,根据我传进去的指定文件夹,获取该文件夹下面的文件数,如果超过了15个文件(自己定义的)就保留最后一个最新的文件(这里主要是用了文件按时间排序来处理),删除其余的旧文件,重启程序,方便videoview的播放。

文件按时间排序返回的处理方式如下+下面的代码配合使用: 

   https://blog.csdn.net/qq_36771930/article/details/103048738 

//这段代码是将排序后的文件夹顺序,旧到新,我这边拿最新的文件名来判断删除,其他旧文件。
String lastmax = list_delete.get(list_delete.size() - 1).getName();
Log.i("ZMS==", lastmax);

 

    /**
     * 删除文件夹排序
     */
    private static void deleteff(String paths, String filename) {
        File dirs = new File(paths);

        if (dirs == null || !dirs.exists() || !dirs.isDirectory())
            return;
        for (File file : dirs.listFiles()) {
            System.gc();
            if (file.isFile()) {
                if (!(file.getName().equals(filename))) {
                    System.out.println("文件排序=" + file.getName());
                    file.delete(); // 删除所有文件
                } else {
                    System.out.println("文件排序名字相等");
                }

            }

        }
    }

下面记录一下,自己的开发常用工具类或者方法的使用:

1.Android 创建指定文件夹路径

文件清单添加权限:

<uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- 文件权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
   
    public static String FILEPATH ="/Medx";
    //视频文件夹

    public static String MEADIA_VIDEO = FILEPATH + "/Video";
    //图片文件夹
    public static String MEADIA_IMAGE = FILEPATH + "/Image";

 private void init_rootfile() {
        if (isSDCardMounted()) {
            //sd卡下
            String  filepaths=Environment.getExternalStorageDirectory().getPath() + UitlData.MEADIA_VIDEO;
            makeRootDirectory(filepaths);
            Log.i(TAG, "12345;" + filepaths);

        } else {
            //   makeRootDirectory(UitlData.MEADIA_IMAGE);
            String  filepaths2=Environment.getDataDirectory().getPath() + UitlData.MEADIA_VIDEO;
            makeRootDirectory(filepaths2);
            Log.i(TAG, "123456" + filepaths2);
        }
    }

    public static void makeRootDirectory(String filePath) {
        try {
            File file = new File(filePath);
            if (!file.exists()) {
                file.mkdirs();
            } else {
            }
        } catch (Exception e) {
            Log.i(TAG,"文件夹创建失败"+e.getMessage());
        }
    }
    /**
     * sd卡是否安装
     */
    public static boolean isSDCardMounted() {
        return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    }

----------2020/7/ 24------上面方式一改良--------- 有网络很好的情况下-------------------

改良后下载缓存的方式:

1上面的方式在拿到视频下载链接之后开始:

  Log.i(TAG, "init_test_json:type=2,本地视频文件不存在,开启下载。。。。");
  new Thread(new VoidThread_Down(uri,url_names)).start();
 //开启视频线程下载
    private class VoidThread_Down implements Runnable {
        String thread_uri, thread_url_names;

        public VoidThread_Down(String thread_uri, String thread_url_names) {
            this.thread_uri = thread_uri;
            this.thread_url_names = thread_url_names;
        }

        @Override
        public void run() {
            Log.i(TAG, "VoidThread_Down链接=" + thread_uri);
            Log.i(TAG, "VoidThread_Down路径=" + rootFilepath());
            Log.i(TAG, "VoidThread_Down文件名=" + thread_url_names);
            downFile(thread_uri, rootFilepath(), thread_url_names);
        }
    }

      2.这是第一下载,有些某些原因,可能导致第一次下载文件存在破损的情况,导致视频不能播放,我们再做第二次视频文件的完整性判断:

  Log.i(TAG, "init_test_json:type=2,本地视频文件已存在,不下载");
  Headers_jc headers_jc = new Headers_jc();   //检测文件已经存在时,对比他的文件大小情况
  int content_length = headers_jc.headers_info(uri);
  getFilesvideo(url_names, content_length, uri);   //开始验证

 

 /**
     * 获取本地文件和服务器接口url返回的文件
     * 进行长度比较
     */
    private void getFilesvideo(final String filename, long cont_length, final String uri2) {
        File file = new File(rootFilepath());
        File[] files = file.listFiles();
        int videofile = getIndex(files, filename);
        Log.i(TAG, "getFilesvideo: videofile数组下标=" + videofile);

        if (videofile != -1) {
            Log.i(TAG, "getFilesvideo:文件名相等 ");
            String childName = files[videofile].getName();
            String fileSizeString = formetFileSize(files[videofile]);
            long filesize = Long.parseLong(fileSizeString);
            Log.e(TAG, "getFilesvideo:本地文件名=" + childName);
            //如果文件破损
            if (cont_length != filesize) {
                Log.i(TAG, "getFilesvideo: 文件的大小跟服务器的大小不一样,破损丢失");
                if (files[videofile] != null && files[videofile].exists()) {
                    files[videofile].delete();
                    new Thread(new VoidThread_Down(uri2, filename)).start();
                }
            } else {
                Log.i(TAG, "getFilesvideo: 文件的大小跟服务器的大小一样,完整无损");
            }

        } else {
            Log.i(TAG, "getFilesvideo:文件不存在 ");
            Log.i(TAG, "getFilesvideo:链接 " + uri2);
            Log.i(TAG, "getFilesvideo:路径 " + rootFilepath());
            Log.i(TAG, "getFilesvideo:名称 " + filename);
            new Thread(new VoidThread_Down(uri2, filename)).start();
        }

    }

    这里面多做了一个文件位置存放的方式:

/**
     * sd卡是否安装
     */
    public static boolean isSDCardMounted() {
        return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    }

    public static String rootFilepath() {
        String rootfile;
        if (isSDCardMounted()) {
            rootfile = Environment.getExternalStorageDirectory().getPath() + UitlData.MEADIA_VIDEO;
        } else {
            rootfile = Environment.getDataDirectory().getPath() + UitlData.MEADIA_VIDEO;
        }
        return rootfile;
    }

这里开一个线程去定时去检测本地存在的视频文件是破损:

 private VerifgVideoRunnable verifgVideoRunnable;




  private void VerifgVideoTime() {
        if (verifgVideoRunnable == null) {
            verifgVideoRunnable = new VerifgVideoRunnable();
            handler.postDelayed(verifgVideoRunnable, 0);

        }
    }

    //3.视频本地资源验证
    private class VerifgVideoRunnable implements Runnable {
        @Override
        public void run() {
            Log.i("video请求时间=", "15分钟");
            init_handerNum(9);
            handler.postDelayed(this, 15 * 60 * 1000);
        }
    }

@Override
    protected void onDestroy() {
        super.onDestroy();
        onclicknum = 0;
        handler.removeCallbacks(verifgVideoRunnable);
        verifgVideoRunnable = null;
        apkRunnable = null;
        handler.removeCallbacksAndMessages(null);
    }

最后在oncreate()调用

  VerifgVideoTime(); //视频文件完整性验证

-------2020/7/ 27----8/5   --记录不依赖网络的情况下使用MediaMetadataRetriever(在Android4.4.2版本会出现无法解决的bug)----------不建议使用---------

1.MediaMetadataRetriever是什么?

官网看不了,可以看看这位大佬的文章:https://blog.csdn.net/alpgao/article/details/88995410,会发现,其实这个类压根没有文件破损的方法,这种方式是为了解决,有时候网络不好的情况下,去请求视频url,会有超时的情况下,体验不好,容易出现各种bug,起初用到该方法的是在,网上找的,拿下来简单测试了一下,就以为可以用了,但是徒劳,Android4.4.2版本上面导致我的页面白屏加载不出来,也找不到原因所在,就放弃了这种方式


    /**
     * 获取本地文件和服务器接口url返回的文件
     * 破损分析
     * @param filename 视频文件名
     * @param uri2     视频链接
     */
    private void getFiles_videos(final String filename, final String uri2) {
        File file = new File(rootFilepath());
        File[] files = file.listFiles();
        int videofile = getIndex(files, filename);
        Log.i(TAG, "getFiles_videos: videofile数组下标=" + videofile);

        if (videofile != -1) {
            Log.i(TAG, "getFiles_videos:文件名相等 ");
            String videopath = files[videofile].getPath();
            boolean isVideo = isVideoStatus(videopath);
            if (!isVideo) {
                Log.i(TAG, "getFiles_videos: 文件破损丢失");
                if (files[videofile] != null && files[videofile].exists()) {
                    files[videofile].delete();
                    new Thread(new VoidThread_Down(uri2, filename)).start();
                }
            }
        } else {
            Log.i(TAG, "getFiles_videos:文件不存在 ");
            Log.i(TAG, "getFiles_videos:链接 " + uri2);
            Log.i(TAG, "getFiles_videos:路径 " + rootFilepath());
            Log.i(TAG, "getFiles_videos:名称 " + filename);
            new Thread(new VoidThread_Down(uri2, filename)).start();
        }

    }

 

 

 private boolean isVideoStatus(String videofile) {
        //新增视频文件是否破损检测
        boolean isVideo = false;
        retriever = new MediaMetadataRetriever();
        try {
            retriever.setDataSource(videofile);
            String hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
            isVideo = "yes".equals(hasVideo);
            Log.i(TAG, "isVideoStatus: 是否破损isVideo=" + isVideo); //true 正常 false 破损
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        }finally {
            retriever.release();
        }

        return isVideo;
    }

在拿到视频uri的地方,调用方法

getFiles_videos(url_names, uri); 即可去检测将要播放的视频文件是否存在破损的情况了,

-----第二种方式---------2020/7/31--------测试备注--------------------------------------------------------------- 

      第二种虽然不用okhttp请求头的,方式避免了一定视频文件的开始就是错误的使用,缺点是当视频文件只是下载了一半的情况下,第二种的方式也不能完全检测该文件的完整性,当我这边用videoview来播放的时候,才测试出来,有视频破损的情况,这里找了很多方式,结合自己的业务代码情况,找到一个折中的处理的办法,大概核心思路是:当我用videoview播放到有错误的视频文件,我就把他的文件名记录下来,然后开个定时循环的线程去检测我的一个list数组里面是不是存放有错误的文件名,把该文件名字对应接口的视频url地址,再去执行一次下载,确保该文件的下载完整。

  video_view.setOnErrorListener(new MediaPlayer.OnErrorListener() {
                    @Override
                    public boolean onError(MediaPlayer mp, int what, int extra) {
                        Log.e(TAG, "off_onlieGetFiles:setOnErrorListener  " + what + "***" + extra);
                        callBackValue.Fragemt_videodownlad(filename); //检测到视频错误,发送到主activity下载
                        iv_videoerror.setVisibility(View.VISIBLE);
                        video_view.pause();//暂停
                        video_view.stopPlayback();
                        video_view.suspend();//在任何状态下释放媒体播放器
                        return true;
                    }
                });
    //专门处理视频异常问题重新开启下载
    @Override
    public void Fragemt_videodownlad(String filename) {
        Log.i(TAG, "Fragemt_videodownlad: 出现播放异常的文件名是=" + filename);

        if (!error_videofile.contains(filename)) {
            error_videofile.add(filename);
        }
        for (int i = 0; i < error_videofile.size(); i++) {
            Log.i(TAG, "Fragemt_videodownlad: error_videofile=" + error_videofile.get(i));
        }
    }
 if (!error_videofile.isEmpty()) {
                err_file = error_videofile.contains(filename);
                Log.i(TAG, "getFiles_videos: err_file=" + err_file);
            }else {
                err_file=false;
            }

            if (isVideo == false || err_file == true) {
                Log.i(TAG, "getFiles_videos:文件存在,破损丢失 ");
                if (files[videofile] != null && files[videofile].exists()) {
                    files[videofile].delete();
                    new Thread(new VoidThread_Down(uri2, filename)).start();
                }
            }

 开始下载的时候,把list 数组保存的错误视频名删除掉

   downFile(thread_uri, rootFilepath(), thread_url_names);
              //及时删掉已经执行下载的数据
            if (!error_videofile.isEmpty()) {
                for (int i = 0; i < error_videofile.size(); i++) {
                  if(error_videofile.get(i).equals(thread_url_names)){
                      error_videofile.remove(i);
                  }
                }
            }

 第三种方式:

 

 ----------------------------------------2020 8-6-------------- 采坑的记录--------------------------------------

       上面代码不是完整的,大概处理方式是这样的,只能说是折中办法。

上面处理方式核心都是想检测到将要播放的视频是报错的,就把本地路径文件夹下的该视频删除掉,然后通过定时请求接口,判断文件是否存在,没有就下载。

        问了很多大佬,其他们都是一致建议我用MD5的方式来判断文件的完整性,在和后台接口对接方面,先和后台的小伙伴来商量好,除了返回视频的下载地址,还有的就是MD5的字段,方便我们后面的文件破损验证,完全不用再和我上面的方式一,二的处理了,更加简单省事。

        可见为了实现一个视频播放,也是不是那么轻松完事的,还要考虑其他bug的处理上,以及我们一开始选择的方向也很重要,但是我们可以清楚知道问题的优缺点,去处理问题,也是事半功倍。当然上面的涉及代码较多,主要是核心的处理方法,展示出来,有时间可以耐心看看,也可ctrl\c到自己的编辑器上去运行使用。完整demo源码不提供下载,当然本人的知识水平有限,如果在本文使用缓存视频文件,有好的建议或者错误,可以在评论区留言一起探讨,方便我的修改和学习,谢谢。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Android VideoView 是一个可以用来播放视频的视图控件,但是有时候会遇到无法播放某些视频的情况。 造成 Android VideoView 无法播放视频的原因有很多,下面是一些可能的原因: 1. 文件格式不受支持:Android VideoView支持播放视频格式有限,例如常见的MP4、3GP等格式,但是某些特殊格式如MKV、FLV等可能无法播放。 2. 编码方式不受支持:即使是支持的文件格式,如果视频使用了不受支持的编码方式,也无法播放Android支持的编码方式包括H.264、H.263等。 3. 视频文件损坏:如果视频文件本身损坏或者缺失必要的编码信息,也会导致无法播放。 4. 网络问题:如果播放视频来自网络,可能是网络连接问题导致无法播放。例如服务器问题、网络限速等。 解决这个问题的方法有几种: 1. 转码视频:将不受支持的视频格式或编码方式转换为支持的格式,可以使用一些视频转换工具来完成。 2. 使用其他视频播放器:如果VideoView无法播放某个视频,可以尝试其他的视频播放器,如ExoPlayer、VLC等。 3. 检查视频文件:确保视频文件没有损坏,尝试播放其他已知可以正常播放视频文件来检查。 4. 检查网络连接:如果视频来自网络,确保网络连接正常,并尝试使用其他网络或播放其他视频来排除网络问题。 总之,如果Android VideoView无法播放某个视频,我们需要先检查视频文件格式、编码方式、文件完整性以及网络连接等方面的问题,然后采取相应的解决方法来解决这个问题。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值