springboot编写mp4视频播放接口

文章介绍了如何通过SpringBoot实现视频文件的流式传输,包括简单的直接读取文件方式以及支持断点续传的优雅方式。断点续传涉及到HTTP的Range头,需要设置正确的Content-Range响应头和206响应码。同时,文章讨论了针对不同浏览器的兼容性问题,特别是处理不同浏览器对Range请求头的处理差异,以确保视频能在各种环境下正常播放。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简单粗暴方式

直接读取指定文件,用文件流读取视频文件,输出到响应中

    @GetMapping("/display1/{fileName}")
    public void displayMp41(HttpServletRequest request, HttpServletResponse response,@PathVariable("fileName") String fileName) throws IOException {
        File file=new File("D:/Download/"+fileName+".mp4");
        if(!file.exists()){
            response.getOutputStream().close();
            return;
        }
        InputStream inStream=new FileInputStream(file);
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inStream.read(buffer)) != -1) {
            response.getOutputStream().write(buffer, 0, len);
        }
        inStream.close();
        response.getOutputStream().flush();
        response.getOutputStream().close();
    }

这种方式很尴尬,可以播放视频,然而你会发现视频自带的进度条无法拖动。。。。。。。,只能暂停播放,没办法前进,也没办法后退。。。。。。

高端优雅方式

需要加一个断点续传的规范,实现很简单

注:如果你是用的h5原生的<video>,请求头会有一个Range: bytes=589824-,表示该请求希望返回的数据是从589824位置开始即可。

实现一共两点:

(1)响应头部添加如下格式响应头

Content-Range: bytes 589824-32153693/32153694

大概就是:Content-Range: bytes 请求头指定的开始字节数-本次返回的文件字节位置/总共多少字节,值得注意的是,本次返回的文件字节位置一定要比总字节数至少少一个字节,否则视频缓存结束的最后一次数据无法播放,可能是浏览器出了异常,视频会重新播放。本次返回的文件字节位置可以不准,因为浏览器会自动记录真实拿到的字节数量,但一定要少一个字节。

(2)响应码改为206

有了这两点就可以实现正常的视频播放接口了。

优化

考虑到视频的进度条很大概览是会被拖来拖去的,导致频繁请求接口。

假设你的文件200MB,频繁的请求每次都会把整个文件读入http流中,如果用户网速慢,或者浏览器的缓存策略会阻塞http请求,慢慢从http响应中读取这部分数据,这可能就会使数据都堆积到服务器内存里(本人毫无根据瞎想的),浪费资源。

(1)因为需要指定字节位置读取视频文件,使用随机读取RandomAccessFile类来操作。

(2)既然支持分段获取数据,不如每次返回定量的字节数即可。我这里设置成每次获取1MB,浏览器播放完了会自动接着调用。根据实际情况考虑,内网环境使用可以设大一些,如果数值设置的小,这请求频率会变的很多,得不偿失。

    @GetMapping("/display/{fileName}")
    public void displayMp4(HttpServletRequest request, HttpServletResponse response, @PathVariable("fileName") String fileName) throws IOException {
        File file = new File("D:/Download/" + fileName + ".mp4");
        if (!file.exists()) {
            response.getOutputStream().close();
            return;
        }
        String range = request.getHeader("Range");
        long lenStart = 0;
        if (range != null && range.length() > 7) {
            range = range.substring(6, range.length() - 1);
            lenStart = Long.parseLong(range);
        }
        int size = 1048576;
        response.setHeader("Content-Range", "bytes " + lenStart + "-" + ((file.length() - lenStart-2 < size)?file.length()-1:lenStart+size- 1) +"/" + file.length());
        response.setHeader("Content-Type", "video/mp4");
        response.setStatus(HttpStatus.PARTIAL_CONTENT.value());//响应码206表示响应内容为部分数据,需要多次请求
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        randomAccessFile.seek(lenStart);//设置读取的开始字节数
        //视频每次返回一兆数据
        byte[] buffer = new byte[size];
        int len = randomAccessFile.read(buffer);
        if (len != -1) {
            response.getOutputStream().write(buffer, 0, len);
        }
        randomAccessFile.close();
        response.getOutputStream().flush();
        response.getOutputStream().close();
    }

跨平台测试优化

上述的方式对于pc端是没有问题的,手机上的谷歌浏览器,火狐浏览器,edge浏览器都没问题,但小米浏览器和夸克不能正常播放,都是播放几秒然后就加载失败了。。。。于是将测试的视频放到普通的web服务器上直接访问,这俩浏览器没问题,那一定是接口兼容性的问题。

通过抓包,不断测试分析,最后大概摸清了问题所在:

(1)对于普通的web服务器是可以用cookie的,浏览器通过cookie能够信任服务器的连接,有计划的指定获取字节数范围,提供给服务器起始和结束两个字节的位置

