SpringBoot结合Vue 播放 m3u8 格式视频

1、将MP4转M3U8格式

使用ffmpeg 命令

基于命令

ffmpeg -i 1.mp4 -c:v copy -c:a copy -strict -2 -f hls -hls_time 5 -hls_list_size 0 -hls_segment_filename %04d.ts index.m3u8

具体java 工具类如下:

复制代码
import lombok.extern.slf4j.Slf4j;

import java.io.*;

/***
 *
 * @author xuancg
 * @date 2025/2/25
 */
@Slf4j
public class FfmpegUtil {

    public static void main(String[] args) {
        File mp4 = new File("C:\\Users\\BHL\\Desktop\\2025-09-20-10-00-48-0.mp4");
        File dir =  new File("C:\\Users\\BHL\\Desktop\\1");
        System.out.println(convertM3u8(mp4.getAbsolutePath(), dir.getAbsolutePath()));
    }

    public static boolean doExeCommd(String cmd) {
        return new FfmpegUtil().exec(cmd);
    }

    /**
     * 转换为m3u8
     * ffmpeg -i 1.mp4 -c:v copy  -c:a copy  -strict -2  -f hls  -hls_time 5  -hls_list_size 0  -hls_segment_filename %04d.ts  index.m3u8
     * @param file 原始视频文件的绝对路径
     * @param dir 转换后的m3u8文件存放的目录
     */
    public static boolean convertM3u8(String file, String dir) {
        String cmd = String.format("ffmpeg -i \"%s\" -c:v copy  -c:a copy  -strict -2  -f hls  -hls_time 5  -hls_list_size 0  " +
                        " -hls_segment_filename \"%s%s \"%s/index.m3u8\" ", file, dir, "/%04d.ts\"", dir);
        return doExeCommd(cmd);
    }

    private  boolean exec(String cmd) {
        log.info("待执行的命令:" + cmd);
        try {
            Runtime rt = Runtime.getRuntime();
            Process proc = rt.exec(cmd);
            InputStream stderr = proc.getErrorStream();
            InputStreamReader isr = new InputStreamReader(stderr);
            BufferedReader br = new BufferedReader(isr);
            String line = null;
            while ((line = br.readLine()) != null){
                log.debug("执行结果" + line);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}

2、下载m3u8文件

通用文件下载接口

注意:

针对m3u8格式需要定义特殊的contentType

response.setHeader("Content-Type", "application/vnd.apple.mpegurl");

具体代码如下:

复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
 

/**
 * 通用请求处理
 *
 * @author xuancg
 */
@RestController
@RequestMapping("/profile")
@Slf4j
public class ProfileController {

    @Resource
    private MinioFileService minioFileService;


    @GetMapping("/**")
    public void profile(HttpServletRequest request, HttpServletResponse response) {
        String path = request.getServletPath();
        if(path.startsWith("/profile")){
            path = path.substring(8);
        }
        // TODO minio下载,依据实际自定义
        File tempFile = minioFileService.download(path);
        if(null != tempFile && tempFile.isFile()){
            preview(tempFile, response);
        }
    }


    public void preview(File file, HttpServletResponse response) {
        try(InputStream ins = new FileInputStream(file);
            ServletOutputStream outStream = response.getOutputStream()) {
            String fileName = file.getName();
            // 替换文件名中的非数字、字母、点、下划线
            fileName = fileName.replaceAll("[^0-9a-zA-Z._-]", "");
            // 设置响应头
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);

            response.setContentLength(ins.available());
            // 如果是m3u8 格式 设置
            if(fileName.endsWith(".m3u8")){
                response.setHeader("Content-Type", "application/vnd.apple.mpegurl");
            } else{
                response.setContentType(FileUtils.getContentType(file.getName()));
            }

            // 写入文件内容
            byte[] buffer = new byte[4096];
            int bytesRead = -1;
            while ((bytesRead = ins.read(buffer)) != -1) {
                outStream.write(buffer, 0, bytesRead);
            }
            outStream.flush();
        } catch (Exception e) {
            log.error("下载失败", e);
        }
    }


}

3、创建m3u8播放器

3.1 引入依赖

npm install video.js videojs-contrib-hls --save

3.2 定义播放器

在 component/M3u8Player文件夹下

创建 index.vue 文件

复制代码
<template>
    <div class="m3u8-player-container">
      <div class="player-wrapper">
        <video 
          ref="videoPlayer" 
          class="video-js vjs-big-play-centered" 
          controls
          :width="width"
          :height="height"
        >
          <p class="vjs-no-js">
            请启用JavaScript以观看视频
          </p>
        </video>
      </div>
      
      <!-- <div class="control-panel" v-if="showControlPanel">
        <div class="form-group">
          <label for="m3u8Url">M3U8地址:</label>
          <input 
            type="text" 
            id="m3u8Url" 
            v-model="videoUrl" 
            placeholder="输入M3U8视频地址"
            class="url-input"
          >
        </div>
        <button 
          @click="loadVideo" 
          class="load-button"
          :disabled="!videoUrl"
        >
          加载视频
        </button>
      </div> -->
      
      <div class="status-message" v-if="statusMessage">
        {{ statusMessage }}
      </div>
    </div>
  </template>
  
  <script>
  import videojs from 'video.js';
  import 'video.js/dist/video-js.css';
  import 'videojs-contrib-hls';
  
  export default {
    name: 'M3u8Player',
    props: {
      // 视频地址
      videoUrl: {
        type: String,
        default: ''
      },
      // 播放器宽度
      width: {
        type: String,
        default: '100%'
      },
      // 播放器高度
      height: {
        type: String,
        default: 'auto'
      },
      // 是否显示控制面板
      showControlPanel: {
        type: Boolean,
        default: true
      }
    },
    data() {
      return {
        player: null,
        statusMessage: '',
        baseUrl: process.env.VUE_APP_BASE_API,
      };
    },
    mounted() {
      // 初始化播放器
      this.initPlayer();
      
      // 如果有初始视频地址,自动加载
      if (this.videoUrl) {
        this.loadVideo();
      }
    },
    beforeUnmount() {
      // 销毁播放器实例
      if (this.player) {
        this.player.dispose();
        this.player = null;
      }
    },
    methods: {
      // 初始化播放器
      initPlayer() {
        const videoElement = this.$refs.videoPlayer;
        
        // 创建播放器实例
        this.player = videojs(videoElement, {
          autoplay: false,
          controls: true,
          responsive: true,
          fluid: true,
          sources: []
        });
        
        // 监听错误事件
        this.player.on('error', (error) => {
          console.error('视频播放错误:', error);
          this.statusMessage = '视频播放出错,请检查地址是否正确';
        });
        
        // 监听加载事件
        this.player.on('loadedmetadata', () => {
          this.statusMessage = '';
        });
      },
      
      // 加载视频
      loadVideo() {
        if (!this.player) {
          this.initPlayer();
        }
        
        if (!this.videoUrl) {
          this.statusMessage = '请输入有效的M3U8地址';
          return;
        }
        
        // 显示加载状态
        this.statusMessage = '正在加载视频...';
        
        // 设置视频源
        this.player.src({
          src: this.baseUrl + this.videoUrl,
          type: 'application/x-mpegURL' // M3U8格式
        });
        
        // 尝试加载视频
        this.player.load();
      }
    },
    watch: {
      // 监听视频地址变化,自动加载新视频
      videoUrl(newVal) {
        if (newVal && this.player) {
          this.loadVideo();
        }
      }
    }
  };
  </script>
  
  <style scoped>
  .m3u8-player-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
  }
  
