SpringBoot + 阿里云视频点播 + 回调通知:视频上传、转码、审核、播放一站式集成

8 阅读5分钟

视频处理的复杂性挑战

在我们的日常开发工作中,经常会遇到这样的视频处理需求:

  • 用户上传的视频需要转码成多种分辨率,适配不同设备
  • 视频内容需要审核,确保符合平台规范
  • 上传后的视频需要实时通知处理状态
  • 视频播放需要防盗链和权限控制
  • 需要统计视频观看数据,用于运营分析

传统的视频处理方式要么需要自建视频处理服务,要么集成多个第三方服务,管理复杂。今天我们就来聊聊如何用阿里云视频点播服务构建一站式视频处理平台。 阅读原文

阿里云视频点播核心功能

相比传统的视频处理方案,阿里云视频点播有以下显著优势:

  • 一键转码:自动转码成多种分辨率和格式
  • 智能审核:AI驱动的内容审核,降低人工成本
  • 全球加速:CDN加速,确保全球流畅播放
  • 安全防护:多重防盗链机制,保护视频版权
  • 回调通知:实时状态回调,便于业务集成

核心集成方案

1. SDK集成配置

@Configuration
public class AliyunVodConfig {
    
    @Value("${aliyun.vod.access-key-id}")
    private String accessKeyId;
    
    @Value("${aliyun.vod.access-key-secret}")
    private String accessKeySecret;
    
    @Bean
    public DefaultAcsClient vodClient() {
        // 创建初始化对象
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
                .setAccessKeyId(accessKeyId)
                .setAccessKeySecret(accessKeySecret);
        
        // 设置VOD的域名
        config.endpoint = "vod.cn-shanghai.aliyuncs.com";
        
        return new DefaultAcsClient(config);
    }
}

2. 视频上传实现

@Service
public class VideoUploadService {
    
    @Autowired
    private DefaultAcsClient vodClient;
    
    public UploadVideoResponse uploadVideo(MultipartFile videoFile) {
        try {
            // 上传视频到阿里云
            UploadVideoRequest request = new UploadVideoRequest(
                    accessKeyId, accessKeySecret, 
                    videoFile.getOriginalFilename(),
                    videoFile.getInputStream());
            
            // 设置回调地址
            request.setUserData("{\"CallbackURL\":\"http://your-domain.com/callback/video-status\"}");
            
            UploadVideoResponse response = new UploadVideoImpl().uploadVideo(request);
            
            if (response.getVideoId() != null) {
                // 保存视频信息到本地数据库
                VideoInfo videoInfo = new VideoInfo();
                videoInfo.setVideoId(response.getVideoId());
                videoInfo.setFileName(videoFile.getOriginalFilename());
                videoInfo.setFileSize(videoFile.getSize());
                videoInfo.setStatus("UPLOADING");
                videoInfo.setUploadTime(LocalDateTime.now());
                
                videoInfoRepository.save(videoInfo);
                
                return response;
            }
        } catch (Exception e) {
            log.error("视频上传失败", e);
            throw new RuntimeException("视频上传失败", e);
        }
        return null;
    }
}

3. 视频转码配置

@Service
public class VideoTranscodeService {
    
    public void submitTranscodeJob(String videoId) {
        try {
            SubmitTranscodeJobsRequest request = new SubmitTranscodeJobsRequest();
            request.setVideoId(videoId);
            
            // 配置转码模板
            List<TranscodeTemplate> transcodeTemplateList = Arrays.asList(
                createTemplate("SD", "标清", 480),
                createTemplate("HD", "高清", 720),
                createTemplate("FHD", "超清", 1080)
            );
            
            request.setTranscodeTemplateList(transcodeTemplateList);
            
            SubmitTranscodeJobsResponse response = vodClient.getAcsResponse(request);
            
            log.info("转码任务提交成功,视频ID:{}", videoId);
        } catch (Exception e) {
            log.error("转码任务提交失败,视频ID:{}", videoId, e);
        }
    }
    
    private TranscodeTemplate createTemplate(String templateId, String name, int height) {
        TranscodeTemplate template = new TranscodeTemplate();
        template.setTemplateId(templateId);
        template.setName(name);
        template.setVideo(Arg().setHeight(height).setBitrate(1000));
        return template;
    }
}

4. 视频审核集成

@Service
public class VideoAuditService {
    
