VOD使用开发

阿里云视频点播官方文档
阿里云视频点播(ApsaraVideo VoD)是集音视频采集、编辑、上传、自动化转码处理、媒体资源管理、高效云剪辑处理、分发加速、视频播放于一体的一站式音视频点播解决方案。

阿里云视频点播基于阿里云强大的基础设施服务,面向视频网站、短视频、在线教育、娱乐社交、新闻传媒等行业,提供端-云-端的视频全链路服务,帮助企业和开发者快速搭建安全、弹性、高效、可定制的视频点播平台和应用。

网站上观看的视频需要保证安全性,不可下载和传播。但是视频所占内存巨大,因此决定使用阿里云的视频点播来存储和处理视频。我负责的是视频上传、加密、解密播放这一块的开发,在开发过程中也是遇到了许多问题,因此记录一下,也是以后遇到问题时,能够及时回顾和查看。

视频上传部分

视频点播支持通过多种方式上传媒体文件(音频、视频、图片等)到点播存储。同时支持自动触发或通过API,发起进行转码、剪辑、分发等后续处理。
支持的方式有:

控制台上传
服务端上传
客户端上传
离线拉取上传
PC客户端工具上传

我做的部分主要是服务端上传,其他方式的上传可以去阿里云视频点播官方文档上查找,里面有详细的文档介绍。

服务端上传阿里云的官方解释为:

服务端上传,是指将应用服务器上的媒体文件上传到点播存储。适合自动化上传、大批量迁移视频上传等场景,也可用于网络媒体文件的上传,其原理也是先下载到应用服务器本地再上传到点播。

这里采用的是SDK的上传方式:

安装SDK需要导入Maven依赖包:

<!-- 添加Jar包依赖 --><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.5.1</version></dependency><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-vod</artifactId><version>2.15.10</version></dependency><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-kms</artifactId><version>2.10.1</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.10</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.7.0</version></dependency><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-vod-upload</artifactId><version>1.4.12</version><scope>system</scope><systemPath>${pom.basedir}/lib/aliyun-java-vod-upload-1.4.12.jar</systemPath></dependency><dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.6.0</version><exclusions><exclusion><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-impl</artifactId></exclusion></exclusions></dependency>

