[Spring Boot] ffmpeg 获得 m3u8 列表和 ts 文件,前端请求视频流进行播放
安装 ffmpeg
FFmpeg 下载地址:GitHub releases。请下载:ffmpeg-master-latest-win64-gpl-shared.zip 压缩包。
解压到你系统盘任意位置(前提是你以后找得到这玩意儿在哪)。
接下来就是配置其环境变量,所有的环境变量都是配置它的启动文件的路径到你系统的 Path,基本上都是(也有例外的?)。如 FFmpeg,就是复制其解压路径下的 bin 文件夹,到 Path 路径中。
VideoToM3u8AndTSUtil
file:[VideoToM3u8AndTSUtil.java]
/**
* @description:
* @package: com.example.m3u8
* @author: zheng
* @date: 2023/10/31
*/
public class VideoToM3u8AndTSUtil {
public static String getFilenameWithoutSuffix(String filename) {
int lastDotIndex = filename.lastIndexOf(".");
if (lastDotIndex > 0) {
return filename.substring(0, lastDotIndex);
} else {
return null;
}
}
public static boolean convert(String srcPathname, String destPathname) {
try {
ProcessBuilder processBuilder = new ProcessBuilder("ffmpeg", "-i", srcPathname, "-c:v", "libx264", "-hls_time", "60",
"-hls_list_size", "0", "-c:a", "aac", "-strict", "-2", "-f", "hls", destPathname);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
int exitCode = process.waitFor();
System.out.println("FFmpeg process exited with code: " + exitCode);
return true;
} catch (IOException | InterruptedException e) {
e.fillInStackTrace();
return false;
}
}
public static boolean write(InputStream inputStream, String filepath, String filename) throws IOException {
File file = new File(filepath, filename);
if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
return false;
}
OutputStream outputStream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.close();
inputStream.close();
return true;
}
}
covert 函数简单说明
需要传递两个参数,srcPathname 和 destPathname,即读取的原视频的目录和存放 m3u8 文件的目录。转换完成之后返回一个布尔值进行判断是否转换成功。
war:[start]
需要注意的是,destPathname 的目录必须要存在,如,你存放 m3u8 的文件目录是 E:\Videos\m3u8s
,那么该目录就必须提前存在。
war:[end]
VideoController
需要三个接口,上传视频、获取 m3u8 文件、获取 ts 文件。我这里就没有写 Service 类,而是直接写在接口里面的。
file:[VideoController.java]
/**
* @description:
* @package: com.example.m3u8
* @author: zheng
* @date: 2023/10/28
*/
@RestController
@RequestMapping("/video")
public class VideoController {
@PostMapping("/upload")
public ResponseEntity<String> upload(MultipartFile file) {
if (file == null) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
if (file.isEmpty()) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
try {
boolean written = VideoToM3u8AndTSUtil.write(file.getInputStream(), "E:/Type Files/Videos/Captures/videos/", file.getOriginalFilename());
if (!written) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
String srcPathname = "E:/Type Files/Videos/Captures/videos/" + file.getOriginalFilename();
String filename = VideoToM3u8AndTSUtil.getFilenameWithoutSuffix(Objects.requireNonNull(file.getOriginalFilename()));
String destPathname = "E:/Type Files/Videos/Captures/m3u8s/" + filename + ".m3u8";
boolean converted = VideoToM3u8AndTSUtil.convert(srcPathname, destPathname);
if (!converted) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok("http://localhost:8080/video/m3u8?filepath=E:/Type Files/Videos/Captures/m3u8s&filename=" + filename + ".m3u8");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@GetMapping("/m3u8")
public ResponseEntity<byte[]> getM3U8Content(@RequestParam String filepath, @RequestParam String filename) {
try {
File file = new File(filepath, filename);
if (file.exists()) {
// 读取M3U8文件内容
FileInputStream fileInputStream = new FileInputStream(file);
byte[] data = new byte[(int) file.length()];
fileInputStream.read(data);
fileInputStream.close();
// 设置响应头为M3U8类型
return ResponseEntity.ok()
.contentType(MediaType.valueOf("application/vnd.apple.mpegurl"))
.body(data);
} else {
return ResponseEntity.notFound().build();
}
} catch (IOException e) {
e.fillInStackTrace();
return ResponseEntity.notFound().build();
}
}
@GetMapping("/{filename}")
public ResponseEntity<byte[]> getTSContent(@PathVariable String filename) {
try {
File file = new File("E:/Type Files/Videos/Captures/m3u8s/", filename);
if (file.exists()) {
// 读取TS文件内容
FileInputStream fileInputStream = new FileInputStream(file);
byte[] data = new byte[(int) file.length()];
fileInputStream.read(data);
fileInputStream.close();
// 设置响应头为TS文件类型
return ResponseEntity.ok()
.contentType(MediaType.valueOf("video/mp2t"))
.body(data);
} else {
return ResponseEntity.notFound().build();
}
} catch (IOException e) {
e.fillInStackTrace();
return null;
}
}
}
上传视频
- 将客户端上传过来的视频存储到本地磁盘。获取已存储的视频目录地址。
- 调用 convert 转换视频为 m3u8 文件和视频的切片文件(ts 文件)。
- 返回一个视频路径,对接下面的接口,当浏览器请求这个接口时就会返回 m3u8 文件给客户端。
获取 m3u8
- 从请求中获取 filename 和 filepath,即 m3u8 存储的目录和 m3u8 的文件名。
- 读取文件二进制返回给客户端。
获取 ts
- 从请求中获取 filename,也就是前端请求 ts 的文件名称。
- 从我们转换完成的目录中获取 ts 文件。
- 读取文件二进制返回给客户端。
前端
file:[VideoPlayer.html]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<title>VideoPlayer</title>
</head>
<body>
<video style="width: 800px; height: 500;" id="video" controls></video>
<script>
const video = document.getElementById("video");
const videoSrc =
"http://localhost:8080/video/m3u8?filepath=E:/Type Files/Videos/Captures/m3u8s&filename=视频.m3u8";
if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = videoSrc;
} else if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(videoSrc);
hls.attachMedia(video);
}
</script>
</body>
</html>