  .player-wrapper {
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    background-color: #000;
  }
  
  .control-panel {
    margin-top: 20px;
    display: flex;
    gap: 10px;
    align-items: center;
  }
  
  .form-group {
    flex: 1;
  }
  
  .url-input {
    width: 100%;
    padding: 8px 12px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 14px;
  }
  
  .load-button {
    padding: 8px 16px;
    background-color: #42b983;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: background-color 0.3s;
  }
  
  .load-button:disabled {
    background-color: #cccccc;
    cursor: not-allowed;
  }
  
  .load-button:not(:disabled):hover {
    background-color: #359e75;
  }
  
  .status-message {
    margin-top: 10px;
    padding: 8px;
    border-radius: 4px;
    color: #666;
    font-size: 14px;
  }
  </style>

3.3 加载组件

修改main.js 文件

复制代码
import M3u8Player from '@/components/M3u8Player'

Vue.component('M3u8Player', M3u8Player)

4、加载播放

<m3u8-player

:videoUrl="url"

width="100%"

:showControlPanel="true"

/>

5、补充Nginx 代理配置

可以使用本地磁盘 方式 alias /etc/nginx/hls;

或者使用远程资源下载 proxy_pass http://192.168.1.1:8080/profile/hls;

其中 hls 为自定义路径

复制代码
location /profile/hls {
				proxy_pass    http://192.168.1.1:8080/profile/hls;
				# alias /etc/nginx/hls/;  # 注意:使用 alias,不是 root
				proxy_set_header  Host            $host;
				proxy_set_header  X-Real-IP       $remote_addr;
				proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
				
				add_header Cache-Control no-cache;
				add_header Access-Control-Allow-Origin *;
				add_header Access-Control-Allow-Methods GET;

				# 关键:设置正确的 MIME 类型
				types {
					application/vnd.apple.mpegurl m3u8;
					video/MP2T ts;
				}
				default_type application/vnd.apple.mpegurl;
		}
相关推荐
EasyDSS2 小时前
超越单向观看:视频直播点播视频会议平台EasyDSS如何赋能远程实时音视频互动场景?
音视频·实时音视频
嘀咕博客3 小时前
Kimi-Audio:Kimi开源的通用音频基础模型,支持语音识别、音频理解等多种任务
人工智能·音视频·语音识别·ai工具
Ai工具分享5 小时前
家庭录像损坏了无法播放?视频修复让回忆重现
音视频
红米饭配南瓜汤15 小时前
WebRTC 发送端 SSRC 生成流程总结
网络·网络协议·音视频·webrtc·媒体
技术小成1 天前
大黄蜂云课堂vep格式加密视频录屏截图翻录转换为mp4
音视频
EasyCVR1 天前
视频融合平台EasyCVR在智慧工地中的应用:构建安全、智能、高效的“云上工地”
安全·音视频
xiaopengbc1 天前
视频媒体影音嗅探神器—Chrome扩展插件(猫抓cat-catch离线版下载)
chrome·音视频·媒体
小椿_1 天前
AI 驱动视频处理与智算革新:蓝耘MaaS释放海螺AI视频生产力
人工智能·深度学习·音视频
2401_872990531 天前
【工具记录分享】提取bilibili视频字幕
音视频