背景介绍:

这个工具类是我依据上一篇博文进行的改良,引入了目前流行的链式编程。具体使用方式如下:

并且对下载模块进行了优化,比如动态的监测下载的情况,下载完成提前结束等。后期我准备增加通过分析ffmpeg的输出流来控制程序的结束,而不是目前的简单通过文件的大小来判别。使线程更加的灵活多变。

默认我把输出流隐藏了,如果需要查看输出流请修改 M3U8Downloader.getStream() 方法中的 LOG.trace...

代码:

M3U8Downloader类

package cn.edu.zua.mytool.media.m3u8;import cn.edu.zua.mytool.core.io.IOUtils;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;/*** M3U8Downloader** @author adeng* @date 2018/8/20 15:09.*/
public class M3U8Downloader {private static final Logger LOG = LoggerFactory.getLogger(M3U8Downloader.class);static final String BASE_PATH = File.separator + "tmp";/*** 如果文件下载正常,等待时间可能会超过该值,一般情况下不需要改动*/private static final int DOWNLOAD_MAX_SECOND = 300;// propertiesprivate String absoluteFilePath;/*** 是否保留文件,默认false*/private boolean saveFile;/*** 包访问全限构造函数,请通过 {@link M3U8DownloaderBuilder} 构造** @param absoluteFilePath 文件的全限定名*/M3U8Downloader(final String absoluteFilePath) {this.absoluteFilePath = absoluteFilePath;}/*** 包访问权限*/void setSaveFile(boolean saveFile) {this.saveFile = saveFile;}/*** 把m3u8短视频下载后提取byte数组,并且删除临时文件** @param m3u8Url 直播源地址* @return byte[], 从文件中提取的字节数组* @throws InterruptedException 中断异常* @throws IOException          IO异常*/public byte[] downloadBytes(String m3u8Url) throws InterruptedException, IOException {File file = downloadFile(m3u8Url);LOG.debug("字节数组方法下载调用成功:{}, 文件大小:{}", file.getAbsolutePath(), file.length());LOG.debug("线程暂停2s,把文件转换为字节数组");TimeUnit.SECONDS.sleep(2);FileInputStream input = FileUtils.openInputStream(file);byte[] bytes = IOUtils.toByteArray(input);input.close();LOG.info("返回文件数组!!文件长度:{}", bytes.length);// 删除下载后的文件,判断是否保存文件if (!this.saveFile && file.exists()) {LOG.debug("文件存在,删除文件!{}", file.getAbsolutePath());try {FileUtils.forceDelete(file);LOG.debug("文件删除成功!");} catch (IOException e) {LOG.debug("文件删除失败!");e.printStackTrace();}}return bytes;}/*** 把m3u8直播流下载后提取File,文件不会主动删除** @param m3u8Url 直播源地址* @return 直播源文件,File,文件名称为随机生成,规则:UUID(32)+yyyyMMddHHmmss* @throws InterruptedException 中断异常* @throws IOException          IO异常*/public File downloadFile(String m3u8Url) throws InterruptedException, IOException {String command = "ffmpeg -i " + m3u8Url + " -vcodec copy " + absoluteFilePath + " -y";LOG.debug("执行命令:{}", command);Process process = Runtime.getRuntime().exec(command);ExecutorService executorService = Executors.newFixedThreadPool(3);getStream(executorService, process.getErrorStream(), true);getStream(executorService, process.getInputStream(), true);TimeUnit.SECONDS.sleep(10);File file = new File(absoluteFilePath);// 最长再等待24秒for (int i = 0; i < 8; i++) {if (!file.exists()) {LOG.debug("文件不存在,重新创建...");TimeUnit.SECONDS.sleep(2);file = new File(absoluteFilePath);}}final boolean[] checkFlag = {true};final int[] waitSecond = {DOWNLOAD_MAX_SECOND};executorService.execute(() -> {try {while (waitSecond[0] >= 0) {TimeUnit.SECONDS.sleep(40);waitSecond[0] = waitSecond[0] -40;LOG.debug("剩余等待时间:{}", waitSecond[0]);}checkFlag[0] = false;} catch (InterruptedException e) {checkFlag[0] = false;LOG.error("最长等待线程被中断,正常错误,文件路径:{}", absoluteFilePath);}});long size = 0;int maxCount = 5;while (checkFlag[0]) {TimeUnit.SECONDS.sleep(6);long fileLength = file.length();LOG.debug("上一次监测的大小:{}, 本次监测的大小:{},6s后刷新状态", size, fileLength);if (file.length() != size) {waitSecond[0] = DOWNLOAD_MAX_SECOND;LOG.debug("文件正常下载中...6s后刷新状态");size = file.length();TimeUnit.SECONDS.sleep(6);} else {// 当文件大小持续为0 或者文件大小持续不变,尝试次数减少,尝试等待次数用尽后提前退出if (fileLength == 0 || fileLength == size) {maxCount--;if (maxCount < 0) {LOG.debug("尝试次数用尽,退出下载线程...");checkFlag[0] = false;continue;}LOG.debug("文件大小未发生变化,剩余尝试次数 {} 次,6s后刷新状态", maxCount);}//. end of if}}process.destroy();executorService.shutdownNow();LOG.info("文件下载成功:{}", file.getAbsolutePath());return file;}/*** 输出流** @param executorService ExecutorService* @param inputStream     数据流* @param printFlag       是否打印标志*/private static void getStream(ExecutorService executorService, final InputStream inputStream, final boolean printFlag) {executorService.execute(() -> {BufferedInputStream in = new BufferedInputStream(inputStream);byte[] bytes = new byte[1024];try {while (in.read(bytes) != -1) {String s = new String(bytes, 0, bytes.length);if (printFlag) {LOG.trace("ffmpeg out:{}", s);}}} catch (IOException e) {LOG.error("读取下载流失败", e);} finally {try {in.close();} catch (IOException e) {LOG.error("关闭读取流失败:", e);}}});}}

M3U8DownloaderBuilder类

package cn.edu.zua.mytool.media.m3u8;import cn.edu.zua.mytool.core.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;/*** M3U8DownloaderBuilder* 模拟流行的链式编程构建M3U8Downloader* 文件的默认下载根目录:/tmp* 文件的默认名称:随机生成,规则:UUID(32)+yyyyMMddHHmmss* 文件默认没有后缀名称,文件名称,后缀名称可包含或者不包含,如果suffix被设置,fileName的后缀名将会被覆盖。** @author adeng* @date 2018/8/20 15:09.*/
public class M3U8DownloaderBuilder {private static final Logger LOG = LoggerFactory.getLogger(M3U8DownloaderBuilder.class);private String basePath = M3U8Downloader.BASE_PATH;private String fileName;private String suffix;/*** 是否保留文件,默认false*/private boolean saveFile;/*** 设置文件父级目录** @param basePath 文件父级目录*/public final M3U8DownloaderBuilder setBasePath(final String basePath) {this.basePath = basePath;return this;}/*** 设置文件名称,根目录为父级目录,可通过 {@link M3U8DownloaderBuilder#setBasePath(String)} 设置** @param fileName 文件名称,后缀名称可包含或者不包含,如果suffix被设置,这里的后缀名将会被覆盖。*/public final M3U8DownloaderBuilder setFileName(final String fileName) {this.fileName = fileName;return this;}/*** 设置文件后缀名,eg: ".mp3|.mp4"** @param suffix 文件后缀名称*/public final M3U8DownloaderBuilder setSuffix(final String suffix) {this.suffix = suffix;return this;}/*** 设置保留文件,也可以调用 {@link M3U8DownloaderBuilder#saveFile() } 默认不保留文件* @param saveFile boolean*/public final M3U8DownloaderBuilder setSaveFile(final boolean saveFile) {this.saveFile = saveFile;return this;}/*** 保留文件,等效于 {@link M3U8DownloaderBuilder#setSaveFile(boolean)} true.*/public final M3U8DownloaderBuilder saveFile() {this.setSaveFile(true);return this;}public M3U8Downloader build() {// 文件父级目录检查,如果不存在目录,则创建File baseDir = new File(basePath);if (!baseDir.exists()) {LOG.warn("文件父级目录不存在,创建目录!");baseDir.mkdirs();}if (M3U8Downloader.BASE_PATH.equals(basePath)) {LOG.info("文件父级目录未设置,采用默认路径:{}", M3U8Downloader.BASE_PATH);}// 文件后缀名称检查if (StringUtils.isBlank(suffix)) {if (StringUtils.isNotBlank(fileName) && fileName.lastIndexOf(".") == -1) {LOG.warn("文件后缀名称未指定!");}} else {if (!suffix.startsWith(".")) {suffix = "." + suffix;}}// 文件名称设置if (StringUtils.isBlank(fileName)) {fileName = UUID.randomUUID().toString().replaceAll("-", "") + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));String fn = fileName + (StringUtils.isBlank(suffix) ? "" : suffix);LOG.info("文件名称未设置,采用随机文件名:{}", fn);}// 后缀名称设置优先if (StringUtils.isNotBlank(suffix)) {if (fileName.lastIndexOf(".") != -1) {fileName = fileName.substring(0, fileName.lastIndexOf("."));}fileName = fileName + suffix;}String absoluteFilePath = new File(basePath, fileName).getAbsolutePath();File absoluteDir = new File(absoluteFilePath.substring(0, absoluteFilePath.lastIndexOf(File.separator)));if (!absoluteDir.exists()) {absoluteDir.mkdirs();}LOG.debug("absoluteFilePath:{}", absoluteFilePath);// 构造DownloaderM3U8Downloader downloader = new M3U8Downloader(absoluteFilePath);downloader.setSaveFile(saveFile);return downloader;}}

测试类:

package cn.edu.zua.mytool.media.m3u8;import org.testng.annotations.Test;import java.io.File;
import java.io.IOException;/*** M3U8DownloaderTest** @author adeng* @date 2018/8/21 9:48.*/
public class M3U8DownloaderTest {@Testpublic void testDown1() throws IOException, InterruptedException {String m3u8Url = "http://recordcdn.quklive.com/upload/vod/user1462960877450854/1527512379701708/3/video.m3u8";M3U8Downloader downloader = new M3U8DownloaderBuilder().setBasePath("/a/b/c").setFileName("hello.mp4").saveFile().build();byte[] bytes = downloader.downloadBytes(m3u8Url);System.out.println("bytes.length = " + bytes.length);}}

从此下片爽不行

再具体的api什么的我就不提供了,注释已经很详细了。到此为止,告辞告辞。

看这里,看这里

文章总目录:博客导航

码字不易,尊重原创,转载请注明:https://blog.csdn.net/u_ascend/article/details/82257680

M3U8下载,直播源下载,FLASH下载(四)-m3u8直播源下载工具类相关推荐

