javacv将视频切分为m3u8视频并播放

学习链接

ffmpeg-demo 当前对应的 gitee代码

Spring boot视频播放(解决MP4大文件无法播放),整合ffmpeg,用m3u8切片播放。

springboot 通过javaCV 实现mp4转m3u8 上传oss

ffmpeg视频转切片m3u8并加密&videojs播放&hls.js播放&dplayer播放(弹幕效果)

video标签学习 & xgplayer视频播放器分段播放mp4

SpringBoot&FFmpeg实现上传视频到本地,使用M3U8切片转码后,下方使用hls.js播放(支持mp4&avi),SpringBoot + FFmpeg实现一个简单的M3U8切片转码系统

文章目录

简介

将上传的视频文件,使用javacv拆分成m3u8文件和ts文件,m3u8文件和ts文件通过nginx访问,而key文件则通过web服务来获取。使用dplayer播放视频。

也可以使用ffmpeg命令来做,可以参考上面链接。

效果图

m3u8文件和ts文件通过nginx访问,而key文件则通过web服务来获取

拿不到key文件是无法播放的

代码

pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <modelVersion>4.0.0</modelVersion>


    <groupId>org.example</groupId>
    <artifactId>ffmpeg-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <javacv.version>1.5.4</javacv.version>
        <ffmpeg.version>4.3.1-1.5.4</ffmpeg.version>
    </properties>

    <dependencies>

        <!--web 模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <!--排除tomcat依赖 -->
                <exclusion>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--undertow容器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--      javacv 和 ffmpeg的依赖包      -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv</artifactId>
            <version>${javacv.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.bytedeco</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>ffmpeg-platform</artifactId>
            <version>${ffmpeg.version}</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.6.5</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>



</project>

application.yml

yml 复制代码
server:
  port: 8080
spring:
  servlet:
    multipart:
      max-file-size: 500MB
      max-request-size: 500MB

WebConfig

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {


    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/test/**").addResourceLocations("file:" + System.getProperty("user.dir") + "/test/");
        registry.addResourceHandler("/tmp/**").addResourceLocations("file:" + System.getProperty("user.dir") + "/tmp/");
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                .addMapping("/**")
                .maxAge(3600)
                .allowCredentials(true)
                .allowedOrigins("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .exposedHeaders("token","Authorization")
        ;
    }
}

TestController

java 复制代码
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.io.file.FileWriter;
import com.zzhua.processor.FFmpegProcessor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
 
@RestController
public class TestController {
 
    /**
     * 目录路径,这个路径需要包含test.info文件,test.key文件和test.mp4文件
     */
    private static final String PATH = "D:\\Projects\\practice\\ffmpeg-demo\\test\\";
 
    @RequestMapping("uploadToM3u8")
    public void uploadToM3u8() throws Exception {
        FileInputStream inputStream = new FileInputStream(PATH + "test.mp4");

        /* 这里原来的逻辑是
            1、m3u8Url是将生成的m3u8文件流写入的位置,可以填写接收该请求的接口路径
            2、infoUrl是获取keyinfo文件的路径,可以是接口路径
            3、上面2个都可以是本地路径*/
        // String m3u8Url = "http://localhost:8080/upload/test.m3u8";
        // String infoUrl = "http://localhost:8080/preview/test.keyinfo";
        String m3u8Url = "D:\\Projects\\practice\\ffmpeg-demo\\test\\test.m3u8";
        String infoUrl = "D:\\Projects\\practice\\ffmpeg-demo\\test\\test.keyinfo";
        String segmentPattern = "http://localhost:8080/upload/test-%d.ts";
        FFmpegProcessor.convertMediaToM3u8ByHttp(inputStream, m3u8Url, infoUrl, segmentPattern);
    }

    @RequestMapping("convertToM3u8")
    public void convertToM3u8(MultipartFile mfile) {
        FFmpegProcessor.convertMediaToM3u8(mfile);
    }

 
    @PostMapping("upload/{fileName}")
    public void upload(HttpServletRequest request, @PathVariable("fileName") String fileName) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        FileWriter writer = new FileWriter(PATH + fileName);
        writer.writeFromStream(inputStream);
        IoUtil.close(inputStream);
    }
 
    /**
     * 预览加密文件
     */
    @PostMapping("preview/{fileName}")
    public void preview(@PathVariable("fileName") String fileName, HttpServletResponse response) throws IOException {
        FileReader fileReader = new FileReader(PATH + fileName);
        fileReader.writeToStream(response.getOutputStream());
    }
 
    /**
     * 预览加密文件
     */
    @GetMapping("download/{fileName}")
    public void download(@PathVariable("fileName") String fileName, HttpServletResponse response) throws IOException {
        FileReader fileReader = new FileReader(PATH + fileName);
        fileReader.writeToStream(response.getOutputStream());
    }
 
}