    public void submitAuditJob(String videoId) {
        try {
            SubmitAIASRJobRequest request = new SubmitAIASRJobRequest();
            request.setVideoId(videoId);
            
            // 配置审核参数
            request.setTypes("terrorism,porn,ad,abuse,logo,ocr,voice"); // 审核类型
            
            SubmitAIASRJobResponse response = vodClient.getAcsResponse(request);
            
            log.info("审核任务提交成功,视频ID:{},任务ID:{}", videoId, response.getAIASRJob().getJobId());
        } catch (Exception e) {
            log.error("审核任务提交失败,视频ID:{}", videoId, e);
        }
    }
    
    public AuditResult getAuditResult(String videoId) {
        try {
            DescribeAIASRJobRequest request = new DescribeAIASRJobRequest();
            request.setVideoId(videoId);
            
            DescribeAIASRJobResponse response = vodClient.getAcsResponse(request);
            
            // 解析审核结果
            return parseAuditResult(response.getAIASRJob());
        } catch (Exception e) {
            log.error("获取审核结果失败,视频ID:{}", videoId, e);
            return AuditResult.error("获取审核结果失败");
        }
    }
}

回调通知处理

1. 回调接口实现

@RestController
@RequestMapping("/callback")
public class VodCallbackController {
    
    @PostMapping("/video-status")
    public ResponseEntity<String> handleVideoCallback(@RequestBody String callbackData) {
        try {
            // 解析回调数据
            JSONObject jsonObject = JSON.parseObject(callbackData);
            String eventType = jsonObject.getString("EventType");
            String videoId = jsonObject.getString("VideoId");
            
            // 根据事件类型处理
            switch (eventType) {
                case "Upload.video.upload.finish":
                    handleUploadFinish(videoId, jsonObject);
                    break;
                case "Transcode.TranscodeSuccess":
                    handleTranscodeSuccess(videoId, jsonObject);
                    break;
                case "Audit.audit.success":
                    handleAuditSuccess(videoId, jsonObject);
                    break;
                case "Audit.audit.fail":
                    handleAuditFail(videoId, jsonObject);
                    break;
            }
            
            // 验证回调签名
            if (validateCallbackSign(callbackData)) {
                return ResponseEntity.ok("success");
            } else {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).body("invalid sign");
            }
        } catch (Exception e) {
            log.error("处理视频回调失败", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("error");
        }
    }
    
    private void handleUploadFinish(String videoId, JSONObject data) {
        // 更新视频上传状态
        VideoInfo video = videoInfoRepository.findByVideoId(videoId);
        if (video != null) {
            video.setStatus("UPLOADED");
            video.setDuration(data.getDoubleValue("Duration"));
            videoInfoRepository.save(video);
            
            // 提交转码任务
            videoTranscodeService.submitTranscodeJob(videoId);
        }
    }
    
    private void handleTranscodeSuccess(String videoId, JSONObject data) {
        // 更新转码状态
        VideoInfo video = videoInfoRepository.findByVideoId(videoId);
        if (video != null) {
            video.setStatus("TRANSCODED");
            videoInfoRepository.save(video);
            
            // 提交审核任务
            videoAuditService.submitAuditJob(videoId);
        }
    }
    
    private void handleAuditSuccess(String videoId, JSONObject data) {
        // 更新审核状态
        VideoInfo video = videoInfoRepository.findByVideoId(videoId);
        if (video != null) {
            video.setStatus("AUDITED_PASS");
            video.setAuditResult("PASS");
            videoInfoRepository.save(video);
            
            // 触发业务通知
            notifyVideoReady(videoId);
        }
    }
    
    private void handleAuditFail(String videoId, JSONObject data) {
        // 更新审核状态
        VideoInfo video = videoInfoRepository.findByVideoId(videoId);
        if (video != null) {
            video.setStatus("AUDITED_FAIL");
            video.setAuditResult("FAIL");
            video.setAuditReason(data.getString("AuditDesc"));
            videoInfoRepository.save(video);
            
            // 触发审核失败通知
            notifyVideoAuditFail(videoId, data.getString("AuditDesc"));
        }
    }
}

2. 回调签名验证

@Component
public class CallbackSignValidator {
    
    @Value("${aliyun.vod.callback.secret}")
    private String callbackSecret;
    
