云计算百科
云计算领域专业知识百科平台

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

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参数或优化缓存策略。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Java实现基于阿里云OSS和FFmpeg的实时视频点播服务器
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!