(2)springboot可能配置的原因,不会记录cookie,导致浏览器某些原因不会有计划的分段获取,而是会给服务端一个自己当前已保存到的字节位置,希望服务器能够每次都返回剩余的全部字节数。然后浏览器自己来确定要多少。

所以,当请求头只指明了起始位置,但是没有结束的位置,就是需要服务端把所有数据都给返回。

如果指明了起始位置,也指明了结束位置,就需要按照要求返回对应的字节数。

这里需要注意的是,content-length要在返回数据之前预先加到响应头上,这个是可以计算出来的,计算要准确,不加上这个也会出兼容性问题。

有了上述这些条件,基本上在各个浏览器就可以正常的视频播放了,如果想通过该接口直接下载视频,还做不到,因为普通下载好像不会用这套协议,需要判断请求头,有Range可能就是播放视频的,没有就是下载视频的,用普通的流输出文件即可。

 /**
     * 读取视频文件
     */
    @GetMapping("/display/{fileName}")
    public void displayMp4(HttpServletRequest request, HttpServletResponse response, @PathVariable("fileName") String fileName) throws IOException {
        File file = new File("/usr/local/nginx/html/video/" + fileName);
        if (!file.exists()||!file.getName().endsWith(".mp4")) {
            response.getOutputStream().close();
            return;
        }
        String range = request.getHeader("Range");
        log.info("Range:" + range);
        if (range != null && range.length() > 7) {
            log.info("该请求符合断点续传");
            range = range.substring(6);
            String[] arr = range.split("-");
            long lenStart = Long.parseLong(arr[0]);
            long end=0;
            if (arr.length > 1) {
                end = Long.parseLong(arr[1]) ;
            }
            long contentLength=end>0?(end-(lenStart-1)):(file.length()-(lenStart>0?lenStart-1:0));//如果指定范围,就返回范围数据长度,如果没有就返回剩余全部长度
            response.setHeader("Content-Length", String.valueOf(contentLength));
//            response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");//加上会报错,不能用中文
            response.setHeader("Content-Range", "bytes " + lenStart + "-" + (end>0?end:(file.length() - 1)) + "/" + file.length());
            response.setContentType("video/mp4");
            response.setHeader("Accept-Ranges", "bytes");
            response.setStatus(HttpStatus.PARTIAL_CONTENT.value());//响应码206表示响应内容为部分数据,需要多次请求
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
            randomAccessFile.seek(lenStart);//设置读取的开始字节数
            if(end>0){//客户端指定了范围的数据,那就只给范围数据
                int size= (int) (end-lenStart+1);
                byte[] buffer = new byte[size];
                int len = randomAccessFile.read(buffer);
                if (len != -1) {
                    response.getOutputStream().write(buffer, 0, len);
                }
            }else{//没有指定范围
                //视频每次返回一兆数据
                int size = 1048576;//1MB
                byte[] buffer = new byte[size];
                int len ;
                while ((len = randomAccessFile.read(buffer)) != -1) {
                    response.getOutputStream().write(buffer, 0, len);
                }
            }
            randomAccessFile.close();
        }else{
            log.info("该请求不符合断点续传");
            response.setHeader("Content-Disposition", "attachment; filename=\"" +System.currentTimeMillis()+".mp4" + "\"");//不能用中文
            response.setHeader("Content-Length", String.valueOf(file.length()-1));
            response.setHeader("Content-Range", "" + (file.length()-1));
            response.setHeader("Accept-Ranges", "bytes");
            InputStream inStream=new FileInputStream(file);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inStream.read(buffer)) != -1) {
                response.getOutputStream().write(buffer, 0, len);
            }
            inStream.close();
        }
        response.getOutputStream().flush();
        response.getOutputStream().close();
    }

 附上测试的vue代码,当然里面有丰富的video的监听事件

<template>
  <div>
    <video ref="video"
           controls
           @loadedmetadata="loadedmetadata"
           @canplay="canplay"
           @waiting="waiting"
           @timeupdate="timeupdate"
           @play="play"
           @pause="pause"
           @ended="ended"
           style="width: 400px;height: 200px;"
          >
      <source :src="getMp4Url(displayName)"  type="video/mp4">
        您的浏览器不支持 HTML5 video 标签。
    </video>
    <div>当前时长:{{formatTime(nowTime)}}</div>
    <div>总时长:{{formatTime(totalTime)}}</div>
    <div>
      <button @click="playPause">{{!displayStatus?'播放':'暂停'}}</button>
    </div>
  </div>

</template>

