Java实现基于阿里云OSS和FFmpeg的实时视频点播服务器
针对你的需求(阿里云OSS存储视频文件,阿里云ECS部署服务,实时使用FFmpeg处理而不提前转码),我将提供一个完整的解决方案。
方案概述
- 架构:前端请求 → Java服务验证 → 动态调用FFmpeg处理 → 返回处理后的流
- 特点:
- 不需要预先转码所有视频
- 按需实时处理
- 利用阿里云内网带宽(OSS与ECS在同一区域)
- 支持HLS(TS)格式输出
1. 环境准备
1.1 安装FFmpeg
在ECS服务器上安装FFmpeg:
# 对于Ubuntu/Debian
sudo apt-get update
sudo apt-get install ffmpeg
# 对于CentOS/RHEL
sudo yum install epel-release
sudo yum install ffmpeg ffmpeg-devel
1.2 添加阿里云OSS SDK依赖
<!-- Maven依赖 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
2. 核心实现代码
2.1 配置类
@Configuration
public class OSSConfig {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
@Bean
public OSS ossClient() {
return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}
@Bean
public String bucketName() {
return bucketName;
}
}
2.2 视频处理服务
@Service
public class VideoStreamingService {
private static final Logger logger = LoggerFactory.getLogger(VideoStreamingService.class);
@Autowired
private OSS ossClient;
@Autowired
private String bucketName;
// 临时处理目录
private final String tempDir = System.getProperty("java.io.tmpdir") + "/video_stream/";
public VideoStreamingService() {
// 创建临时目录
new File(tempDir).mkdirs();
}
/**
* 获取视频流并转换为HLS格式
* @param ossKey OSS上的视频文件路径
* @param segmentDuration 每个TS片段的持续时间(秒)
* @return 包含m3u8和ts文件的临时目录
*/
public File processVideoToHLS(String ossKey, int segmentDuration) throws IOException, InterruptedException {
// 1. 下载视频到临时文件
File inputFile = new File(tempDir + "input_" + System.currentTimeMillis());
try (InputStream ossStream = ossClient.getObject(bucketName, ossKey).getObjectContent();
FileOutputStream fileStream = new FileOutputStream(inputFile)) {
IOUtils.copy(ossStream, fileStream);
}
// 2. 创建输出目录
File outputDir = new File(tempDir + "hls_" + System.currentTimeMillis());
outputDir.mkdirs();
// 3. 构建FFmpeg命令
String outputPattern = outputDir.getAbsolutePath() + "/segment_%03d.ts";
String m3u8File = outputDir.getAbsolutePath() + "/playlist.m3u8";
List<String> command = new ArrayList<>();
command.add("ffmpeg");
command.add("-i"); command.add(inputFile.getAbsolutePath());
command.add("-c:v"); command.add("libx264"); // H.264编码
command.add("-c:a"); command.add("aac"); // AAC音频
command.add("-f"); command.add("hls"); // HLS格式
command.add("-hls_time"); command.add(String.valueOf(segmentDuration));
command.add("-hls_playlist_type"); command.add("event");
command.add("-hls_segment_filename"); command.add(outputPattern);
command.add(m3u8File);
// 4. 执行FFmpeg命令
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
// 读取输出(可选,用于调试)
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
logger.info("FFmpeg output: {}", line);
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("FFmpeg processing failed with exit code " + exitCode);
}
// 5. 清理临时输入文件
inputFile.delete();
return outputDir;
}
/**
* 获取单个TS片段
*/
public File getTSSegment(File hlsDir, String segmentName) {
return new File(hlsDir, segmentName);
}
/**
* 获取m3u8播放列表
*/
public File getPlaylist(File hlsDir) {
return new File(hlsDir, "playlist.m3u8");
}
}
2.3 控制器实现
@RestController
@RequestMapping("/api/stream")
public class StreamingController {
@Autowired
private VideoStreamingService streamingService;
// 缓存已处理的视频目录,避免重复处理
private final ConcurrentHashMap<String, File> processedVideos = new ConcurrentHashMap<>();
/**
* 获取m3u8播放列表
*/
@GetMapping("/{ossKey}/playlist.m3u8")
public ResponseEntity<Resource> getPlaylist(
@PathVariable String ossKey,
@RequestParam(defaultValue = "10") int segmentDuration) {
try {
// 检查是否已处理过
File hlsDir = processedVideos.computeIfAbsent(ossKey, k -> {
try {
return streamingService.processVideoToHLS(ossKey, segmentDuration);
} catch (Exception e) {
throw new RuntimeException("Failed to process video", e);
}
});
File playlist = streamingService.getPlaylist(hlsDir);
Resource resource = new FileSystemResource(playlist);
return ResponseEntity.ok()
.contentType(MediaType.valueOf("application/vnd.apple.mpegurl"))
.body(resource);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 获取TS片段
*/
@GetMapping("/{ossKey}/{segmentName:segment_\\d+\\.ts}")
public ResponseEntity<Resource> getSegment(
@PathVariable String ossKey,
@PathVariable String segmentName) {
try {
// 确保视频已处理
File hlsDir = processedVideos.get(ossKey);
if (hlsDir == null || !hlsDir.exists()) {
return ResponseEntity.notFound().build();
}
File segment = streamingService.getTSSegment(hlsDir, segmentName);
if (!segment.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(segment);
return ResponseEntity.ok()
.contentType(MediaType.valueOf("video/MP2T"))
.body(resource);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 清理缓存(可定时执行)
*/
@DeleteMapping("/cache/{ossKey}")
public ResponseEntity<Void> clearCache(@PathVariable String ossKey) {
File hlsDir = processedVideos.remove(ossKey);
if (hlsDir != null && hlsDir.exists()) {
try {
FileUtils.deleteDirectory(hlsDir);
} catch (IOException e) {
logger.error("Failed to delete directory: " + hlsDir, e);
}
}
return ResponseEntity.ok().build();
}
}
3. 优化与增强
3.1 性能优化
-
FFmpeg参数调优:
// 在processVideoToHLS方法中调整FFmpeg参数 command.add("-preset"); command.add("fast"); // 编码速度与质量的平衡 command.add("-crf"); command.add("23"); // 质量控制 command.add("-threads"); command.add(String.valueOf(Runtime.getRuntime().availableProcessors()));
-
缓存策略:
- 使用Caffeine或Redis缓存已处理的视频路径
- 设置合理的过期时间
-
异步处理:
@Async public CompletableFuture<File> asyncProcessVideoToHLS(String ossKey, int segmentDuration) { try { File result = processVideoToHLS(ossKey, segmentDuration); return CompletableFuture.completedFuture(result); } catch (Exception e) { return CompletableFuture.failedFuture(e); } }
3.2 防盗链实现
@GetMapping("/{ossKey}/playlist.m3u8")
public ResponseEntity<Resource> getPlaylist(
@PathVariable String ossKey,
@RequestParam(defaultValue = "10") int segmentDuration,
@RequestHeader(value = "Referer", required = false) String referer,
HttpServletRequest request) {
// 防盗链验证
if (!isValidRequest(referer, request)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// 其余代码不变...
}
private boolean isValidRequest(String referer, HttpServletRequest request) {
// 1. 检查Referer
if (referer != null && !referer.contains("yourdomain.com")) {
return false;
}
// 2. 检查IP白名单
String clientIp = request.getRemoteAddr();
// if (!allowedIps.contains(clientIp)) return false;
// 3. 可以添加token验证等
return true;
}
3.3 内存与磁盘管理
// 添加定时任务清理旧文件
@Scheduled(fixedRate = 24 * 60 * 60 * 1000) // 每天执行一次
public void cleanupOldFiles() {
File tempDir = new File(System.getProperty("java.io.tmpdir") + "/video_stream/");
if (tempDir.exists()) {
long currentTime = System.currentTimeMillis();
File[] files = tempDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory() && currentTime - file.lastModified() > 24 * 60 * 60 * 1000) {
try {
FileUtils.deleteDirectory(file);
} catch (IOException e) {
logger.error("Failed to delete old directory: " + file, e);
}
}
}
}
}
}
4. 前端集成示例
<video id="video" controls></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
const videoId = "path/to/your/video.mp4"; // OSS上的路径
if (Hls.isSupported()) {
const video = document.getElementById('video');
const hls = new Hls();
// 添加防盗链token(如果需要)
hls.loadSource(`/api/stream/${videoId}/playlist.m3u8?token=your_token`);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
video.play();
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// 原生HLS支持(如Safari)
video.src = `/api/stream/${videoId}/playlist.m3u8?token=your_token`;
video.addEventListener('loadedmetadata', function() {
video.play();
});
}
</script>
5. 部署注意事项
-
阿里云内网优化:
- 确保ECS和OSS在同一区域,使用内网Endpoint
- 配置OSS访问权限(RAM策略)
-
FFmpeg性能:
- 选择合适规格的ECS实例(CPU密集型)
- 考虑使用GPU实例进行硬件加速(如果需要处理大量高清视频)
-
监控与日志:
- 监控FFmpeg进程资源使用情况
- 记录处理失败的视频以便后续处理
-
扩展性:
- 对于高并发场景,考虑使用消息队列异步处理视频
- 可以结合Lambda@Edge进行边缘计算
这个方案实现了从OSS动态获取视频并实时转码为HLS格式的功能,同时考虑了性能、缓存和安全性。根据你的实际需求,可以进一步调整FFmpeg参数或优化缓存策略。