一. 设计需求
使用阿里云视频点播服务对客户端上传的视频进行转码处理, 并存储到本地. 支持海外用户的大视频文件上传. 解决海外上传的有无问题.
二. 实现思路
客户端向服务端请求凭证, 获取上传地址, 使用阿里云sdk上传视频.
缺点:
目前美国, 澳大利亚没有节点, 这2地视频上传目前使用的是德国法兰克福的节点. 后台请求凭证接口缺少选择上传节点的模块.
后台处理需要时间, 刚刚完成上传的视频无法在中台页面播放.
三. 代码结构
1. 基础jar包. 导入点播Java SDK, 封装阿里云点播各个接口和上传所需的基础参数, 事件监听, 打成jar包, 供服务调用
2. 业务代码分层: config, controller, service, dao, 事件消息处理, 工具类, 定时扫描作业.
四. 基础配置
1. 开通vod服务, mns服务, 并创建mns消息队列
2. 配置vod服务的转码模版组, 设置默认模板, 消息回调设置(这里使用mns)
3. 测试环境与生产环境
默认环境配置可通过控制台或者OpenApi配置,
非默认环境需要调用阿里云的接口, 传appId进行配置. 部分通过OpenApi配置无效.
五. 上传相关代码
MNSListener: 监听器, 使用阿里提供的实现方案.
ClientConfiguration clientConf = new ClientConfiguration();
clientConf.setMaxConnections(maxConnection);
clientConf.setIoReactorThreadCount(ioCount);
CloudAccount account = new CloudAccount(accessKeyId, accessKeySecret, endPoint, clientConf);
//this client need only initialize once
mnsClient = account.getMNSClient();
// 开启监听线程
try {
Thread thread = new Thread(() -> {
CloudQueue cloudQueue = mnsClient.getQueueRef(queue);
while (true) {
Message message = null;
try {
message = cloudQueue.popMessage(15);
} catch (Exception e) {
e.printStackTrace();
}
try {
if (message == null) {
//PollingWaitSeconds expire
//And you could do some work here or do nothing according to your business
} else {
// 使用自定义的msgHandler处理消息
boolean res = mnsMessageHandler.handle(message);
if (res) {
if (delMsg) {
cloudQueue.deleteMessage(message.getReceiptHandle());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
} catch (ClientException ce) {
UploadService: 实现阿里的上传接口. 例如文件流上传接口:
public UploadStreamResponse uploadFile(VodUploadModule file,
String extend,
InputStream inputStream,
String regionId,
String securityToken) {
UploadStreamRequest request = new UploadStreamRequest(accessKeyId, accessKeySecret, file.getTitle(),
file.getFileName(), inputStream);
request.setSecurityToken(securityToken);
request.setApiRegionId(regionId);
request.setCoverURL(file.getCover());
//视频分类ID(可选)
request.setCateId(file.getCateId());
//视频标签,多个用逗号分隔(可选)
request.setTitle(file.getTitle());
if (request.getTemplateGroupId() != null) {
request.setTemplateGroupId(file.getTemplateGroupId());
}
request.setTags(file.getTags());
request.setDescription(file.getDescription());
request.setAppId(file.getAppId());
request.setUserData(VodRequestUserDataUtil.extendWrapper(extend));
return videoUploadExecute(request);
}
private UploadStreamResponse videoUploadExecute(UploadStreamRequest request) {
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(request);
if (response != null && !response.isSuccess()) {
log.info("UploadVideoResponse ErrorCode: {} ErrorMessage: {} VideoId: {}", response.getCode(),
response.getMessage(), response.getVideoId());
}
return response;
}
VodUploadModule: 图片/视频上传实体类:
private String title;
private String tags;
private String url;
private String description;
private String appId;
private Long cateId;
// 视频必须带后缀
private String fileName;
private String cover;
/**
* default/cover
*/
private String imageType;
private String businessType;
private String fileSize;
private String templateGroupId;
// imageExt materialExt
private String fileExt;
MediaExtend: 业务所需实体类. 用户上传记录.
public class MediaExtend {
private Long userId;
private String userType;
private String plt;
private String videoId;
private String imageType;
private String region;
// 当需要下载到本地时, 由上传者预生成视频地址作为转码完成后的访问地址
private String localVideoPath;
private String localCoverPath;
private Date createTime;
private String extend;
}
用户请求凭证controller:
public ServiceResponse getMaterialUploadAddress(@RequestBody FileUploadRequest info) {
String message = null;
try {
MediaExtendUtil.getCurrentUserInfo(info);
MediaExtend extend = info.getMediaExtend();
if (!extend.validateMediaExtend()) {
message = "Extend 不能为空";
}
CreateUploadAttachedMediaResponse response = uploadService.createUploadAttachedMedia(info, extend);
return apiResponse(ResponseCode.SC_OK, message, response);
} catch (Exception e) {
log.error("ErrorMessage = " + e.getMessage());
return apiResponse(ResponseCode.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
}
凭证service:
public CreateUploadAttachedMediaResponse createUploadAttachedMedia(FileUploadRequest info, MediaExtend extend) throws Exception {
CreateUploadAttachedMediaRequest request = new CreateUploadAttachedMediaRequest();
request.setAppId(appId);
BeanUtils.copyProperties(info, request);
request.setMediaExt(info.getFileExt());
DefaultAcsClient client = aliConfigService.getAcsClient(regionId);
// extend包含了视频上传信息, 可在视频信息和转码事件中获取
request.setUserData(VodRequestUserDataUtil.extendWrapper(JSONObject.toJSONString(extend)));
return (CreateUploadAttachedMediaResponse)client.getAcsResponse(request)
}
中台或App在获取到凭证后, 使用阿里云SDK上传视频.
分享生成阿里云配置链接的代码(已删除敏感信息):
public class AliSignatureUtil {
/*对所有参数名称和参数值做URL编码*/
public static List getAllParams(Map publicParams, Map privateParams) {
List encodeParams = new ArrayList();
if (publicParams != null) {
for (Map.Entry entry : publicParams.entrySet()) {
//将参数和值都urlEncode一下。
String encodeKey = percentEncode(entry.getKey());
String encodeVal = percentEncode(entry.getValue());
encodeParams.add(encodeKey + "=" + encodeVal);
}
}
if (privateParams != null) {
for (String key : privateParams.keySet()) {
String value = privateParams.get(key);
//将参数和值都urlEncode一下。
String encodeKey = percentEncode(key);
String encodeVal = percentEncode(value);
encodeParams.add(encodeKey + "=" + encodeVal);
}
}
return encodeParams;
}
/*获取 CanonicalizedQueryString*/
public static String getCQS(List allParams) {
ParamsComparator paramsComparator = new ParamsComparator();
Collections.sort(allParams, paramsComparator);
StringBuffer cqString = new StringBuffer("");
for (int i = 0; i < allParams.size(); i++) {
cqString.append(allParams.get(i));
if (i != allParams.size() - 1) {
cqString.append("&");
}
}
return cqString.toString();
}
/*字符串参数比较器,按字母序升序*/
public static class ParamsComparator implements Comparator {
@Override
public int compare(String lhs, String rhs) {
return lhs.compareTo(rhs);
}
}
/*构造待签名的字符串*/
//String StringToSign = httpMethod + "&" + percentEncode("/") + "&" + percentEncode(CanonicalizedQueryString);
/*特殊字符替换为转义字符*/
public static String percentEncode(String value) {
try {
String urlEncodeOrignStr = URLEncoder.encode(value, "UTF-8");
String plusReplaced = urlEncodeOrignStr.replace("+", "%20");
String starReplaced = plusReplaced.replace("*", "%2A");
String waveReplaced = starReplaced.replace("%7E", "~");
return waveReplaced;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return value;
}
public static byte[] hmacSHA1Signature(String accessKeySecret, String stringToSign) {
try {
String key = accessKeySecret + "&";
try {
SecretKeySpec signKey = new SecretKeySpec(key.getBytes(), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signKey);
return mac.doFinal(stringToSign.getBytes());
} catch (Exception e) {
throw new SignatureException("Failed to generate HMAC : " + e.getMessage());
}
} catch (SignatureException e) {
e.printStackTrace();
}
return null;
}
public static String newStringByBase64(byte[] bytes)
throws UnsupportedEncodingException {
if (bytes == null || bytes.length == 0) {
return null;
}
return new BASE64Encoder().encode(bytes);
}
/*生成当前UTC时间戳Time*/
public static String generateTimestamp() {
Date date = new Date(System.currentTimeMillis());
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
df.setTimeZone(new SimpleTimeZone(0, "GMT"));
return df.format(date);
}
public static String generateRandom() {
String signatureNonce = UUID.randomUUID().toString();
return signatureNonce;
}
/* public static void main(String[] args) {
Map publicParams = new HashMap<>();
Map privateParams = new HashMap<>();
publicParams.put("Format", "json");
publicParams.put("Version", "2017-03-21");
publicParams.put("AccessKeyId", "");
publicParams.put("SignatureMethod", "HMAC-SHA1");
publicParams.put("Timestamp", generateTimestamp());
publicParams.put("SignatureVersion", "1.0");
publicParams.put("SignatureNonce", generateRandom());
privateParams.put("Action", "SetMessageCallback");
privateParams.put("CallbackType", "MNS");
privateParams.put("MnsEndpoint", "https://111111.mns.cn-shanghai.aliyuncs.com/");
privateParams.put("MnsQueueName", "name");
//privateParams.put("EventTypeList", "ALL");
privateParams.put("EventTypeList", "FileUploadComplete,ImageUploadComplete,TranscodeComplete,SnapshotComplete,UploadByURLComplete,CreateAuditComplete");
privateParams.put("AppId", "app-1000002");
List allParams = getAllParams(publicParams, privateParams);
String cqs = getCQS(allParams);
System.out.println(cqs);
String httpMethod = "GET";
String stringToSign = httpMethod + "&" + percentEncode("/") + "&" + percentEncode(cqs);
String signature = null;
String domain = "http://vod.eu-central-1.aliyuncs.com/";
try {
signature = newStringByBase64(hmacSHA1Signature("", stringToSign));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = domain + "?" + cqs + "&Signature=" + signature;
System.out.println(url);
}*/
/*public static void main(String[] args) {
Map publicParams = new HashMap<>();
Map privateParams = new HashMap<>();
publicParams.put("Format", "json");
publicParams.put("Version", "2017-03-21");
publicParams.put("AccessKeyId", "");
publicParams.put("SignatureMethod", "HMAC-SHA1");
publicParams.put("Timestamp", generateTimestamp());
publicParams.put("SignatureVersion", "1.0");
publicParams.put("SignatureNonce", generateRandom());
privateParams.put("Action", "SetMessageCallback");
privateParams.put("CallbackType", "MNS");
privateParams.put("MnsEndpoint", "https://111111.mns.cn-shanghai.aliyuncs.com/");
privateParams.put("MnsQueueName", "name");
//privateParams.put("EventTypeList", "ALL");
privateParams.put("EventTypeList", "FileUploadComplete,ImageUploadComplete,TranscodeComplete,SnapshotComplete,UploadByURLComplete,CreateAuditComplete");
privateParams.put("AppId", "app-1000002");
List allParams = getAllParams(publicParams, privateParams);
String cqs = getCQS(allParams);
System.out.println(cqs);
String httpMethod = "GET";
String stringToSign = httpMethod + "&" + percentEncode("/") + "&" + percentEncode(cqs);
String signature = null;
String domain = "http://vod.eu-central-1.aliyuncs.com/";
try {
signature = newStringByBase64(hmacSHA1Signature("", stringToSign));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = domain + "?" + cqs + "&Signature=" + signature;
System.out.println(url);
}*/
/**
* 添加转码模板组配置
*/
public static AddTranscodeTemplateGroupResponse addTranscodeTemplateGroup(DefaultAcsClient client) throws Exception {
AddTranscodeTemplateGroupRequest request = new AddTranscodeTemplateGroupRequest();
//转码模板ID
request.setName("video-transcode-LD-1");
request.setAppId("app-1000002");
System.out.println(buildTranscodeTemplateList().toJSONString());
request.setTranscodeTemplateList(buildTranscodeTemplateList().toJSONString());
return client.getAcsResponse(request);
}
/**
* 构建需要添加的转码模板配置数据
*
* @return
*/
public static JSONArray buildTranscodeTemplateList() {
JSONObject transcodeTemplate = new JSONObject();
//模板名称
transcodeTemplate.put("TemplateName", "video-transcode-LD-1");
//清晰度
transcodeTemplate.put("Definition", "SD");
//视频流转码配置
JSONObject video = new JSONObject();
// 文档地址https://help.aliyun.com/document_detail/52839.html?spm=a2c4g.11186623.2.16.6bb96d22lCXeg4#h2--transcodetemplategroup-div-id-transcodetemplategroup-div-36
// 转码视频流配置Video
//分辨率(宽x高)
video.put("Width", 960);
//码率(Kbps)
video.put("Bitrate", 900);
//帧率(fps)
video.put("Fps", 25);
video.put("Remove", false);
//编码格式
video.put("Codec", "H.264");
// 关键帧最大间隔(帧)
video.put("Gop", "250");
transcodeTemplate.put("Video", video);
//音频流转码配置
JSONObject audio = new JSONObject();
// 编码格式
audio.put("Codec", "AAC");
// 码率(Kbps)
audio.put("Bitrate", "96");
// 声道数
audio.put("Channels", "2");
// 采样率
audio.put("Samplerate", "44100");
transcodeTemplate.put("Audio", audio);
// 封装容器
JSONObject container = new JSONObject();
// 封装格式
container.put("Format", "mp4");
transcodeTemplate.put("Container", container);
//条件转码配置
JSONObject transconfig = new JSONObject();
transconfig.put("IsCheckReso", false);
transconfig.put("IsCheckResoFail", false);
transconfig.put("IsCheckVideoBitrate", false);
transconfig.put("IsCheckVideoBitrateFail", false);
transconfig.put("IsCheckAudioBitrate", false);
transconfig.put("IsCheckAudioBitrateFail", false);
transcodeTemplate.put("TransConfig", transconfig);
//加密配置(只支持m3u8)
//JSONObject encryptSetting = new JSONObject();
//encryptSetting.put("EncryptType", "Private");
//transcodeTemplate.put("EncryptSetting", encryptSetting);
//水印ID(多水印关联)
/* JSONArray watermarkIdList = new JSONArray();
watermarkIdList.add("263261bdc1ff65782f8995c6dd22a16a");
//USER_DEFAULT_WATERMARK 代表默认水印ID
watermarkIdList.add("USER_DEFAULT_WATERMARK");
transcodeTemplate.put("WatermarkIds", watermarkIdList);*/
JSONArray transcodeTemplateList = new JSONArray();
transcodeTemplateList.add(transcodeTemplate);
return transcodeTemplateList;
}
/* */
/**
* 以下为调用示例
*/
/* public static void main(String[] args) throws ClientException {
DefaultAcsClient client = initVodClient("", "");
AddTranscodeTemplateGroupResponse response = new AddTranscodeTemplateGroupResponse();
try {
response = addTranscodeTemplateGroup(client);
System.out.println("TranscodeTemplateGroupId = " + response.getTranscodeTemplateGroupId());
} catch (Exception e) {
System.out.println("ErrorMessage = " + e.getLocalizedMessage());
}
System.out.println("RequestId = " + response.getRequestId());
}*/
public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {
/* String regionId = "cn-shanghai"; // 点播服务接入区域
String regionId2 = "ap-southeast-1";
String regionId3 = "ap-southeast-5";
String regionId4 = "ap-south-1";*/
String regionId5 = "eu-central-1";
//String regionId6 = "ap-northeast-1";
DefaultProfile profile = DefaultProfile.getProfile(regionId5, accessKeyId, accessKeySecret);
DefaultAcsClient client = new DefaultAcsClient(profile);
return client;
}
public static ListTranscodeTemplateGroupResponse listTranscodeTemplateGroup(DefaultAcsClient client) throws Exception {
ListTranscodeTemplateGroupRequest request = new ListTranscodeTemplateGroupRequest();
request.setAppId("app-1000002");
return client.getAcsResponse(request);
}
/**
* 以下为调用示例
*/
/*public static void main(String[] args) throws ClientException {
DefaultAcsClient client = initVodClient("", "");
ListTranscodeTemplateGroupResponse response = new ListTranscodeTemplateGroupResponse();
try {
response = listTranscodeTemplateGroup(client);
System.out.println(JSONObject.toJSONString(response.getTranscodeTemplateGroupList()));
} catch (Exception e) {
System.out.println("ErrorMessage = " + e.getLocalizedMessage());
}
System.out.println("RequestId = " + response.getRequestId());
}*/
/**
* 设置默认转码模板组
*/
public static SetDefaultTranscodeTemplateGroupResponse setDefaultTranscodeTemplateGroup(DefaultAcsClient client) throws Exception {
SetDefaultTranscodeTemplateGroupRequest request = new SetDefaultTranscodeTemplateGroupRequest();
request.setTranscodeTemplateGroupId("111111111111111111");
return client.getAcsResponse(request);
}
/**
* 以下为调用示例
*/
/*public static void main(String[] args) throws ClientException {
DefaultAcsClient client = initVodClient("", "");
SetDefaultTranscodeTemplateGroupResponse response = new SetDefaultTranscodeTemplateGroupResponse();
try {
response = setDefaultTranscodeTemplateGroup(client);
} catch (Exception e) {
System.out.println("ErrorMessage = " + e.getLocalizedMessage());
}
System.out.println("RequestId = " + response.getRequestId());
}*/
}