FFmpegProcessor

java 复制代码
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;


public class FFmpegProcessor {

    /**
     * 这个方法的url地址都必须是一样的类型 同为post
     */
    public static void convertMediaToM3u8ByHttp(InputStream inputStream, String m3u8Url, String infoUrl, String segmentPattern) throws IOException {

        avutil.av_log_set_level(avutil.AV_LOG_INFO);
        FFmpegLogCallback.set();

        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputStream);
        grabber.start();

        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(m3u8Url, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());

        recorder.setFormat("hls");
        // 拆分时间片段长度
        recorder.setOption("hls_time", "60");
        recorder.setOption("hls_list_size", "0");
        recorder.setOption("hls_flags", "delete_segments");
        recorder.setOption("hls_delete_threshold", "1");
        recorder.setOption("hls_segment_type", "mpegts");
        /* 这里指定生成的ts文件保存位置,可以写接口路径, 该接口用于接收ts文件流*/
        // recorder.setOption("hls_segment_filename", "http://localhost:8080/upload/test-%d.ts");
        recorder.setOption("hls_segment_filename", segmentPattern);
        recorder.setOption("hls_key_info_file", infoUrl);

        // http属性
        recorder.setOption("method", "POST");

        recorder.setFrameRate(25);
        recorder.setGopSize(2 * 25);
        recorder.setVideoQuality(1.0);
        recorder.setVideoBitrate(10 * 1024);
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
        /*
        // 只保存图像,而不保存声音
        recorder.start();
 
        Frame frame;
        while ((frame = grabber.grabImage()) != null) {
            try {
                recorder.record(frame);
            } catch (FrameRecorder.Exception e) {
                e.printStackTrace();
            }
        }
        recorder.setTimestamp(grabber.getTimestamp());
        recorder.close();
        grabber.close();*/