  1. Android通过url下载图片到手机本地并显示和删除本地ImageUtil工具类

    文章目录 布局 Activity BroadcastReceiver 文件工具类 图片工具类 Manifest MyApplication 完成代码下载 这里的demo是一个通过下载地址下载图片文件到 ...

  2. Java源码详解四:String源码分析--openjdk java 11源码

    文章目录 注释 类的继承 数据的存储 构造函数 charAt函数 equals函数 hashCode函数 indexOf函数 intern函数 本系列是Java详解,专栏地址:Java源码分析 Str ...

  3. JUC系列(四) callable与 常用的工具类

  4. Android源码分析(十一)-----Android源码中如何引用aar文件

    一:aar文件如何引用 系统Settings中引用bidehelper-1.1.12.aar 文件为例 源码地址:packages/apps/Settings/Android.mk LOCAL_PAT ...

  5. Android音频实时传输与播放(四):源码下载(问题更新)【转】

    Android音频实时传输与播放(四):源码下载(问题更新) 激动人心的时刻到了有木有 ^_^ 服务端下载请点击这里,客户端下载请点击这里! 最近有朋友在下载源码使用之后,说播放出来的声音噪声很大.其 ...

  6. 【流媒体开发】VLC Media Player - Android 平台源码编译 与 二次开发详解 (提供详细800M下载好的编译源码及eclipse可调试播放器源码下载)

