学习链接
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文件
播放效果
