最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot基于FFmpeg實(shí)現(xiàn)壓縮視頻切片為m3u8

 更新時間:2026年02月13日 09:44:36   作者:god_cvz  
本文介紹了一個使用FFmpeg將MP4視頻壓縮切片為HLS格式M3U8文件的Java工具類,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

分享一個關(guān)于使用ffmpeg對mp4文件進(jìn)行壓縮切片為hls格式m3u8文件的命令行調(diào)用程序。

前提是已經(jīng)安裝了ffmpeg,安裝過程就不再贅述了。

直接上代碼

package xxxxx;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import com.czi.uavcloud.common.utils.StringUtils;
import com.czi.uavcloud.fileprocess.utils.FileUtils;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.UUID;

/**
 * 壓縮切片處理器
 *
 * @Author god_cvz
 */
@Slf4j
public class VideoCompressHandle {

    protected static final boolean WINDOWS = System.getProperty("os.name").startsWith("Windows");
    protected static final String SLASH = WINDOWS ? "\\" : "/";
    // 輸出文件路徑,可以自行修改
    public static final String TEMP_FOLDER_PATH = (WINDOWS ? "D:" : SLASH + "var") + SLASH + "tempVideoCompress" + SLASH;

    // 切片時長,默認(rèn)5秒一個切片
    private static final String DEFAULT_HLS_TIME = "5";


    /**
     * 從指定URL下載視頻并壓縮切片為m3u8
     *
     * @param downloadUrl 視頻下載地址
     */
    public static void compressWithUrl(String downloadUrl) {
        String videoFilePath = TEMP_FOLDER_PATH + UUID.randomUUID() + SLASH + FileUtil.getName(downloadUrl);
        // 下載視頻至指定路徑
        try (BufferedInputStream bufferInput = new BufferedInputStream(getInputStreamFromUrl(downloadUrl));
             FileOutputStream out = new FileOutputStream(videoFilePath)) {
            IoUtil.copy(bufferInput, out);
        } catch (Exception e) {
            e.printStackTrace();
        }

        compressWithLocal(videoFilePath);
    }

