Java实现基于阿里云OSS和FFmpeg的实时视频点播服务器

Java实现基于阿里云OSS和FFmpeg的实时视频点播服务器

针对你的需求(阿里云OSS存储视频文件,阿里云ECS部署服务,实时使用FFmpeg处理而不提前转码),我将提供一个完整的解决方案。

方案概述

  1. 架构:前端请求 → Java服务验证 → 动态调用FFmpeg处理 → 返回处理后的流
  2. 特点
    • 不需要预先转码所有视频
    • 按需实时处理
    • 利用阿里云内网带宽(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 性能优化

  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()));
    
  2. 缓存策略

    • 使用Caffeine或Redis缓存已处理的视频路径
    • 设置合理的过期时间
  3. 异步处理

    @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. 部署注意事项

  1. 阿里云内网优化

    • 确保ECS和OSS在同一区域,使用内网Endpoint
    • 配置OSS访问权限(RAM策略)
  2. FFmpeg性能

    • 选择合适规格的ECS实例(CPU密集型)
    • 考虑使用GPU实例进行硬件加速(如果需要处理大量高清视频)
  3. 监控与日志

    • 监控FFmpeg进程资源使用情况
    • 记录处理失败的视频以便后续处理
  4. 扩展性

    • 对于高并发场景,考虑使用消息队列异步处理视频
    • 可以结合Lambda@Edge进行边缘计算

这个方案实现了从OSS动态获取视频并实时转码为HLS格式的功能,同时考虑了性能、缓存和安全性。根据你的实际需求,可以进一步调整FFmpeg参数或优化缓存策略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值