这个工具类是我依据上一篇博文进行的改良,引入了目前流行的链式编程。具体使用方式如下:
并且对下载模块进行了优化,比如动态的监测下载的情况,下载完成提前结束等。后期我准备增加通过分析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;
// properties
private 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);
// 构造Downloader
M3U8Downloader 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 {
@Test
public 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