    /**
     * 從本地路徑讀取視頻并壓縮切片為m3u8
     *
     * @param videoFilePath 本地視頻地址
     */
    public static void compressWithLocal(String videoFilePath) {
        // hutool工具返回的名稱是帶后綴的,入http://abc/123.mp4,返回的是123.mp4
        String videoName = FileUtil.getName(videoFilePath);
        // 創(chuàng)建臨時文件夾目錄
        String tempFolderPath = TEMP_FOLDER_PATH + UUID.randomUUID();
        FileUtil.mkdir(tempFolderPath);
        if (!WINDOWS) {
            try {
                // 文件夾授權(quán)
                asyncExecute(new String[]{"chmod", "777", "-R", tempFolderPath});
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        // m3u8文件輸出地址:/var/tempVideoCompress/業(yè)務(wù)id/文件名/文件名.m3u8
        String outputFolderPath = tempFolderPath + SLASH + videoName.split("\\.")[0];
        FileUtil.mkdir(outputFolderPath);
        String m3u8FilePath = outputFolderPath + SLASH + videoName.split("\\.")[0] + ".m3u8";

        handle(tempFolderPath, videoFilePath, m3u8FilePath);
    }

    /**
     * 從指定 URL 下載資源并返回 InputStream
     *
     * @param urlString 資源的 URL 地址
     * @return InputStream (需要調(diào)用方手動關(guān)閉)
     * @throws IOException 下載或連接失敗時拋出異常
     */
    public static InputStream getInputStreamFromUrl(String urlString) throws Exception {
        if (urlString == null || urlString.isEmpty()) {
            throw new IllegalArgumentException("URL 不能為空");
        }
        log.info("[Download] 開始下載文件:{}", urlString);
        long startTime = System.currentTimeMillis();

        try {
            URL url = new URL(urlString);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(10_000);
            connection.setReadTimeout(15_000);
            connection.setDoInput(true);
            // 檢查響應(yīng)碼
            int responseCode = connection.getResponseCode();
            if (responseCode != HttpURLConnection.HTTP_OK) {
                throw new IOException("下載失敗,HTTP 響應(yīng)碼:" + responseCode);
            }
            log.info("[Download] 連接成功[{}],開始接收數(shù)據(jù)...", url);
            InputStream inputStream = connection.getInputStream();
            long endTime = System.currentTimeMillis();
            log.info("[Download] 下載完成,用時 {} ms", (endTime - startTime));
            return inputStream;
        } catch (Exception e) {
            log.error("[Download] 下載文件失敗[{}], error:{}", urlString, e.getMessage());
            throw new Exception("[Download] 下載文件失敗: " + e.getMessage());
        }
    }

    public static void handle(String tempFolderPath, String videoFilePath, String m3u8FilePath) {
        long startTime = System.currentTimeMillis();
        try {
            // 壓縮切片
            log.info("[視頻壓縮切片]壓縮切片文件");
            ffmpegCompress(videoFilePath, m3u8FilePath);
            // 需要上傳的可以自己處理
//            log.info("[視頻壓縮切片]上傳切片目錄文件:{}", videoFilePath);
//            uploadFolder(outputFolder);
        } catch (Exception e) {
            log.info("[視頻壓縮切片]處理失敗:{}", e.getMessage());
        } finally {
            long costTime = System.currentTimeMillis() - startTime;
            log.info("[視頻壓縮切片]總耗時:{}", costTime);
//            log.info("[視頻壓縮切片]刪除本地壓縮包,釋放空間:{}", tempFolderPath);
//            FileUtil.del(tempFolderPath);
        }
    }

    /**
     * 將mp4視頻文件轉(zhuǎn)碼壓縮為m3u8并切片
     *
     * @param inputFile
     * @param outputFile
     */
    public static void ffmpegCompress(String inputFile, String outputFile) {
        ffmpegCompress(inputFile, outputFile, DEFAULT_HLS_TIME);
    }

    /**
     * 將mp4視頻文件轉(zhuǎn)碼壓縮為m3u8并切片
     *
     * @param inputFile  待處理文件
     * @param outputFile 輸出的具體文件路徑
     */
    public static void ffmpegCompress(String inputFile, String outputFile, String hlsTime) {
        // 上級目錄絕對路徑
        String absoluteFolderPath = outputFile.substring(0, outputFile.indexOf(FileUtil.getName(outputFile)));
        if (!FileUtil.exist(absoluteFolderPath)) {
            FileUtils.createDirIfAbsent(absoluteFolderPath);
        }
        log.info("開始壓縮轉(zhuǎn)碼文件:in:{},out:{},hlsTime:{}", inputFile, outputFile, hlsTime);
        long startTime = System.currentTimeMillis();
        // linux的ffmpeg可執(zhí)行文件放在/usr/bin/ffmpeg,可以自行修改
        String cmd = WINDOWS ? "D:\\ffmpeg\\bin\\ffmpeg.exe" : "/usr/bin/ffmpeg";
        String[] command = new String[]{cmd,
                "-i", inputFile,
                //多線程數(shù)
                "-threads", "2",
                //幀數(shù)
                "-r", "25",
                //碼率
                "-b:v", "3000k",
                //分辨率
                "-s", "1920x1080",
                //ultrafast(轉(zhuǎn)碼速度最快,視頻往往也最模糊)、superfast、veryfast、faster、fast、medium、slow、slower、veryslow、placebo這10個選項(xiàng),從快到慢
                "-preset", "ultrafast",
                //視頻畫質(zhì)級別 1-5
                "-level", "3.0",
                //從0開始
                "-start_number", "0",
                "-g", "50",
                //設(shè)置編碼器
                "-codec:v", "h264",
                //轉(zhuǎn)碼
                "-f", "hls",
                //每5秒切一個
                "-hls_time", StringUtils.isNotBlank(hlsTime) ? hlsTime : DEFAULT_HLS_TIME,
                //設(shè)置播放列表保存的最多條目,設(shè)置為0會保存所有切片信息,默認(rèn)值為5
                "-hls_list_size", "0",
                outputFile};
        try {
            asyncExecute(command);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        log.info("壓縮轉(zhuǎn)碼文件完成:{},耗時:{}", outputFile, System.currentTimeMillis() - startTime);
    }

    /**
     * 執(zhí)行shell命令
     *
     * @param cmd
     */
    public static Integer asyncExecute(String[] cmd) throws IOException {
        return asyncExecute(cmd, null, null);
    }

    /**
     * 執(zhí)行shell命令(支持任務(wù)管理器)
     *
     * @param cmd         命令數(shù)組
     * @param taskManager 任務(wù)管理器,用于注冊進(jìn)程和檢查取消狀態(tài)
     * @param taskId      任務(wù)ID
     */
    public static Integer asyncExecute(String[] cmd, Object taskManager, Long taskId) throws IOException {
        log.info("執(zhí)行命令:{}", String.join(" ", cmd));
        Process process = new ProcessBuilder(cmd)
                // 合并錯誤流和標(biāo)準(zhǔn)流
                .redirectErrorStream(true)
                .start();

        // 如果有任務(wù)管理器,注冊進(jìn)程
        if (taskManager != null && taskId != null) {
            try {
                // 使用反射調(diào)用 registerChildProcess 方法
                taskManager.getClass().getMethod("registerChildProcess", Long.class, Process.class)
                        .invoke(taskManager, taskId, process);
            } catch (Exception e) {
                log.warn("注冊子進(jìn)程失敗: {}", e.getMessage());
            }
        }

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
            // 異步讀取輸出(防止阻塞)
            Thread outputThread = new Thread(() -> {
                try {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        System.out.println("[PROCESS] " + line);

                        // 檢查任務(wù)是否被取消
                        if (taskManager != null && taskId != null) {
                            try {
                                Object context = taskManager.getClass().getMethod("getTaskContext", Long.class)
                                        .invoke(taskManager, taskId);
                                if (context != null) {
                                    Boolean shouldStop = (Boolean) context.getClass().getMethod("shouldStop")
                                            .invoke(context);
                                    if (shouldStop != null && shouldStop) {
                                        log.info("檢測到任務(wù)取消,停止讀取進(jìn)程輸出,任務(wù)ID: {}", taskId);
                                        break;
                                    }
                                }
                            } catch (Exception e) {
                                // 忽略反射調(diào)用異常
                            }
                        }
                    }
                } catch (IOException e) {
                    System.err.println("輸出流讀取異常: " + e.getMessage());
                }
            });
            outputThread.start();

            // 等待進(jìn)程完成,支持中斷檢查
            int exitCode = waitForProcessWithCancellationCheck(process, taskManager, taskId);

            // 等待輸出線程結(jié)束
            outputThread.join(1000);
            return exitCode;
        } catch (InterruptedException e) {
            log.info("進(jìn)程執(zhí)行被中斷");
            process.destroyForcibly();
            throw new RuntimeException("進(jìn)程執(zhí)行被取消", e);
        }
    }

    /**
     * 等待進(jìn)程完成,支持取消檢查
     */
    private static int waitForProcessWithCancellationCheck(Process process, Object taskManager, Long taskId) throws InterruptedException {
        while (true) {
            try {
                // 檢查線程是否被中斷
                if (Thread.currentThread().isInterrupted()) {
                    log.info("檢測到線程中斷,正在終止進(jìn)程...");
                    process.destroyForcibly();
                    throw new InterruptedException("任務(wù)被取消");
                }

                // 檢查任務(wù)管理器中的任務(wù)狀態(tài)
                if (taskManager != null && taskId != null) {
                    try {
                        Object context = taskManager.getClass().getMethod("getTaskContext", Long.class)
                                .invoke(taskManager, taskId);
                        if (context != null) {
                            Boolean shouldStop = (Boolean) context.getClass().getMethod("shouldStop")
                                    .invoke(context);
                            if (shouldStop != null && shouldStop) {
                                log.info("檢測到任務(wù)取消請求,正在終止進(jìn)程,任務(wù)ID: {}", taskId);
                                process.destroyForcibly();
                                throw new InterruptedException("任務(wù)被取消");
                            }
                        }
                    } catch (Exception e) {
                        // 忽略反射調(diào)用異常
                    }
                }

                // 非阻塞檢查進(jìn)程是否完成
                if (process.isAlive()) {
                    // 進(jìn)程還在運(yùn)行,等待一小段時間后再檢查
                    Thread.sleep(1000);
                } else {
                    // 進(jìn)程已完成,返回退出碼
                    return process.exitValue();
                }
            } catch (InterruptedException e) {
                log.info("等待進(jìn)程時被中斷,正在強(qiáng)制終止進(jìn)程...");
                process.destroyForcibly();
                throw e;
            }
        }
    }

    /**
     * 上傳切片文件目錄
     *
     * @param folder
     * @return
     */
    public static void uploadFolder(String folder) {
        // 切片文件上傳至源文件同級目錄下的一個同名文件夾
        File[] files = FileUtil.ls(folder);
        if (files == null) {
            return;
        }
        for (File file : files) {
            uploadFile(file);
        }
    }

    /**
     * todo:自定義OSS上傳
     *
     * @param file
     */
    private static void uploadFile(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {

        } catch (FileNotFoundException e) {
            log.error("讀取文件報(bào)錯");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

原視頻文件:

切片后文件:

到此這篇關(guān)于SpringBoot基于FFmpeg實(shí)現(xiàn)壓縮視頻切片為m3u8的文章就介紹到這了,更多相關(guān)SpringBoot FFmpeg視頻壓縮內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 如何在springboot中使用定時任務(wù)

    如何在springboot中使用定時任務(wù)

    這篇文章主要介紹了如何在springboot中使用定時任務(wù),幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下
    2020-12-12
  • springboot日期轉(zhuǎn)換器實(shí)現(xiàn)實(shí)例解析

    springboot日期轉(zhuǎn)換器實(shí)現(xiàn)實(shí)例解析

    這篇文章主要介紹了springboot日期轉(zhuǎn)換器實(shí)現(xiàn)實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-12-12
  • 關(guān)于JpaRepository的關(guān)聯(lián)查詢和@Query查詢

    關(guān)于JpaRepository的關(guān)聯(lián)查詢和@Query查詢

    這篇文章主要介紹了JpaRepository的關(guān)聯(lián)查詢和@Query查詢,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • Java中的Map接口實(shí)現(xiàn)類HashMap和LinkedHashMap詳解

    Java中的Map接口實(shí)現(xiàn)類HashMap和LinkedHashMap詳解

    這篇文章主要介紹了Java中的Map接口實(shí)現(xiàn)類HashMap和LinkedHashMap詳解,我們常會看到這樣的一種集合,IP地址與主機(jī)名,等,這種一一對應(yīng)的關(guān)系,就叫做映射,Java提供了專門的集合類用來存放這種對象關(guān)系的對象,需要的朋友可以參考下
    2024-01-01
  • Spring?Boot的無縫銜接實(shí)踐案例

    Spring?Boot的無縫銜接實(shí)踐案例

    在快速迭代的軟件開發(fā)環(huán)境中,無縫銜接是提升開發(fā)效率、降低維護(hù)成本、增強(qiáng)系統(tǒng)穩(wěn)定性的關(guān)鍵,本文將深入解析Spring?Boot無縫銜接的幾大優(yōu)勢,并通過實(shí)際案例和深入分析,展示這些優(yōu)勢如何在項(xiàng)目中發(fā)揮作用
    2024-08-08
  • 深入解讀MVC模式和三層架構(gòu)

    深入解讀MVC模式和三層架構(gòu)

    這篇文章主要介紹了深入解讀MVC模式和三層架構(gòu),三層架構(gòu)就是為了符合“高內(nèi)聚,低耦合”思想,把各個功能模塊劃分為表示層(UI)、業(yè)務(wù)邏輯層(BLL)和數(shù)據(jù)訪問層(DAL)的三層架構(gòu),各層之間采用接口相互訪問,需要的朋友可以參考下
    2023-04-04
  • JDK?version和class?file?version(Class編譯版本號)對應(yīng)關(guān)系解讀

    JDK?version和class?file?version(Class編譯版本號)對應(yīng)關(guān)系解讀

    這篇文章主要介紹了JDK?version和class?file?version(Class編譯版本號)對應(yīng)關(guān)系,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • springboot項(xiàng)目中常用的工具類和api詳解

    springboot項(xiàng)目中常用的工具類和api詳解

    在Spring Boot項(xiàng)目中,開發(fā)者通常會依賴一些工具類和API來簡化開發(fā)、提高效率,以下是一些常用的工具類及其典型應(yīng)用場景,涵蓋 Spring 原生工具、第三方庫(如Hutool、Guava) 和 Java 自帶工具,本文給大家介紹springboot項(xiàng)目中常用的工具類和api,感興趣的朋友一起看看吧
    2025-04-04
  • java學(xué)習(xí)筆記之eclipse+tomcat 配置

    java學(xué)習(xí)筆記之eclipse+tomcat 配置

    俗話說:工欲善其事必先利其器,既然要學(xué)習(xí)java,首先把java的開發(fā)環(huán)境搗鼓一下吧,這里我們來談?wù)別clipse+tomcat的配置方法。
    2014-11-11
  • zuul轉(zhuǎn)發(fā)后服務(wù)取不到請求路徑的解決

    zuul轉(zhuǎn)發(fā)后服務(wù)取不到請求路徑的解決

    這篇文章主要介紹了zuul轉(zhuǎn)發(fā)后服務(wù)取不到請求路徑的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-07-07

最新評論

桦南县| 晋州市| 湟源县| 晋中市| 外汇| 汨罗市| 桐梓县| 防城港市| 菏泽市| 长葛市| 永仁县| 方山县| 西乌| 冕宁县| 金秀| 巴林左旗| 高清| 抚松县| 灌南县| 广元市| 蒙山县| 海宁市| 偃师市| 孟州市| 合肥市| 福建省| 庐江县| 汽车| 台南县| 东安县| 工布江达县| 闻喜县| 宝丰县| 基隆市| 中江县| 澄江县| 重庆市| 大足县| 武陟县| 咸阳市| 耿马|