    public boolean validateCallbackSign(String callbackData) {
        try {
            // 解析回调数据,提取签名参数
            JSONObject data = JSON.parseObject(callbackData);
            String sign = data.getString("Sign");
            String timestamp = data.getString("Timestamp");
            
            // 重新构造待签名字符串
            String signStr = constructSignString(data);
            
            // 计算签名
            String calculatedSign = calculateSign(signStr, callbackSecret);
            
            return calculatedSign.equals(sign);
        } catch (Exception e) {
            log.error("回调签名验证失败", e);
            return false;
        }
    }
    
    private String calculateSign(String signStr, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA1");
            SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA1");
            mac.init(signingKey);
            byte[] rawHmac = mac.doFinal(signStr.getBytes("UTF-8"));
            return Base64.getEncoder().encodeToString(rawHmac);
        } catch (Exception e) {
            throw new RuntimeException("签名计算失败", e);
        }
    }
}

视频播放集成

1. 播放地址获取

@Service
public class VideoPlayService {
    
    public PlayInfoResponse getPlayInfo(String videoId) {
        try {
            GetPlayInfoRequest request = new GetPlayInfoRequest();
            request.setVideoId(videoId);
            
            // 设置播放权限(防盗链)
            request.setAuthTimeout(3600); // 1小时有效期
            
            GetPlayInfoResponse response = vodClient.getAcsResponse(request);
            
            return response;
        } catch (Exception e) {
            log.error("获取播放信息失败,视频ID:{}", videoId, e);
            throw new RuntimeException("获取播放信息失败", e);
        }
    }
    
    public String generatePlayAuth(String videoId) {
        try {
            GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
            request.setVideoId(videoId);
            
            GetVideoPlayAuthResponse response = vodClient.getAcsResponse(request);
            
            return response.getPlayAuth();
        } catch (Exception e) {
            log.error("生成播放凭证失败,视频ID:{}", videoId, e);
            throw new RuntimeException("生成播放凭证失败", e);
        }
    }
}

2. 播放控制

@RestController
@RequestMapping("/video")
public class VideoController {
    
    @GetMapping("/{videoId}/play-info")
    public ResponseEntity<PlayInfoResponse> getPlayInfo(@PathVariable String videoId) {
        // 检查视频状态
        VideoInfo video = videoInfoRepository.findByVideoId(videoId);
        if (video == null) {
            return ResponseEntity.notFound().build();
        }
        
        if (!"AUDITED_PASS".equals(video.getStatus())) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }
        
        // 获取播放信息
        PlayInfoResponse playInfo = videoPlayService.getPlayInfo(videoId);
        
        return ResponseEntity.ok(playInfo);
    }
    
    @GetMapping("/{videoId}/play-auth")
    public ResponseEntity<Map<String, String>> getPlayAuth(@PathVariable String videoId) {
        // 验证用户权限
        if (!hasPlayPermission(videoId)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }
        
        String playAuth = videoPlayService.generatePlayAuth(videoId);
        
        Map<String, String> response = new HashMap<>();
        response.put("playAuth", playAuth);
        
        return ResponseEntity.ok(response);
    }
}

高级功能实现

1. 视频水印

@Service
public class VideoWatermarkService {
    
    public void addWatermark(String videoId, WatermarkConfig config) {
        try {
            SubmitSnapshotJobRequest request = new SubmitSnapshotJobRequest();
            request.setVideoId(videoId);
            
            // 配置水印参数
            SnapshotConfig snapshotConfig = new SnapshotConfig();
            snapshotConfig.setWaterMark(config);
            
            vodClient.getAcsResponse(request);
        } catch (Exception e) {
            log.error("添加水印失败", e);
        }
    }
}

2. 智能封面生成

@Service
public class VideoCoverService {
    
    public void generateSmartCover(String videoId) {
        try {
            SubmitAIJobRequest request = new SubmitAIJobRequest();
            request.setVideoId(videoId);
            request.setTypes("smart_cover"); // 智能封面
            
            vodClient.getAcsResponse(request);
        } catch (Exception e) {
            log.error("生成智能封面失败", e);
        }
    }
}

最佳实践建议

  1. 分步处理:上传、转码、审核分步进行,提高用户体验
  2. 状态管理:完善的状态跟踪,便于问题排查
  3. 回调验证:严格验证回调签名,防止恶意请求
  4. 监控告警:监控视频处理成功率、失败率等指标
  5. 成本控制:合理配置转码参数,控制存储和流量成本

通过阿里云视频点播服务,我们可以快速构建功能完善的视频处理平台,专注于业务逻辑而非底层实现。


以上就是本期分享的内容,希望对你有所帮助。更多技术干货,请关注服务端技术精选,我们下期再见!