1.1.1 ffmpeg视频分片上传
前言
视频切片技术(如 HLS)允许用户在不下载整个视频文件的情况下进行播放,这意味着用户可以快速开始播放,并且能够更流畅地观看视频,特别是在网络条件不佳的情况下。这种技术也支持拖动进度条和倍速播放,从而提供更丰富的观看体验
新建一个空的springboot工程
环境:
Windows:11
Java:
- 亚马逊jdk17
- springboot3
- maven4
ffmpeg:
- 下载网址:https://ffmpeg.org/download.html
- 配置环境变量,检测方式:cmd输出
ffmpeg -version
,出现版本号则表示可用- 版本:N-116990-g4646a74d1e-20240911
前端:
服务器:
- Server version: Apache/2.4.55 (Win64)(非必须)
- 或 可用vscode插件
ffmpeg下载:
其中:会转跳到GitHub,需使用魔法,推荐Windows微软商店的Watt Toolkit(免费)加速
FFmpeg中文网链接https://ffmpeg.github.net.cn/
所需依赖:
<!--父依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>
<dependencies>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.2</version>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 视频处理相关依赖-->
<!--管理hutool版本 依情况引入-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bom</artifactId>
<version>5.8.27</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--核心依赖-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.27</version>
</dependency>
<!--ffmpeg依赖-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>6.1.1-1.5.10</version>
</dependency>
<!--javacv依赖-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.10</version>
</dependency>
<!-- 单元测试 非必须 依情况引入-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
项目结构:
其中:
- config目录中:WebConfig 文件非必须
- utils目录中:CreateDirectoryUtil 文件非必须
- main\resources目录中:application.yml 文件非必须
在VideoController中:
@Slf4j
@RestController
@RequestMapping("/video")
public class VideoController {
@GetMapping("/test")// 测试用接口
public String getVideoList(){
log.info("测试成功!");
return "成功";
}
}
运行springboot,浏览器网址栏输入:localhost:8080/video/test
,如页面上出现”成功“则spring boot服务启动成功,可进行下一步,如出现其他情况,请先研习springboot相关知识,或私信留言
准备info文件和key文件(非必须)
-
文件名及路径参考“项目结构”
-
文件信息:
-
Video.info:
以下三行分别对应(需准备相应请求地址):
- 外部访问key文件的地址
- 执行时访问key的地址
- 密钥
http://localhost:8080/video/preview/video.key http://localhost:8080/video/preview/video.key 682f5033538cf71567e1bdb38f5f9a07
-
Video.key:
n4DHLX7kMPeewvW3dGlm5i/EE8I
-
编写配置类:
FfmpegConfig:
-
方式一 通过配置文件注入:
-
配置文件:application.yml
# 配置相关信息 video: profiles: folder: 'E:\Code\JavaLearn\SpringBootTest\springboot-video\src\test\resources\profiles\' infoPath: 'E:\Code\JavaLearn\SpringBootTest\springboot-video\src\test\resources\profiles\Video.info' keyPath: 'E:\Code\JavaLearn\SpringBootTest\springboot-video\src\test\resources\profiles\Video.key' folder: path: 'E:\Code\JavaLearn\SpringBootTest\springboot-video\src\test\resources\videos\'
-
配置类:FfmpegConfig
@Data @Component public class FfmpegConfig { public static String PROFILES_FOLDER_PATH; public static String INFO_PROFILES_PATH;// 视频信息配置文件路径 public static String KEY_PROFILES_PATH;// 视频密钥配置文件路径 public static String VIDEOS_FOLDER;// 视频总文件夹 // 临时变量 @Value("${video.profiles.folder}") private String tmpProfilesFolderPath; @Value("${video.profiles.infoPath}") private String tmpInfoProfilesPath; @Value("${video.profiles.keyPath}") private String tmpKeyProfilesPath; @Value("${video.folder.path}") private String tmpVideoFolder; // 将配置文件中的值赋值给静态变量 @PostConstruct public void init(){ PROFILES_FOLDER_PATH = tmpProfilesFolderPath; INFO_PROFILES_PATH = tmpInfoProfilesPath; KEY_PROFILES_PATH = tmpKeyProfilesPath; VIDEOS_FOLDER = tmpVideoFolder; } }
-
-
方式二 硬编码:
@Data @Component public class FfmpegConfig { public static String PROFILES_FOLDER_PATH = "你的配置文件夹地址"; public static String INFO_PROFILES_PATH = "视频信息配置文件路径"; public static String KEY_PROFILES_PATH = "视频密钥配置文件路径"; public static String VIDEOS_FOLDER = "视频总文件夹"; }
需将配置文件中的地址替换成你自己的地址!
编写工具类
FfmpegUtil:
package ;
import com.itheima.config.FfmpegConfig;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
@Slf4j
public class FfmpegUtil {
public static void changeMediaToM3u8(InputStream inputStream, String m3u8Url, String infoUrl) throws IOException {
// 构造ts文件输出地址
String filePath = m3u8Url.substring(0, m3u8Url.lastIndexOf("\\") - 1);// 视频总文件夹路径
String tmp = m3u8Url.substring(m3u8Url.lastIndexOf("\\") + 1);
String fileName = tmp.substring(0, tmp.lastIndexOf("-"));// 文件夹名
// CreateDirectoryUtil.createDirectory(filePath);// 以下测试用
log.info("m3u8Url: {}", m3u8Url); // 打印完整的m3u8Url
log.info("filePath: {}", filePath); // 打印文件路径
log.info("tmp: {}", tmp);
log.info("folderName: {}", fileName); // 打印文件夹名称
log.info(filePath + "\\" + fileName +"-%d.ts");// 文件真实地址
avutil.av_log_set_level(avutil.AV_LOG_DEBUG);// 设置Ffmpeg日志级别为debug级别
FFmpegLogCallback.set();// 设置Ffmpeg日志回调
// 创建FfmpegFrameGrabber对象,用于从输入流中抓取帧
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputStream);
grabber.start();// 开始抓取
// 创建FfmpegFrameRecorder对象,用于录制M3U8格式的视频
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(
m3u8Url,// m3u8文件输出地址,也可设置为请求上传地址(需准备好请求api)
grabber.getImageWidth(),
grabber.getImageHeight(),
grabber.getAudioChannels());
recorder.setFormat("hls");// 设置输出格式为HLS(HTTP Live Streaming)
recorder.setOption("hls_time", "5");// 设置每个HLS片段的时间长度为5秒
recorder.setOption("hls_list_size", "0");// 设置播放列表中的片段数量,0表示不限制
recorder.setOption("hls_flags", "delete_segments");// 设置删除旧片段的标志
recorder.setOption("hls_delete_threshold", "1");// 设置删除片段的阈值
recorder.setOption("hls_segment_type", "mpegts");// 设置HLS片段的类型为mpegts
recorder.setOption("hls_segment_filename", filePath + "\\" + fileName +"-%d.ts");// 设置HLS片段的文件名模板,%d会被替换为序列号 ts文件输出地址
// recorder.setOption("hls_key_info_file", infoUrl);// 设置密钥信息文件的URL,酌情设置
// 设置请求上传方式为POST
// recorder.setOption("method", "POST");// 将输出片段通过请求上传时设置
recorder.setGopSize((int) grabber.getFrameRate()); // 尝试将 GOP 设置为帧率的整数倍
recorder.setFrameRate(grabber.getFrameRate()); // 确保帧率与源视频一致
recorder.setVideoQuality(1.0);// 设置视频质量(1.0表示无损)
recorder.setVideoBitrate(1920 * 1080);// 设置视频比特率,此设置可调清晰度
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);// 设置视频编码器为H264
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); // 设置音频编码器
recorder.setAudioChannels(grabber.getAudioChannels()); // 设置音频通道数
recorder.setSampleRate(grabber.getSampleRate()); // 设置音频采样率
recorder.start();// 开始录制
// 循环抓取帧并录制
Frame frame;
while ((frame = grabber.grab()) != null) {
if (frame.image != null) {
// 处理视频帧
recorder.record(frame);
} else if (frame.samples != null) {
// 处理音频帧
recorder.record(frame);
}
}
recorder.setTimestamp(grabber.getTimestamp());// 设置录制结束的时间戳
// 关闭录制器和抓取器
try {
recorder.close();
grabber.close();
} catch (FrameRecorder.Exception | FrameGrabber.Exception e) {
log.error("Error closing recorder or grabber", e);
}
}
// 重载方法,支持默认地址输出
public static void changeMediaToM3u8(InputStream inputStream) throws IOException {
changeMediaToM3u8(inputStream, FfmpegConfig.VIDEOS_FOLDER + "\\" + UUID.randomUUID() + "-video.m3u8", FfmpegConfig.INFO_PROFILES_PATH);
}
}
其中:
recorder.setOption("hls_key_info_file", infoUrl);
:在你没有配置密钥文件的时候不用设置
recorder.setOption("method", "POST");
:在你的m3u8Url
为本地路径的时候不用设置
其中:
博主路径(除info文件)均为本地地址
编写WebConfig(非必须):
目的:解决前端请求视频时跨域
WebConfig:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost") // 允许来自此源的请求
.allowedMethods("GET", "POST", "OPTIONS") // 允许的方法
.allowedHeaders("*"); // 允许的头部
}
}
其中:根据自己的前端服务器情况酌情配置:allowedOrigins("http://localhost")
补充controller:
@Slf4j
@RestController
@RequestMapping("/video")
public class VideoController {
@GetMapping("/test")// 测试用接口
public String getVideoList(){
log.info("测试成功!");
return "成功";
}
@PostMapping("/uploadToM3u8")
public void uploadToM3u8() throws Exception {
// 模拟一个已被服务器保存的MP4文件
FileInputStream inputStream = new FileInputStream("E:\\Code\\JavaLearn\\SpringBootTest\\springboot-video\\src\\test\\resources\\Video\\testVideo.mp4");
FfmpegUtil.changeMediaToM3u8(inputStream);// 进行切片保存
}
// @CrossOrigin(origins = "http://localhost")
@GetMapping("/preview/{fileName}")
public void preview(@PathVariable("fileName") String fileName, HttpServletResponse response) throws IOException {
FileReader fileReader = new FileReader(FfmpegConfig.PROFILES_FOLDER_PATH + fileName);
fileReader.writeToStream(response.getOutputStream());
}
// @CrossOrigin(origins = "http://localhost)
@GetMapping("/play/{file:.+}")
public ResponseEntity<Resource> play(@PathVariable String file) {
Path videoPath = Paths.get(FfmpegConfig.VIDEOS_FOLDER, file);
try {
// 检查文件是否存在且可读
Resource resource = new UrlResource(videoPath.toUri());
if (resource.exists() || resource.isReadable()) {
// 设置Content-Disposition头部,非必须
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"");
// 根据文件扩展名设置Content-Type头部,非必须
if (videoPath.toString().endsWith(".m3u8")) {
headers.setContentType(MediaType.parseMediaType("application/vnd.apple.mpegurl"));
} else if (videoPath.toString().endsWith(".ts")) {
headers.setContentType(MediaType.parseMediaType("video/mp2t"));
}
// 返回文件资源
return ResponseEntity.ok().headers(headers).body(resource);
} else {
log.error("文件不可读或不存在: {}", videoPath);
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
log.error("错误: {}", videoPath, e);
return ResponseEntity.badRequest().build();
}
}
}
其中:
preview():是对应key文件中的请求路径,不加密则此方法无用,当加密使用时注意:
@CrossOrigin(origins = "http://localhost")
用于解决跨域问题
其中:
WebConfig 和 @CrossOrigin(origins = "http://localhost)
注解二选一,作用:解决跨域问题
生成视频切片文件:
-
启动springboot
-
使用PostMan或者其他接口测试工具:
POST请求:http://localhost:8080/video/uploadToM3u8
此时ffmpeg开始工作,控制台输出信息,
src\test\resources\videos
目录产生相应文件
前端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HLS.js Example</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"></script>
</head>
<body>
<video id="video" controls width="990px" height="540px"></video>
<script>
var video = document.getElementById('video');
var hls = new Hls();
if (Hls.isSupported()) {
hls.attachMedia(video);
hls.loadSource('http://localhost:8080/video/play/c844fae5-7544-4872-b9a2-f63cfff63ae9-video.m3u8');
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = 'http://localhost:8080/video/play/c844fae5-7544-4872-b9a2-f63cfff63ae9-video.m3u8';
video.addEventListener('canplay', function() {
video.play(); }, false);
}
</script>
</body>
</html>
将html文件中m3u8文件名替换成生成的m3u8文件名
此时,运行你的前端服务器,就可以在网页中看到输出的视频了
如有其他疑问请私信或评论区
00后博主第一次写技术类文章,如有不对请嘴下留情,私信与评论区均可留言
文章不定时更新