学习链接
ffmpeg-demo 当前对应的 gitee代码
Spring boot视频播放(解决MP4大文件无法播放),整合ffmpeg,用m3u8切片播放。
springboot 通过javaCV 实现mp4转m3u8 上传oss
如何保护会员或付费视频?优酷是怎么做的? - HLS 流媒体加密
ffmpeg视频转切片m3u8并加密&videojs播放&hls.js播放&dplayer播放(弹幕效果)
video标签学习 & xgplayer视频播放器分段播放mp4
SpringBoot&FFmpeg实现上传视频到本地,使用M3U8切片转码后,下方使用hls.js播放(支持mp4&avi),SpringBoot + FFmpeg实现一个简单的M3U8切片转码系统
文章目录
- 学习链接
- 简介
- 效果图
- 代码
- pom.xml
- application.yml
- WebConfig
- TestController
- FFmpegProcessor
- App
- nginx配置
- player.html
- 测试
简介
将上传的视频文件,使用javacv拆分成m3u8文件和ts文件,m3u8文件和ts文件通过nginx访问,而key文件则通过web服务来获取。使用dplayer播放视频。
也可以使用ffmpeg命令来做,可以参考上面链接。
(当前能把mp4这样处理,但是不能处理avi,处理avi会报错;处理avi的可以参考springboot-ffmpeg-m3u8-convertor - gitee代码,或者这个springboot-ffmpeg-demo - gitee代码)
效果图
m3u8文件和ts文件通过nginx访问,而key文件则通过web服务来获取
拿不到key文件是无法播放的
代码
pom.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
server:port: 8080
spring:servlet:multipart:max-file-size: 500MBmax-request-size: 500MB
WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/test/**").addResourceLocations("file:" + System.getProperty("user.dir") + "/test/");registry.addResourceHandler("/tmp/**").addResourceLocations("file:" + System.getProperty("user.dir") + "/tmp/");}@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").maxAge(3600).allowCredentials(true).allowedOrigins("*").allowedMethods("*").allowedHeaders("*").exposedHeaders("token","Authorization");}
}
TestController
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
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 {// 生成IVProcessBuilder 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
@SpringBootApplication
public class App {public static void main(String[] args) {SpringApplication.run(App.class, args);}}
nginx配置
#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
<!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>// 另一种方式,使用 customTypeconst 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文件
播放效果