Spring boot集成ffmpeg实现视频极速播放

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下载:

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后博主第一次写技术类文章,如有不对请嘴下留情,私信与评论区均可留言
文章不定时更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值