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

背景介绍:

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

M3U8下载,直播源下载,FLASH下载(四)-m3u8直播源下载工具类_第1张图片

    并且对下载模块进行了优化,比如动态的监测下载的情况,下载完成提前结束等。后期我准备增加通过分析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

 

 

 

 

 

 

你可能感兴趣的:(m3u8,java)