Spring Boot - ffmpeg 获得 m3u8 列表和 ts 文件,前端请求视频流进行播放

发布时间 2023-10-31 23:27:41作者: Himmelbleu

安装 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;
        }
    }
}

上传视频

  1. 将客户端上传过来的视频存储到本地磁盘。获取已存储的视频目录地址。
  2. 调用 convert 转换视频为 m3u8 文件和视频的切片文件(ts 文件)。
  3. 返回一个视频路径,对接下面的接口,当浏览器请求这个接口时就会返回 m3u8 文件给客户端。

获取 m3u8

  1. 从请求中获取 filename 和 filepath,即 m3u8 存储的目录和 m3u8 的文件名。
  2. 读取文件二进制返回给客户端。

获取 ts

  1. 从请求中获取 filename,也就是前端请求 ts 的文件名称。
  2. 从我们转换完成的目录中获取 ts 文件。
  3. 读取文件二进制返回给客户端。

前端

<!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>Document</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>

源码

title:(m3u8 视频上传和播放源码) link:(https://gitee.com/Himmelbleu/java-learning) cover:(https://www.infocode.com.cn/blog/wp-content/uploads/2021/10/f8fba7a2f3c35d3d7c16892b38ba4785.jpg)