FFmpeg+SpringBoot视频流接口无时长显示,无法拖拽播放求助
解决HTTP视频流无法快进/显示时长的问题
问题核心分析
当前实现将FFmpeg编码后的视频直接流式输出,存在三个关键缺陷:
- 未支持HTTP范围请求(Range Requests),播放器无法请求指定时间段内容,导致无法快进/后退。
- 未返回视频总时长、总大小等元数据,播放器无法渲染进度条和显示时长。
- FFmpeg参数设置偏向直播场景(
empty_moov),缺少可seek的索引信息。
解决方案步骤
1. 支持HTTP范围请求
修改Spring控制器,处理Range请求头,返回206 Partial Content响应,同时设置Accept-Ranges、Content-Range等必要响应头。
2. 预获取视频元数据
使用FFprobe获取原视频总时长,结合编码码率估算总大小,用于填充响应头,让播放器知晓视频整体信息。
3. 调整FFmpeg参数
移除empty_moov(适合无时长的直播场景),保留frag_keyframe+faststart,确保MP4的moov原子包含索引信息且位于文件头部,让播放器快速获取元数据。
4. 修改编码逻辑,支持起始位置编码
在FFmpeg命令中添加-ss参数,实现从指定时间点开始编码,响应播放器的Range请求。
代码修改示例
1. 新增FFprobe工具类获取元数据
import net.bramp.ffmpeg.probe.FFmpegProbeResult; import net.bramp.ffmpeg.probe.FFmpegStream; import net.bramp.ffmpeg.FFprobe; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class VideoMetadataService { private final FFprobe ffprobe; public VideoMetadataService() { this.ffprobe = FFprobe.atPath(); } // 获取视频总时长(秒) public double getVideoDuration(String videoPath) throws IOException { FFmpegProbeResult probeResult = ffprobe.probe(videoPath); FFmpegStream videoStream = probeResult.getStreams().stream() .filter(s -> s.codec_type.equals("video")) .findFirst() .orElseThrow(() -> new RuntimeException("No video stream found")); return videoStream.duration; } // 估算编码后的视频大小(基于码率和时长) public long getEstimatedEncodedSize(double duration, int videoBitrate, int audioBitrate) { long totalBitrate = (videoBitrate + audioBitrate) * 1000; return (long) (totalBitrate * duration) / 8; } }
2. 修改控制器支持Range请求
@RestController @RequestMapping("/api") @Log4j2 public class StreamController { @Autowired VideoStreamingService videoStreamingService; @Autowired VideoService videoService; @Autowired VideoMetadataService metadataService; private static final int VIDEO_BITRATE = 5000; // 对应FFmpeg的-b:v 5000k private static final int AUDIO_BITRATE = 320; // 对应FFmpeg的-b:a 320k @GetMapping("/stream/{videoId}") public ResponseEntity<StreamingResponseBody> streamVideo(@PathVariable Long videoId, @RequestParam(required = false) String codec, @RequestHeader(value = "Range", required = false) String range) throws IOException { Video video = videoService.findVideoById(videoId); if (video == null) { return ResponseEntity.notFound().build(); } Codec codecEnum = Codec.fromString(codec); double totalDuration = metadataService.getVideoDuration(video.getPath()); long totalSize = metadataService.getEstimatedEncodedSize(totalDuration, VIDEO_BITRATE, AUDIO_BITRATE); if (range == null) { // 完整视频请求 return ResponseEntity.ok() .contentType(MediaType.valueOf("video/mp4")) .header("Accept-Ranges", "bytes") .header("Content-Length", String.valueOf(totalSize)) .body(outputStream -> videoStreamingService.streamVideo(video.getPath(), outputStream, codecEnum, 0)); } else { // 处理Range请求 long startByte = parseRangeStart(range, totalSize); double startTime = (startByte * 8.0) / ((VIDEO_BITRATE + AUDIO_BITRATE) * 1000); long contentLength = totalSize - startByte; return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT) .contentType(MediaType.valueOf("video/mp4")) .header("Accept-Ranges", "bytes") .header("Content-Range", String.format("bytes %d-%d/%d", startByte, totalSize - 1, totalSize)) .header("Content-Length", String.valueOf(contentLength)) .body(outputStream -> videoStreamingService.streamVideo(video.getPath(), outputStream, codecEnum, startTime)); } } private long parseRangeStart(String range, long totalSize) { String[] parts = range.replace("bytes=", "").split("-"); long start = Long.parseLong(parts[0]); return Math.min(start, totalSize - 1); } }
3. 修改服务层支持起始时间编码
@Service public class VideoStreamingService { public void streamVideo(String videoPath, OutputStream outputStream, Codec codec, double startTime) { FFmpegBuilder builder = new FFmpegBuilder() .setInput(videoPath) .addExtraArgs("-ss", String.valueOf(startTime)) // 从指定时间点开始编码 .addExtraArgs("-b:v", "5000k") .addExtraArgs("-maxrate", "5000k") .addExtraArgs("-bufsize", "10000k") .addExtraArgs("-c:a", "aac") .addExtraArgs("-b:a", "320k") .addExtraArgs("-movflags", "frag_keyframe+faststart") // 移除empty_moov .setOutput(PipeOutput.pumpTo(outputStream)) .setFormat("mp4") .addExtraArgs("-nostdin"); if (codec == Codec.AMD) { builder.addExtraArgs("-profile:v", "high"); } builder.addExtraArgs("-c:v", codec.getFfmpegArgument()); FFmpeg ffmpeg = FFmpeg.atPath(); try { ffmpeg.run(builder); } catch (IOException | InterruptedException e) { log.error("Error streaming video", e); throw new RuntimeException(e); } } }
关键参数说明
-movflags frag_keyframe+faststart:faststart将MP4的moov原子(包含索引和元数据)移到文件开头,播放器加载初期即可获取时长和索引;frag_keyframe确保视频按关键帧分段,方便seek操作。- HTTP Range请求:播放器通过
Range头请求指定字节范围,服务器返回对应片段,实现快进/后退功能。 - FFprobe元数据:提前获取原视频时长,结合编码码率估算总大小,让播放器能渲染完整进度条。
内容的提问来源于stack exchange,提问作者lxluxo23
相关产品推荐
相关产品推荐