其中aliyun-java-vod-upload 这个包需要去网上找相应版本的jar包,新建一个lib包放到里面。如果直接用阿里云提供的Maven依赖,可能会产生各种各样的问题,会产生各种版本冲突。也可以直接通过百度网盘下载:
链接
提取码:ldvu

 /*** 视频文件流上传调用接口* @param title* @param fileName* @return*/public static Map<Object,Object> VodUploadStream( String title,String fileName,String TemplateGroupId) {// 一、视频文件上传// 视频标题(必选)//String title = "测试标题";// 1.本地文件上传和文件流上传时,文件名称为上传文件绝对路径,如:/User/sample/文件名称.mp4 (必选)// 2.网络流上传时,文件名称为源文件名,如文件名称.mp4(必选)。// 3.流式上传时,文件名称为源文件名,如文件名称.mp4(必选)。// 任何上传方式文件名必须包含扩展名//String fileName = "E:\\baa4cfc04fd94efca3b515b0ed35c4dc.mp4";// 待上传视频的网络流地址//String url = "http://test.aliyun.com/video/test.mp4";// 4.流式上传,如文件流和网络流InputStream inputStream = null;// 4.1 文件流try {inputStream = new FileInputStream(fileName);} catch (FileNotFoundException e) {e.printStackTrace();}// 4.2 网络流/*try {inputStream = new URL(url).openStream();} catch (IOException e) {e.printStackTrace();}*/Map<Object,Object>map=UploadStream(accessKeyId, accessKeySecret, title, fileName, inputStream,TemplateGroupId);try {inputStream.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}return map;
}/*** 流式上传接口** @param accessKeyId* @param accessKeySecret* @param title* @param fileName* @param inputStream*/private static Map<Object,Object> UploadStream(String accessKeyId, String accessKeySecret, String title, String fileName, InputStream inputStream,String TemplateGroupId) {UploadStreamRequest request = new UploadStreamRequest(accessKeyId, accessKeySecret, title, fileName, inputStream);File newFile = new File(fileName);Long totalBytes = newFile.length();//System.out.println(totalBytes);Map<Object, Object> resultMap = new HashMap<Object, Object>();/* 是否使用默认水印(可选),指定模板组ID时,根据模板组配置确定是否使用默认水印*///request.setShowWaterMark(true);/* 设置上传完成后的回调URL(可选),建议通过点播控制台配置消息监听事件,参见文档 https://help.aliyun.com/document_detail/57029.html *///request.setCallback("http://callback.sample.com");/* 自定义消息回调设置,参数说明参考文档 https://help.aliyun.com/document_detail/86952.html#UserData */  //request.setUserData("{\"MessageCallback\":{\"CallbackURL\":\"http://xxx/api/HlsDecryptServer/uploadCallBack\"}}");request.setUserData(uploadCallBack);/* 视频分类ID(可选) */request.setCateId((long) 0000156004);/* 视频标签,多个用逗号分隔(可选) *///request.setTags("标签1,标签2");/* 视频描述(可选) *///request.setDescription("视频描述");/* 封面图片(可选) *///request.setCoverURL("https:///9f0b5337bbea4076ad9594feb2ebb59e/snapshots/2cb1545b97fb4229b4d89402137c9f2f-00001.jpg");/* 模板组ID(可选) */request.setTemplateGroupId(TemplateGroupId);/* 工作流ID(可选) *///request.setWorkflowId("d4430d07361f0*be1339577859b0177b");/* 存储区域(可选) */request.setStorageLocation(storageLocation);/* 开启默认上传进度回调 *///request.setPrintProgress(true);/* 设置自定义上传进度回调 (必须继承 VoDProgressListener) */PutObjectProgressListener putObjectProgressListener = new PutObjectProgressListener();putObjectProgressListener.setTotalBytes(totalBytes);request.setProgressListener(putObjectProgressListener);/* 设置应用ID*///request.setAppId("app-1000000");/* 点播服务接入点 *///request.setApiRegionId("cn-shanghai");/* ECS部署区域*///request.setEcsRegionId("cn-shanghai");UploadVideoImpl uploader = new UploadVideoImpl();UploadStreamResponse response = uploader.uploadStream(request);//System.out.print("RequestId=" + response.getRequestId() + "\n");  //请求视频点播服务的请求IDresultMap .put("RequestId", response.getRequestId());if (response.isSuccess()) {//System.out.print("VideoId=" + response.getVideoId() + "\n");resultMap .put("VideoId", response.getVideoId());} else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因//System.out.print("VideoId=" + response.getVideoId() + "\n");resultMap .put("VideoId", response.getVideoId());//System.out.print("ErrorCode=" + response.getCode() + "\n");resultMap .put("ErrorCode", response.getCode());//System.out.print("ErrorMessage=" + response.getMessage() + "\n");resultMap .put("ErrorMessage=", response.getMessage());}return resultMap;}

其中 TemplateGroupId 是模板组Id,可以在阿里云控制台上获取,设置模板组可以使视频转码,高清、超清等等都有,我这里设置的是不转码,把上传视频和视频加密分开操作。

上传进度回调函数示例代码:

package com.huanke.managesystem.framework.core.service.impl;import com.aliyun.oss.event.ProgressEvent;
import com.aliyun.oss.event.ProgressEventType;
import com.aliyun.vod.upload.impl.VoDProgressListener;/*** 上传进度回调方法类* 当您开启上传进度回调时该事件回调才会生效。* OSS分片上传成功或失败均触发相应的回调事件,您可根据业务逻辑处理相应的事件回调。* 当创建音视频信息成功后,此上传进度回调中的videoId为本次上传生成的视频ID,您可以根据视频ID进行音视频管理。* 当创建图片信息成功后,此上传进度回调中的ImageId为本次上传生成的图片ID,您可以根据视频ID进行图片管理。*/
public class PutObjectProgressListener implements VoDProgressListener{/*** 已成功上传至OSS的字节数*/private long bytesWritten = 0;/*** 原始文件的总字节数*/private long totalBytes ;/*** 本次上传成功标记*/private boolean succeed = false;/*** 视频ID*/private String videoId;/*** 图片ID*/private String imageId;public void progressChanged(ProgressEvent progressEvent) {long bytes = progressEvent.getBytes();ProgressEventType eventType = progressEvent.getEventType();switch (eventType) {// 开始上传事件case TRANSFER_STARTED_EVENT:if (videoId != null) {System.out.println("Start to upload videoId " + videoId + "......");}if (imageId != null) {System.out.println("Start to upload imageId " + imageId + "......");}break;// 计算待上传文件总大小事件通知,只有调用本地文件方式上传时支持该事件case REQUEST_CONTENT_LENGTH_EVENT:this.totalBytes = bytes;System.out.println(this.totalBytes + "bytes in total will be uploaded to OSS.");break;// 已经上传成功文件大小事件通知case REQUEST_BYTE_TRANSFER_EVENT:this.bytesWritten += bytes;if (this.totalBytes != -1) {int percent = (int) (this.bytesWritten * 100.0 / this.totalBytes);System.out.println(bytes + " bytes have been written at this time, upload progress: " +percent + "%(" + this.bytesWritten + "/" + this.totalBytes + ")");} else {System.out.println(bytes + " bytes have been written at this time, upload sub total : " +"(" + this.bytesWritten + ")");}break;// 文件全部上传成功事件通知case TRANSFER_COMPLETED_EVENT:this.succeed = true;if (videoId != null) {System.out.println("Succeed to upload videoId " + videoId + " , " + this.bytesWritten + " bytes have been transferred in total.");}if (imageId != null) {System.out.println("Succeed to upload imageId " + imageId + " , " + this.bytesWritten + " bytes have been transferred in total.");}break;// 文件上传失败事件通知case TRANSFER_FAILED_EVENT:if (videoId != null) {System.out.println("Failed to upload videoId " + videoId + " , " + this.bytesWritten + " bytes have been transferred.");}if (imageId != null) {System.out.println("Failed to upload imageId " + imageId + " , " + this.bytesWritten + " bytes have been transferred.");}break;default:break;}}public boolean isSucceed() {return succeed;}public void onVidReady(String videoId) {setVideoId(videoId);}public void onImageIdReady(String imageId) {setImageId(imageId);}public String getVideoId() {return videoId;}public void setVideoId(String videoId) {this.videoId = videoId;}public Long getTotalBytes() {return totalBytes;}public void setTotalBytes(Long totalBytes) {this.totalBytes = totalBytes;}public String getImageId() {return imageId;}public void setImageId(String imageId) {this.imageId = imageId;}}

其中accessKeyId和accessKeySecret是访问密钥,可以在控制台上创建和查看

request.setUserData(uploadCallBack); 可以设置回调地址,确定视频是否上传成功,并根据这个回调成功信息,对视频进行加密。

上传视频会返回一个VideoId,这个是阿里云视频点播中区分视频的唯一id,以后对这个视频的相关操作,都需要VideoId,建议做持久化处理。

上传完视频后我们也可以查询一些视频信息,代码如下:

public static Map<Object,Object> getPlayInfoAddress(String VideoId) {DefaultAcsClient client = initVodClient(accessKeyId, accessKeySecret);GetPlayInfoResponse response = new GetPlayInfoResponse();Map<Object, Object> resultMap = new HashMap<Object, Object>();ArrayList  list = new ArrayList();try {GetPlayInfoRequest request = new GetPlayInfoRequest();request.setVideoId(VideoId);//request.setResultType("Multiple");response = client.getAcsResponse(request);List<GetPlayInfoResponse.PlayInfo> playInfoList = response.getPlayInfoList();System.out.println(playInfoList.size());//播放地址for ( int i = 0; i<playInfoList.size(); i++) {Map<Object, Object> map = new HashMap<Object, Object>();String PlayURL = playInfoList.get(i).getPlayURL();list.add(PlayURL);map.put("PlayURL", PlayURL);resultMap.put("Address", PlayURL);System.out.print("PlayInfo.PlayURL = " + PlayURL + "\n");}//Base信息System.out.print("VideoBase.Title = " + response.getVideoBase().getTitle() + "\n");resultMap.put("Title", response.getVideoBase().getTitle());//resultMap.put("Address", list);} catch (Exception e) {System.out.print("ErrorMessage = " + e.getLocalizedMessage());}System.out.print("RequestId = " + response.getRequestId() + "\n");return resultMap;}

上传完视频后,会根据你写进去的回调地址回调信息,阿里云那边会返回是否回调成功,可以根据这个信息再进行视频加密,视频上传和加密不能同时进行,否则会造成加密失败,本人吃过这个亏。
如果不填写回调地址,控制台上的回调地址一定不能写死,可以写一个通配的,否则会造成视频加密失败,回调地址尽量通过代码控制,这可能是阿里云的的一个bug,但是问阿里云客服的话,也没问出什么实质性的内容。

回调接口代码:

@ApiOperation(value = "视频上传回调接口", hidden = true)@RequestMapping(value = "/uploadCallBack", method = RequestMethod.POST)public Object uploadOSSVodCallBack( @RequestBody String data , HttpServletRequest request) throws Exception {JSONObject jsonObject=JSONObject.fromObject(data);String eventType = jsonObject.getString("EventType");String videoId = jsonObject.getString("VideoId");if(eventType.equals("FileUploadComplete")) {List<Long> s  = baseAttachmentRepoDao.queryByVideoId(videoId);BaseAttachmentRepo attachment;attachment = baseAttachmentRepoDao.getOne(s.get(0));if(attachment.getJobId()== null) {Map<Object,Object> map = GenerateDataKey.submitTranscodeJobs(videoId, TemplateGroupId);attachment.setJobId(map.get("JobId").toString());}baseAttachmentRepoDao.save(attachment);}System.out.println(jsonObject);return jsonObject;}

在这个接口里面会判断视频上传是否成功,如果成功就进行视频加密,不成功就需要标记一下。阿里云官方说99%是会成功的,遇上了那1%也没办法。需要找失败原因。

视频加密

阿里云的视频加密有两种方式HLS标准加密和SSL私有加密,因为SSL私有加密不支持客户端播放,因此建议使用HLS标准加密
提交媒体处理作业:

package com.huanke.managesystem.framework.core.service.impl;import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import org.apache.commons.lang.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.kms.model.v20160120.DecryptRequest;
import com.aliyuncs.kms.model.v20160120.DecryptResponse;
import com.aliyuncs.kms.model.v20160120.GenerateDataKeyRequest;
import com.aliyuncs.kms.model.v20160120.GenerateDataKeyResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.vod.model.v20170321.SubmitTranscodeJobsRequest;
import com.aliyuncs.vod.model.v20170321.SubmitTranscodeJobsResponse;
import com.google.gson.Gson;@Service
@Transactional
public class GenerateDataKey {private static final Logger logger = LoggerFactory.getLogger(GenerateDataKey.class);private static String accessKeyId;@Value("${vod.accessKeyId}")public void setAccessKeyId(String accessKeyId) {GenerateDataKey.accessKeyId = accessKeyId;}private static String accessKeySecret;@Value("${vod.accessKeySecret}")public void setAccessKeySecret(String accessKeySecret) {GenerateDataKey.accessKeySecret = accessKeySecret;}private static String transcodeCallBack;@Value("${vod.transcodeCallBack}")public void setTranscodeCallBack(String transcodeCallBack) {GenerateDataKey.transcodeCallBack = transcodeCallBack;}private static String serviceKey;@Value("${vod.serviceKey}")public void setServiceKey(String serviceKey) {GenerateDataKey.serviceKey = serviceKey;}private static String decrypt;@Value("${vod.decrypt}")public void setDecrypt(String decrypt) {GenerateDataKey.decrypt = decrypt;}/*** 提交媒体处理作业* 以下为调用示例*/public static Map<Object,Object> submitTranscodeJobs(String VideoId,String TemplateGroupId ) {DefaultAcsClient client = null;try {client = initVodClient(accessKeyId, accessKeySecret);} catch (ClientException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}SubmitTranscodeJobsResponse response = new SubmitTranscodeJobsResponse();Map<Object,Object> map = new HashMap();try {SubmitTranscodeJobsRequest request = new SubmitTranscodeJobsRequest();//需要转码的视频IDrequest.setVideoId(VideoId);//转码模板IDrequest.setTemplateGroupId(TemplateGroupId);  //https://b.unistudy.top/api/HlsDecryptServer/callBack\//构建需要替换的水印参数(只有需要替换水印相关信息才需要构建)//request.setUserData("{\"MessageCallback\":{\"CallbackURL\":\"http://xxxxx/api/HlsDecryptServer/transcodeCallBack\"}}");request.setUserData(transcodeCallBack);//JSONObject overrideParams = buildOverrideParams();//覆盖参数,暂只支持水印部分参数替换(只有需要替换水印相关信息才需要传递)//request.setOverrideParams(overrideParams.toJSONString());//构建标准加密配置参数(只有标准加密才需要构建)JSONObject encryptConfig = buildEncryptConfig(client);//HLS标准加密配置(只有标准加密才需要传递)request.setEncryptConfig(encryptConfig.toJSONString());//return client.getAcsResponse(request);response = client.getAcsResponse(request);//任务ID//System.out.println("JobId = " + response.getTranscodeJobs().get(0).getJobId());map.put("JobId", response.getTranscodeJobs().get(0).getJobId());} catch (Exception e) {System.out.println("ErrorMessage = " + e.getLocalizedMessage());}//System.out.println("RequestId = " + response.getRequestId());map.put("RequestId", response.getRequestId());return map;}/*** 初始化函数* @param accessKeyId* @param accessKeySecret* @return* @throws ClientException*/public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {String regionId = "cn-shanghai";  // 点播服务接入区域DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);DefaultAcsClient client = new DefaultAcsClient(profile);return client;} /*** 构建HLS标准加密的配置信息* @return* @throws ClientException* @throws com.aliyuncs.exceptions.ClientException * @throws ServerException */public static JSONObject buildEncryptConfig(DefaultAcsClient client) throws ClientException, ServerException, com.aliyuncs.exceptions.ClientException {//点播给用户在KMS(秘钥管理服务)中的Service Key,可在用户秘钥管理服务对应的区域看到描述为vod的service key//String serviceKey = "c92a024d-515c-453e-b5bb-e81f8000d28e";//随机生成一个加密的秘钥,返回的response包含明文秘钥以及密文秘钥,//视频标准加密只需要传递密文秘钥即可//注意:KMS Client建议单独初始化来保证正确的接入区域,可参考VOD初始化方式,传入正确的KMS服务区域。GenerateDataKeyResponse response = generateDataKey(client, serviceKey);JSONObject encryptConfig = new JSONObject();//解密接口地址,该参数需要将每次生成的密文秘钥与接口URL拼接生成,表示每个视频的解密的密文秘钥都不一样//至于Ciphertext这个解密接口参数的名称,用户可自行制定,这里只作为参考参数名称encryptConfig.put("DecryptKeyUri", decrypt+"?" +"Ciphertext=" + response.getCiphertextBlob());System.out.println("解密地址为:" +  decrypt+"?" +"Ciphertext=" + response.getCiphertextBlob());//秘钥服务的类型,目前只支持KMSencryptConfig.put("KeyServiceType", "KMS");//密文秘钥encryptConfig.put("CipherText", response.getCiphertextBlob());System.out.println("密文秘钥" + response.getCiphertextBlob() );return encryptConfig;}/*** 生成加密需要的秘钥,response中包含密文秘钥和明文秘钥,用户只需要将密文秘钥传递给点播即可* 注意:KeySpec 必须传递AES_128,且不能设置NumberOfBytes* @param client KMS-SDK客户端* @param serviceKey 点播提供生成秘钥的service key,在用户的秘钥管理服务中可看到描述为vod的加密key* @return* @throws ClientException* @throws com.aliyuncs.exceptions.ClientException * @throws ServerException */public static GenerateDataKeyResponse generateDataKey(DefaultAcsClient client, String serviceKey) throws ClientException, ServerException {GenerateDataKeyRequest request = new GenerateDataKeyRequest();request.setKeyId(serviceKey);request.setKeySpec("AES_128");request.setRegionId("cn-shanghai");try {return client.getAcsResponse(request);} catch (com.aliyuncs.exceptions.ClientException e) {// TODO Auto-generated catch blocke.printStackTrace();}return null;}}

视频啊加密总共分为这几步:

1. 开通KMS服务。

2. 提交工单申请创建Service Key。Service Key与视频存储的源站区域必须一致,例如:视频存储在华东2,则Service Key必须是华东2。
3. 说明 Service Key是密钥管理服务的一种加密主Key,接入标准加密的密钥必须要使用该Service Key生成。视频点播控制台暂不支持用户自主创建Service Key。
4. 搭建密钥管理服务,封装阿里云密钥管理服务(KMS),调用GenerateDataKey接口生成一个AES_128密钥。
5. 说明 GenerateDataKey接口只需要传KeyId(上述中的Service Key)和KeySpec(固定为:AES_128)即可,其他参数不用传,否则可能加密失败。
6. 搭建令牌颁发服务,生成MtsHlsUriToken。
7.解密密钥EDK(密文密钥),调用Decrypt接口进行解密。如果业务方需要对解密接口进行安全验证,则需要提供令牌生成服务,生成的令牌能够在解密服务中被解析验证。
8.说明 解密接口返回的数据,是GenerateDataKey生成的两种密钥中的明文密钥(PlainText)经过base64decode之后的数据。

接入流程:

创建转码模板:
HLS标准加密转码需要创建两个转码模板。

加密模板:

在控制台的添加转码模板组页面,创建HLS模板,开启视频加密,并勾选私有加密选项(必须勾选,否则不加密)。

说明 该模板在调用提交媒体转码作业接口时,通过TemplateGroupId参数传递,如此视频点播将按照设置的模板和传递的密钥信息进行标准加密转码。

不转码模板

可在控制台的转码设置页面激活不转码模板,如果不转码模板已经存在则无须再次激活。
说明 目前点播上传视频默认都会自动触发转码(自动触发暂不支持HLS标准加密)),因此对于标准加密为防止自动触发转码,需要先使用不转码模板上传视频(该类模板不会自动触发转码),然后再调用提交媒体转码作业接口发起标准加密转码。

RAM授权

使用RAM服务给视频点播授权访问业务方密钥管理服务(KMS)的权限,请参见RAM授权给视频点播授权。

调用提交媒体转码作业接口传递创建的加密模板ID和标准加密参数发起标准加密转码。
验证加密转码是否成功
可通过以下三种方式来逐步判断标准加密是否成功。
如果视频只有m3u8格式输出,那么视频状态为转码失败。
如果视频不只有m3u8格式输出,那么需要到视频点播控制台视频管理详情查看是否存在加密标志的m3u8文件地址输出,如果有一般表明标准加密成功。
如果前面两种方式都不能判断,那么可以将带有加密标志的m3u8文件的地址拷贝出来,使用curl -v “m3u8文件地址”,查看获取到的m3u8内容是否存在URI="<业务方在发起标准加密时传递的解密地址,即加密配置EncryptConfig中的DecryptKeyUri参数值>"关键信息,有则表明为标准加密且加密成功。

在加密的过程中需要设置回调地址和解密地址的接口,这个解密地址是写进加密信息里的,会体现在加密视频的url里,当使用阿里云的sdk播放时,输入这个加密视频的url,会自动解析出解密地址,并调用这个接口,播放视频。

加密完成回调地址:
@ApiOperation(value = "视频转码回调接口", hidden = true)@RequestMapping(value = "/transcodeCallBack", method = RequestMethod.POST)public Object transcodeOssVodCallBack( @RequestBody String data , HttpServletRequest request) throws Exception {JSONObject jsonObject=JSONObject.fromObject(data);String eventType = jsonObject.getString("EventType");String status = jsonObject.getString("Status");String videoId = jsonObject.getString("VideoId");BaseAttachmentRepo attachment;if(eventType.equals("TranscodeComplete")) {List<Long> s  = baseAttachmentRepoDao.queryByVideoId(videoId);attachment = baseAttachmentRepoDao.getOne(s.get(0));attachment.setTranscodeStatus(status);baseAttachmentRepoDao.save(attachment);}System.out.println(jsonObject);return jsonObject;}
回传的信息示例
{"Status":"success",
"VideoId":"2ece1a5333f744179afddab403df",
"EventType":"TranscodeComplete",
"EventTime":"2021-01-09T02:40:15Z",
"StreamInfos":[{"Status":"success","IsAudio":false,"UserData":{"MessageCallback":{"CallbackURL":"https://xxxx/api/HlsDecryptServer/callBack"}},"Size":17951932,"Definition":"SD","Fps":"25","StartTime":"2021-01-09T02:40:03Z","Duration":81,"Bitrate":"1766","Encrypt":true,"FileUrl":"http://xxx/2ece1a5333f744179afddab403df457b/7e25a3364ce1a726d75231b7fbe25ce2-sd-encrypt-stream.m3u8","Format":"m3u8","FinishTime":"2021-01-09T02:40:16Z","Height":720,"Width":1280,"JobId":"b69e831b59b3491a8ddd328726b0"}]
}
在自己的服务器上搭建解密服务

代码如下,真实可用:

package com.huanke.managesystem.framework.core.controller;import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.ProtocolType;
import com.aliyuncs.kms.model.v20160120.DecryptRequest;
import com.aliyuncs.kms.model.v20160120.DecryptResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.huanke.managesystem.framework.core.common.ajax.ResultBean;
import com.huanke.managesystem.framework.core.service.impl.HlsDecryptServerImpl;
import com.huanke.managesystem.framework.core.service.impl.OSSUtilsImpl;
import com.huanke.managesystem.framework.util.Md5Util;import org.apache.commons.codec.binary.Base64;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;@RestController
@RequestMapping("/HlsDecryptServer")
public class HlsDecryptServerController {private static final Logger logger = LoggerFactory.getLogger(HlsDecryptServerController.class);@RequestMapping(value = "/decrypt", method = RequestMethod.GET)public Object uploadOSSVod(String Ciphertext) throws Exception {/*HlsDecryptServerImpl s = new HlsDecryptServerImpl();s.serviceBootStrap();*/byte[] s = HlsDecryptServerImpl.HlsDecryptHandler.decrypt(Ciphertext);return s;}}
package com.huanke.managesystem.framework.core.service.impl;import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.ProtocolType;
import com.aliyuncs.kms.model.v20160120.DecryptRequest;
import com.aliyuncs.kms.model.v20160120.DecryptResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.huanke.managesystem.framework.core.controller.HlsDecryptServerController;
import com.huanke.managesystem.framework.core.service.impl.OSSUtilsImpl;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.spi.HttpServerProvider;import org.apache.commons.codec.binary.Base64;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;@Service
@Transactional
public class HlsDecryptServerImpl {private static final Logger logger = LoggerFactory.getLogger(HlsDecryptServerImpl.class);private static DefaultAcsClient client;static {//KMS的区域,必须与视频对应区域String region = "cn-shanghai";//访问KMS的授权AK信息String accessKeyId = "";String accessKeySecret = "";client = new DefaultAcsClient(DefaultProfile.getProfile(region, accessKeyId, accessKeySecret));}/*** 说明:* 1、接收解密请求,获取密文秘钥和令牌Token* 2、调用KMS decrypt接口获取明文秘钥* 3、将明文秘钥base64decode返回*/public static class HlsDecryptHandler implements HttpHandler {/*** 处理解密请求* @param httpExchange* @throws IOException*/public void handle(HttpExchange httpExchange) throws IOException {String requestMethod = httpExchange.getRequestMethod();if ("GET".equalsIgnoreCase(requestMethod)) {//校验token的有效性String token = getMtsHlsUriToken(httpExchange);boolean validRe = validateToken(token);/*if (!validRe) {return;}*///从URL中取得密文密钥String ciphertext = getCiphertext(httpExchange);if (null == ciphertext)return;//从KMS中解密出来,并Base64 decodebyte[] key = decrypt(ciphertext);//设置headersetHeader(httpExchange, key);//返回base64decode之后的密钥OutputStream responseBody = httpExchange.getResponseBody();responseBody.write(key);responseBody.close();}}private void setHeader(HttpExchange httpExchange, byte[] key) throws IOException {Headers responseHeaders = httpExchange.getResponseHeaders();responseHeaders.set("Access-Control-Allow-Origin", "*");httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, key.length);}/*** 调用KMS decrypt接口解密,并将明文base64decode* @param ciphertext* @return*///@RequestMapping(value = "/decrypt", method = RequestMethod.GET)public static  byte[] decrypt(String Ciphertext) {DecryptRequest request = new DecryptRequest();request.setCiphertextBlob(Ciphertext);request.setProtocol(ProtocolType.HTTPS);try {DecryptResponse response = client.getAcsResponse(request);String plaintext = response.getPlaintext();//注意:需要base64 decodereturn Base64.decodeBase64(plaintext);} catch (ClientException e) {e.printStackTrace();return null;}}/*** 校验令牌有效性* @param token* @return*/private boolean validateToken(String token) {if (null == token || "".equals(token)) {return false;}//TODO 业务方实现令牌有效性校验return true;}/*** 从URL中获取密文秘钥参数* @param httpExchange* @return*/private String getCiphertext(HttpExchange httpExchange) {URI uri = httpExchange.getRequestURI();String queryString = uri.getQuery();String pattern = "Ciphertext=(\\w*)";Pattern r = Pattern.compile(pattern);Matcher m = r.matcher(queryString);if (m.find())return m.group(1);else {System.out.println("Not Found Ciphertext Param");return null;}}/*** 获取Token参数** @param httpExchange* @return*/private String getMtsHlsUriToken(HttpExchange httpExchange) {URI uri = httpExchange.getRequestURI();String queryString = uri.getQuery();String pattern = "MtsHlsUriToken=(\\w*)";Pattern r = Pattern.compile(pattern);Matcher m = r.matcher(queryString) ;if (m.find())return m.group(1);else {System.out.println("Not Found MtsHlsUriToken Param");return null;}}}/*** 服务启动** @throws IOException*/public void serviceBootStrap() throws IOException {HttpServerProvider provider = HttpServerProvider.provider();//监听端口9999,能同时接受30个请求HttpServer httpserver = provider.createHttpServer(new InetSocketAddress(8089), 60);httpserver.createContext("/", new HlsDecryptHandler());httpserver.start();System.out.println("hls decrypt server started");}public static void main(String[] args) throws IOException {HlsDecryptServerImpl server = new HlsDecryptServerImpl();server.serviceBootStrap();}
}

当服务搭建完毕并测试没问题后,则可以完整上传和加密视频。

视频播放

当加密完成后,需要获取加密后的视频URL,
代码如下:

/*以下为调用示例*/public static Map<Object,Object> getPlayInfoAddress(String VideoId) {DefaultAcsClient client = initVodClient("accesskey", "accesskId");GetPlayInfoResponse response = new GetPlayInfoResponse();Map<Object, Object> resultMap = new HashMap<Object, Object>();ArrayList  list = new ArrayList();try {GetPlayInfoRequest request = new GetPlayInfoRequest();request.setVideoId(VideoId);//request.setResultType("Multiple");response = client.getAcsResponse(request);List<GetPlayInfoResponse.PlayInfo> playInfoList = response.getPlayInfoList();System.out.println(playInfoList.size());//播放地址for ( int i = 0; i<playInfoList.size(); i++) {Map<Object, Object> map = new HashMap<Object, Object>();String PlayURL = playInfoList.get(i).getPlayURL();list.add(PlayURL);map.put("PlayURL", PlayURL);resultMap.put("Address", PlayURL);System.out.print("PlayInfo.PlayURL = " + PlayURL + "\n");}//Base信息System.out.print("VideoBase.Title = " + response.getVideoBase().getTitle() + "\n");resultMap.put("Title", response.getVideoBase().getTitle());//resultMap.put("Address", list);} catch (Exception e) {System.out.print("ErrorMessage = " + e.getLocalizedMessage());}System.out.print("RequestId = " + response.getRequestId() + "\n");return resultMap;}

获取m3u8文件地址后,播放器会解析m3u8文件中的EXT-X-KEY标签中的URI并访问,从而获取到带密文密钥的解密接口URI,此URI为您发起标准加密时传递的加密配置 EncryptConfig中的DecryptKeyUri参数值。
若只允许合法用户才可以访问,那么需要播放器在获取解密密钥时携带您承认的认证信息,认证信息可以通过MtsHlsUriToken参数传入。播放器在解析到解密地址URI时会自动请求解密接口获取解密密钥,拿到解密密钥去解密加密过的ts文件进行播放。

至此视频加解密开发工作完成。如果遇到其他问题,可以在下面评论,也可以在官方文档中查找,也可以提交工单。

阿里云视频加解密VOD开发相关推荐

  1. 海康威视连接阿里云视频监控并sdk开发

    阿里云开启视频监控 NVR连接 效果 国际接入流程 预览失败的话,就是视频编码问题,要改成H.264

  2. CRMEB知识付费二次开发 加密阿里云视频MP4点播链接为m3u8格式 hls blob协议

    只有添加分发加速的域名才能使用HLS加密,同时也要做HTTPS证书添加,不然也会报错. 1.这是电脑端视频播放页面效果 2.这个手机端H5视频播放页面效果 3.在网站后台上传你的视频内容 4.上传完之 ...

  3. 阿里云视频云「 vPaaS 」演绎了怎样的音视频应用开发「未来图景」

    简介:前瞻音视频平台的演进未来 vPaaS是阿里云视频云最新推出的低代码音视频应用开发产品,其中,vPaaS低代码音视频工厂,彻底打破了音视频应用的繁冗技术开发壁垒:vPaaS视频原生应用开发平台,全 ...

  4. Edusoho网校对接阿里云视频vod实现CDN云视频加速播放OSS

    操作系统: Linux 推荐使用Ubuntu,CentOS Web服务器: 推荐Nginx或Apache2 MYSQL数据库: 推荐5.5及以上版本 主要文件目录结构说明 目录 说明 App 应用程序 ...

  5. 阿里云视频云「 vPaaS 」演绎了怎样的音视频应用开发「未来图景」?

    vPaaS是阿里云视频云最新推出的低代码音视频应用开发产品,其中,vPaaS低代码音视频工厂,彻底打破了音视频应用的繁冗技术开发壁垒:vPaaS视频原生应用开发平台,全新定义了音视频应用的开发方式. ...

  6. 透析阿里云视频云「低代码音视频工厂」之能量引擎——vPaaS视频原生应用开发平台

    简介:支撑15分钟上线高品质专属音视频平台 为满足企业用户极速搭建高品质专属音视频业务的需求,阿里云视频云的"低代码音视频工厂"应运而生,但极速而高品质的平台搭建诉求,需要用全新的 ...

  7. 阿里云视频云低代码音视频工厂正式上线,以vPaaS全新定义企业级音视频应用开发

    1月5日,阿里云视频云"低代码音视频工厂vPaaS"正式上线,极大程度降低音视频开发门槛,打破传统音视频技术壁垒,全新定义企业级的音视频应用开发. 低代码音视频工厂基于云原生.音视 ...

  8. 深度解读:阿里云视频直播功能升级

    2022年注定是体育超级大年,冬奥.亚运会.大运会.世界杯等各类大型体育赛事应接不暇.随之而来便是各类赛事直播,客户对直播服务要求变得越来越高,视频直播技术创新显得格外重要.如何利用直播技术创新降低线 ...

  9. 阿里云视频云互动虚拟技术,打造虚拟直播最佳沉浸式体验

    2022是"体育超级大年",冬奥会.亚运会.大运会.世界杯等各类大型体育赛事贯穿全年.由于受到疫情管控和物理空间的限制,赛事直播至关重要,观众体验需求也在不断升级. 于此,企业对直 ...

最新文章

  1. LLVM PHI - if else
  2. 软件公司为什么要加密源代码,而且是前前后后,反反复复
  3. ajax请求后台php数据时查看报错parse error
  4. C# Gooflow+layer弹出层 全js代码
  5. dreamweaver cc php mysql_Dreamweaver cc 2015 +PHP+MySQL动态网站开发案例教程集合
  6. STM32F103C8T6 USART2 配置
  7. mysql 默认my.cnf_在mysql中更改默认的my.cnf路径
  8. TCP 协议(序号和确认号)
  9. Ansible秘钥认证
  10. Visio中关于跨线的设置
  11. multisim怎么设置晶体管rbe_multisim中添加大功率三极管的办法 multisim 三极管设置方法...
  12. new bing 重定向到cn.bing,new bing使用不了的问题
  13. 详解c语言编程库题,详解C语言编程
  14. partition 0 ended too near . couldnt find ntldr
  15. Vue组件,带标题的边框
  16. Windows上如何手动安装Perl模块(ActivePerl)
  17. BIOS升级与CMOS设置
  18. 1998年与2021年的一些高校学生数据等
  19. classid 是什么意思?
  20. ds1302 涓流充电整理

热门文章

  1. 京东量化手把手教你写“法玛三因子”策略
  2. Happy new year!
  3. 简单理解socket(AF_INETSOCK_STREAM,SOCK_DGRAM)
  4. 数据分析:SQL和Python
  5. Javascript之正则表达式学习
  6. springboot根据模板生成pdf文件
  7. ABB 机器人调试总结1
  8. Java微服务学习笔记(一):微服务架构的概念理解
  9. excel数据分析常用技能汇总
  10. 结题答辩常见问题及作答