<script>
import config from "@/config";
export default {
  name: "VideoIndex",
  data(){
    return{
      displayName: '最伟大的作品',
      displayStatus:false,
      nowTime: 0,//当前正在播放的时间,单位:秒,带三位小数
      totalTime:0,//视频的总长度,单位:秒,带三位小数
      videoWidth:0,//视频宽度
      videoHeight:0,//视频宽度
    }
  },
  mounted(){
    // this.$refs.video.onloadstart =(e)=> {
    //   //在浏览器开始寻找指定视频/音频(audio/video)触发
    //   console.log("onloadstart",e)
    // }
    // this.$refs.video.onprogress =(e)=> {
    //   //在浏览器下载指定的视频/音频(audio/video)时触发
    //   console.log("onprogress",e)
    // }
    // this.$refs.video.ondurationchange =(e)=> {
    //   //事件在视频/音频(audio/video)的时长发生变化时触发
    //   console.log("ondurationchange",e)
    // }
    // this.$refs.video.onloadeddata =(e)=> {
    //   //事件在当前帧的数据加载完成且还没有足够的数据播放视频/音频(audio/video)的下一帧时触发
    //   console.log("onloadeddata",e)
    // }
    // this.$refs.video.oncanplaythrough =(e)=> {
    //   //可以正常播放且无需停顿和缓冲时触发
    //   console.log("oncanplaythrough",e)
    // }
  },
  methods:{
    getMp4Url(name){
      return config.BASE_URL+"/video/display/"+name
    },
    playPause(){//播放状态切换
      if(this.$refs.video.paused){
        this.$refs.video.play();
      }else{
        this.$refs.video.pause();
      }
    },
    waiting(){//转圈的时候才会调用,秒加载好像不会触发
      console.log("加载中");
    },
    loadedmetadata(){
      this.totalTime=this.$refs.video.duration;
      console.log("获取视频总时间长度:"+this.formatTime(this.totalTime));
    },
    canplay(){
      //表示视频已经加载好了
      //这可以获取视频真是高度和宽度,
      this.videoWidth=this.$refs.video.videoWidth
      this.videoHeight=this.$refs.video.videoHeight
      console.log("视频已准备好了,可以播放,宽度:"+this.videoWidth+",高度:"+this.videoHeight)
    },
    play(){
      this.displayStatus=true;
      console.log("开始播放");
    },
    pause(){
      console.log("暂停播放");
      this.displayStatus=false;
    },
    ended(){
      console.log("播放结束");
    },
    timeupdate(){ //播放的时间戳更新
      this.nowTime=this.$refs.video.currentTime
    },
    formatTime(time){
      let temp=time; //302.432s
      let s= Math.ceil(temp%60); //0.01会进位+1
      temp=temp/60;
      let m=Math.floor(temp%60);
      let h=Math.floor(temp/60);
      return `${h>9?h:("0"+h)}:${m>9?m:("0"+m)}:${s>9?s:("0"+s)}`
    },

  },
}
</script>

<style scoped>

</style>

### 创建 Spring Boot 应用程序中的视频上传接口 #### 准备工作 为了创建一个能够处理视频上传的Spring Boot应用程序,需要确保环境配置正确。这包括使用Java 17以及Spring Boot版本3.0来初始化一个新的Spring Boot项目[^2]。 #### 添加依赖项 在`pom.xml`文件中加入必要的Maven依赖以支持多部分文件上传: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 如果计划保存文件至本地 --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.8.0</version> </dependency> ``` #### 编写控制器代码 定义一个RESTful API端点用于接收客户端发送过来的大尺寸媒体文件(如MP4)。这里提供了一个简单的例子说明如何编写这样的服务方法[^1]: ```java import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; @RestController @RequestMapping("/api/video") public class VideoUploadController { private static final String UPLOAD_DIR = "uploads/"; @PostMapping("/upload") public ResponseEntity<String> uploadVideo(@RequestParam("file") MultipartFile file){ try { if (!file.isEmpty()) { File dir = new File(UPLOAD_DIR); if (!dir.exists()){ dir.mkdirs(); } byte[] bytes = file.getBytes(); Path path = Paths.get(UPLOAD_DIR + file.getOriginalFilename()); Files.write(path, bytes); return ResponseEntity.ok("File uploaded successfully"); }else{ return ResponseEntity.badRequest().body("Please select a file to upload."); } } catch (IOException e) { e.printStackTrace(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); } } } ``` 此段代码实现了基本的功能——接收到POST请求后会尝试读取并保存传入的多媒体数据流到指定目录下;如果遇到任何错误,则返回相应的HTTP状态码给调用者告知失败原因。 #### 处理大文件上传设置 对于较大的视频文件,默认情况下Tomcat服务器可能会因为超时或者其他限制而拒绝接受这些请求。因此,在application.properties或yml配置文件里适当调整参数是非常重要的: ```properties # application.properties spring.servlet.multipart.max-file-size=50MB spring.servlet.multipart.max-request-size=50MB server.tomcat.connection-timeout=60000 ``` 以上配置允许单个文件最大可达50兆字节,并给予足够的连接等待时间以便完成整个传输过程[^4]。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值