        /* 图像 + 声音 */
        recorder.start(grabber.getFormatContext());
        AVPacket packet;
        while ((packet = grabber.grabPacket()) != null) {
            try {
                recorder.recordPacket(packet);
            } catch (FrameRecorder.Exception e) {
                e.printStackTrace();
            }
        }
        recorder.setTimestamp(grabber.getTimestamp());
        recorder.stop();
        recorder.release();
        grabber.stop();
        grabber.release();
    }

    private static final String BASE_PATH = System.getProperty("user.dir") + "\\tmp\\";


    public static void convertMediaToM3u8(MultipartFile mfile) {

        String origFileName = mfile.getOriginalFilename();
        String fileName = origFileName.substring(0, origFileName.lastIndexOf("."));
        String dirName = fileName;
        String fileDir = BASE_PATH + fileName;
        File dirFile = new File(fileDir);
        if (!dirFile.exists()) {
            dirFile.mkdirs();
        }

        try {
            File rawFile = new File(dirFile, origFileName);
            // 保存文件
            mfile.transferTo(rawFile);

            // 生成密钥文件
            String commonFileName = fileDir + "\\" + fileName;
            generateKeyFile(commonFileName + ".key");

            // 生成keyInfo文件
            generateKeyInfoFile(dirName, commonFileName + ".key", commonFileName + ".keyinfo");

            convertMediaToM3u8ByHttp(new FileInputStream(rawFile),
                    commonFileName + ".m3u8",
                    commonFileName + ".keyinfo",
                    commonFileName + "-%d.ts");

        } catch (Exception e) {
            e.printStackTrace(System.err);
        }

    }

    /**
     * 生成keyInfo文件
     *
     * @param keyFilePath     密钥文件路径
     * @param keyInfoFilePath keyInfo文件路径
     */
    private static void generateKeyInfoFile(String dirName, String keyFilePath, String keyInfoFilePath) {
        try {
            // 生成IV
            ProcessBuilder ivProcessBuilder = new ProcessBuilder("openssl", "rand", "-hex", "16");
            Process ivProcess = ivProcessBuilder.start();
            BufferedReader ivReader = new BufferedReader(new InputStreamReader(ivProcess.getInputStream()));
            String iv = ivReader.readLine();

            // 写入keyInfo文件
            String keyInfoContent =
                    "http://127.0.0.1:8080/tmp/" + dirName + "/" + new File(keyFilePath).getName() + "\n"
                    + keyFilePath + "\n"
                    + iv;
            Files.write(Paths.get(keyInfoFilePath), keyInfoContent.getBytes());

            System.out.println("keyInfo文件已生成: " + keyInfoFilePath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成密钥文件
     *
     * @param keyFilePath 密钥文件路径
     */
    private static void generateKeyFile(String keyFilePath) {
        try {
            ProcessBuilder processBuilder = new ProcessBuilder("openssl", "rand", "16");
            processBuilder.redirectOutput(new File(keyFilePath));
            Process process = processBuilder.start();
            process.waitFor();
            System.out.println("密钥文件已生成: " + keyFilePath);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

App

java 复制代码
@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}

nginx配置

conf 复制代码
#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
	
	
	
	server {
		listen 80;
		server_name localhost;
		
		add_header 'Access-Control-Allow-Origin' $http_origin always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With, token';
        add_header 'Access-Control-Allow-Credentials' 'true';
		
		
		
		location / {
			if ($request_method = 'OPTIONS') {
				return 204;
			}
			root D:/Projects/practice/ffmpeg-demo/tmp;
		}
	}
	
	
}

player.html

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
        }

        .dplayer-container {
            width: 800px;
            height: 500px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        #dplayer {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
    <script src="https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js"></script>
</head>

<body>

    <div class="dplayer-container">
        <div id="dplayer"></div>
    </div>

    <hr />

    <script>
        // 另一种方式,使用 customType
        const dp = new DPlayer({
            container: document.getElementById('dplayer'),
            autoplay: false, // 自动播放

            video: {
                url: 'http://127.0.0.1/zzhua/zzhua.m3u8',
                type: 'customHls',
                customType: {
                    customHls: function (video, player) {
                        const hls = new Hls();
                        hls.loadSource(video.src);
                        hls.attachMedia(video);
                    },
                },
            },
           
        });


        Window.dp = dp;

    </script>
</body>

</html>

测试

上传1个301M的视频,耗时15s,

共188个文件,其中184个ts文件

播放效果

相关推荐
qq_12498707533 分钟前
Java+SpringBoot+Vue+数据可视化的综合健身管理平台(程序+论文+讲解+安装+调试+售后)
java·开发语言·spring boot·毕业设计
qq_124987075332 分钟前
Java+SpringBoot+Vue+数据可视化的美食餐饮连锁店管理系统
java·spring boot·毕业设计·美食
m0_748248231 小时前
Spring Framework 中文官方文档
java·后端·spring
Vacant Seat1 小时前
矩阵-矩阵置零
java·矩阵·二维数组
先睡1 小时前
Spring MVC的基本概念
java·spring·mvc
m0_748240541 小时前
Springboot项目:使用MockMvc测试get和post接口(含单个和多个请求参数场景)
java·spring boot·后端
CoderCodingNo1 小时前
【GESP】C++二级真题 luogu-b3865, [GESP202309 二级] 小杨的 X 字矩阵
java·c++
暗诺星刻1 小时前
Java 数学函数库
java·数学·函数·计算器·计算
Shuzi_master71 小时前
<02.21>八股文
java·开发语言
元亓亓亓1 小时前
java后端开发day18--学生管理系统
java·开发语言