    作者 : 韩曙亮  博客地址 : http://blog.csdn.net/shulianghan/article/details/42707293 转载请注明出处 : http://blog.csd ...

  7. cordova打开文件_Cordova开发App入门(四)打开及下载第三方App

    前言 在开发app时经常会碰到这种需求:我要在A应用中打开B应用,那么用Cordova如何实现呢? 欲知后事如何,且听下回分解...... ... 大大大大佬,别,别打我,我继续写还不行了吗. 本文涉 ...

  8. centos php rpm下载源,CentOS 6.2 使用第三方yum源安装更多rpm软件包 | 系统运维

    引言:CentOS自带的yum源中rpm包数量有限,很多时候找不到我们需的软件包,(例如:要安装网络连接查看软件iftop,默认设置下无法使用yum命令安装),下面教大家在CentOS 6.2中(以3 ...

  9. 必不可少!STL源码目录结构分析,附加源码下载链接

    一.STL源码的下载 下载地址1 3种下载方式: 公众号[多栖技术控小董]回复[12754727]获取百度云下载链接. CSDN:https://download.csdn.net/download/ ...

  10. FLASH制作全套装备(强烈推荐下载)

    FLASH制作全套装备(强烈推荐下载) 作者:admin 日期:2008-09-19 字体大小: 小 中 大 读完绝对超值!不管你是FLASH爱好者,还是FLASH专业制作者,我相信,下面的软件肯定有 ...

最新文章

  1. 博客园Markdown新建笔记
  2. java中filereader读取文件_java – 如何使用FileReader逐行读取
  3. 4 .2 .4 配置存储系统
  4. 面试中经常会问的智力题,来看看你会做几道
  5. 移动端禁止视频自动全屏播放
  6. php语言注册信息表,php语言表的空结果
  7. 线程的异常捕获与线程池的异常捕获
  8. 6. Controller
  9. Java http响应状态码_如何获得HttpClient返回状态码和响应正文?
  10. this.$router.push跳转到原来页面不刷新的问题解决
  11. idea插件开发实战基础
  12. AUTOCAD——设置颜色
  13. 蓝桥杯——摔手机 动态规划
  14. NBIOT专栏之BC28与STM32F103单片机串口通信连接阿里云
  15. 自定义字体之BMFont的使用
  16. 个性化智能推荐(协同过滤算法)技术研究
  17. 计算机网络——RIP 路由协议配置
  18. 怎么找小红书达人推广?小红书达人推广方式有哪些_云媒易
  19. JavaScript封装异步函数 —— 【异步编程】 —— 如何获取一个函数中异步操作的结果?
  20. 记录一些博客写作心得

热门文章

  1. android 空格的转义字符,Android 常用转义字符
  2. 数据结构(C语言版)严蔚敏李冬梅(第2版)课后习题答案
  3. android系统源代码单独编译应用程序
  4. 使用 Jupyter Notebook
  5. eclipse反编译调试源码调试
  6. 模糊层次分析法matlab,求三角模糊数层次分析法(FEAHP)模型计算的MATLAB程
  7. Java数据类型转换超详解
  8. java数据类型int_java数据类型
  9. python多态实例_Python多态实例详解
  10. 随身助手271个可用api接口网站php源码(随身助手API)