package com.ld.vps.manager; import cn.hutool.core.date.DateUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONUtil; import com.huaweicloud.sdk.core.auth.BasicCredentials; import com.huaweicloud.sdk.core.auth.ICredential; import com.huaweicloud.sdk.core.exception.ServiceResponseException; import com.huaweicloud.sdk.vod.v1.VodClient; import com.huaweicloud.sdk.vod.v1.model.*; import com.huaweicloud.sdk.vod.v1.region.VodRegion; import com.ld.vps.bean.UploadTaskInfo; import com.ld.vps.bean.UploadTaskStatus; import com.ld.vps.dao.UploadTaskRepository; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.security.MessageDigest; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.TimeZone; public class HwVodUploader { Logger logger = LoggerFactory.getLogger(HwVodUploader.class); public static final String COVER_PATH = "../cover.jpg"; // 设置缓冲区大小,每次读取文件分段的大小,根据情况自行设定 public static final int BUFFER_SIZE = 1024 * 1024 * 1; // 4MB // 区域 public static final String REGION_NORTH4 = "cn-north-4"; public static final String REGION_NORTH1 = "cn-north-1"; public static final String REGION_EAST2 = "cn-east-2"; // ak/sk private static final String AK = "T1BPEDBJLCWV62HJWA9A"; private static final String SK = "0jCNb5ZlkIfc5UIR0tHadybChuaS45ChvUp8a5zL"; /** * 上传任务信息 */ private UploadTaskInfo taskInfo; private UploadTaskRepository taskRepo; /** * @param taskInfo 上传任务信息 */ public HwVodUploader(UploadTaskInfo taskInfo, UploadTaskRepository taskRepository) { this.taskInfo = taskInfo; this.taskRepo = taskRepository; } public void start() { this.uploadPartFile(); } /** * 分段上传 */ private void uploadPartFile() { if (taskInfo.getStatus() == UploadTaskStatus.FILE_SUCCESS) { return; } // 校验一下文件路径和文件 File file = validFile(this.taskInfo.getFilePath()); try { taskInfo.setStatus(UploadTaskStatus.UPLOADING); taskInfo.setFileSize(file.length()); String fileName = file.getName(); // 此处仅以MP4文件示例,其他格式可参考官网说明 String fileType = "MP4"; String fileContentType = "video/mp4"; // FileInfo fileInfo = new FileInfo(filePath, file); // 1.初始化鉴权并获取vodClient VodClient vodClient = this.createVodClient(); logger.info("华为VOID开始创建媒资:" + file.getName()); // 2.创建点播媒资 com.ld.vps.bean.AssetInfo asset; if (taskInfo.getAssetInfo() != null) { asset = JSONUtil.toBean(taskInfo.getAssetInfo(), com.ld.vps.bean.AssetInfo.class); } else { CreateAssetByFileUploadReq reqbody = this.buildFileUploadReq(fileName, fileType, null, null); CreateAssetByFileUploadResponse resp = this.createAssetByFileUpload(vodClient, reqbody); asset = new com.ld.vps.bean.AssetInfo(); asset.setAssetId(resp.getAssetId()); asset.setBucket(resp.getTarget().getBucket()); asset.setObject(resp.getTarget().getObject()); taskInfo.setAssetInfo(JSONUtil.toJsonStr(asset)); taskInfo.setAssetId(resp.getAssetId()); taskRepo.save(taskInfo); } // 3.获取初始化上传任务授权 ShowAssetTempAuthorityResponse initAuthResponse = this.initPartUploadAuthority(vodClient, asset, fileContentType); // 4.初始化上传任务 String uploadId = taskInfo.getParUploadId(); if (uploadId == null) { uploadId = this.initPartUpload(initAuthResponse.getSignStr(), fileContentType); taskInfo.setParUploadId(uploadId); taskRepo.save(taskInfo); } // 文件分段计数 int partNumber = taskInfo.getPartNumber(); // 缓冲区 byte[] fileByte = new byte[BUFFER_SIZE]; // 记录读取的长度 int readLength = 0; // 7.读取文件内容, 循环5-6步上传所有分段 FileInputStream fis = new FileInputStream(file); fis.skip(taskInfo.getUploadedSize()); // MD5 MessageDigest md = MessageDigest.getInstance("MD5"); while ((readLength = fis.read(fileByte)) > 0) { // 读取的长度小于缓冲区长度,复制有效长度内数据,用于最后一段 if (readLength < BUFFER_SIZE) { fileByte = Arrays.copyOf(fileByte, readLength); } // 先md5,再base64 encode byte[] digest = md.digest(fileByte); String contentMd5 = new String(Base64.encodeBase64(digest)); logger.debug("该文件第" + (partNumber) + "段MD5为: " + contentMd5); // 5.获取上传分段的授权 ShowAssetTempAuthorityResponse partUploadAuthorityResponse = this.getPartUploadAuthority(vodClient, fileContentType, asset, contentMd5, uploadId, partNumber); // 6.上传分段 this.uploadPartFile(partUploadAuthorityResponse.getSignStr(), fileByte, contentMd5); // 段号自增 partNumber++; taskInfo.setUploadedSize(taskInfo.getUploadedSize() + readLength); taskInfo.setPartNumber(partNumber); taskRepo.save(taskInfo); } fis.close(); // 8.获取已上传分段的授权 ShowAssetTempAuthorityResponse listPartUploadAuthorityResponse = this.listUploadedPartAuthority(vodClient, asset, uploadId); // 9.获取已上传的分段 String partInfo = this.listUploadedPart(listPartUploadAuthorityResponse.getSignStr()); // 10.获取合并段授权 ShowAssetTempAuthorityResponse mergePartUploadAuthorityResponse = this.mergeUploadedPartAuthority(vodClient, asset, uploadId); // 11.合并上传分段 this.mergeUploadedPart(mergePartUploadAuthorityResponse.getSignStr(), partInfo); // 12.确认媒资上传 this.confirmUploaded(vodClient, asset); logger.info("创建媒资结束 assetId:" + asset.getAssetId()); // 13.启动转码、封面截图处理流程 this.assetProcess(vodClient, asset.getAssetId()); this.getAssetDetail(vodClient, asset.getAssetId()); taskInfo.setAssetId(asset.getAssetId()); taskInfo.setStatus(UploadTaskStatus.FILE_SUCCESS); taskInfo.setEndTime(DateUtil.formatDateTime(new Date())); } catch (ServiceResponseException e) { e.printStackTrace(); logger.error(String.valueOf(e.getHttpStatusCode())); logger.error(e.getRequestId()); logger.error(e.getErrorCode()); logger.error(e.getErrorMsg()); taskInfo.setStatus(UploadTaskStatus.FAILED); taskInfo.setErrMsg(e.getMessage()); } catch (Exception e) { e.printStackTrace(); taskInfo.setStatus(UploadTaskStatus.FAILED); taskInfo.setErrMsg(e.getMessage()); }finally { taskRepo.save(taskInfo); } } private void getAssetDetail(VodClient vodClient, String assetId) { ShowAssetDetailRequest req = new ShowAssetDetailRequest(); req.setAssetId(assetId); ShowAssetDetailResponse resp = vodClient.showAssetDetail(req); String url = resp.getBaseInfo().getVideoUrl(); logger.debug("视频原始url:" + url); taskInfo.setVideoUrl(url); } private void assetProcess(VodClient vodClient, String assetId) { AssetProcessReq body = new AssetProcessReq(); body.setAssetId(assetId); body.setTemplateGroupName("system_template_group"); body.setAutoEncrypt(0); CreateAssetProcessTaskRequest taskReq = new CreateAssetProcessTaskRequest(); taskReq.setBody(body); CreateAssetProcessTaskResponse taskResp = vodClient.createAssetProcessTask(taskReq); logger.debug("媒资处理完成,task:" + taskResp.toString()); } /** * 校验文件路径和文件 * * @param filePath */ private File validFile(String filePath) { if (StringUtils.isEmpty(filePath)) { throw new RuntimeException("输入的文件路径为空!"); } File file = new File(filePath); if (!file.exists()) { throw new RuntimeException("文件不存在!"); } else if (file.isDirectory()) { try { throw new RuntimeException(file.getCanonicalPath() + "这是一个目录!"); } catch (IOException e) { throw new RuntimeException(e); } } return file; } /** * 1.构建鉴权 * * @return */ private VodClient createVodClient() { ICredential auth = new BasicCredentials() .withAk(AK) .withSk(SK); return VodClient.newBuilder() .withCredential(auth) .withRegion(VodRegion.valueOf(REGION_NORTH4)) .build(); } /** * 2.创建媒资 * * @param client * @param reqbody * @return */ private CreateAssetByFileUploadResponse createAssetByFileUpload(VodClient client, CreateAssetByFileUploadReq reqbody) { logger.debug("createAssetByFileUpload start"); CreateAssetByFileUploadRequest request = new CreateAssetByFileUploadRequest(); // 设置 X-Sdk-Date参数,ak/sk认证必传 request.setXSdkDate(getXSdkDate()); // 设置上传参数 request.withBody(reqbody); // 调用创建媒资 CreateAssetByFileUploadResponse response = client.createAssetByFileUpload(request); logger.debug("createAssetByFileUpload end; createAssetResponse:" + response.toString()); return response; } /** * 构建创建媒资请求参数 * * @param fileName * @param videoName * @param title * @return */ private CreateAssetByFileUploadReq buildFileUploadReq(String fileName, String videoType, String videoName, String title) { CreateAssetByFileUploadReq req = new CreateAssetByFileUploadReq(); req.withVideoName(StringUtils.isNotEmpty(videoName) ? videoName : fileName); req.withTitle(StringUtils.isNotEmpty(title) ? title : fileName); // 设置媒资类型 req.withVideoType(videoType); return req; } /** * 取 X-Sdk-Date 参数,当前UTC时间, 时间格式 yyyyHHddTHHmmssZ 例如: 20240312T092514Z * * @return */ private String getXSdkDate() { TimeZone zone = TimeZone.getTimeZone("UTC"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"); return LocalDateTime.now(zone.toZoneId()).format(formatter); } /** * 3.获取初始化上传任务授权 * * @param client * @param asset */ private ShowAssetTempAuthorityResponse initPartUploadAuthority(VodClient client, com.ld.vps.bean.AssetInfo asset, String fileContentType) { logger.debug("获取初始化上传任务授权 initPartUploadAuthority start"); ShowAssetTempAuthorityRequest request = new ShowAssetTempAuthorityRequest(); request.setXSdkDate(getXSdkDate()); // 设置参数 request.withHttpVerb("POST"); request.withBucket(asset.getBucket()); request.withObjectKey(asset.getObject()); request.withContentType(fileContentType); // 发送初始化请求 ShowAssetTempAuthorityResponse response = client.showAssetTempAuthority(request); logger.debug("获取初始化上传任务授权 initPartUploadAuthority end; response: " + response.toString()); return response; } /** * 4.初始化分段上传 * * @param signStr * @param contentType * @return */ private String initPartUpload(String signStr, String contentType) throws DocumentException { logger.debug("初始化分段上传 initPartUpload start"); HttpResponse response = HttpRequest.post(signStr).header("Content-type", contentType).execute(); logger.debug(response.body()); Document document = DocumentHelper.parseText(response.body()); Element root = document.getRootElement(); Element u = root.element("UploadId"); logger.debug("初始化分段上传 initPartUpload end; UploadId:" + u.getText()); return u.getText(); } /** * 5.获取分段上传授权 * * @param client * @param fileContentType * @param assetResponse * @param contentMd5 * @param uploadId * @param partNumber * @return */ private ShowAssetTempAuthorityResponse getPartUploadAuthority(VodClient client, String fileContentType, com.ld.vps.bean.AssetInfo assetResponse, String contentMd5, String uploadId, int partNumber) { logger.debug("获取分段上传授权 getPartUploadAuthority start; partNumber: " + partNumber); ShowAssetTempAuthorityRequest request = new ShowAssetTempAuthorityRequest(); request.setXSdkDate(getXSdkDate()); request.withHttpVerb("PUT"); request.withBucket(assetResponse.getBucket()); request.withObjectKey(assetResponse.getObject()); request.withContentType(fileContentType); request.withContentMd5(contentMd5); request.withUploadId(uploadId); request.withPartNumber(partNumber); ShowAssetTempAuthorityResponse response = client.showAssetTempAuthority(request); logger.debug("获取分段上传授权 getPartUploadAuthority end; partNumber: " + partNumber + "; response" + response.toString()); return response; } /** * 6.上传分段 * * @param signStr * @param fileByte * @param contentMd5 */ private void uploadPartFile(String signStr, byte[] fileByte, String contentMd5) { logger.debug("上传分段 uploadPartFile start"); HttpResponse response = HttpRequest.put(signStr) // 此处contentMd5不需要转义 .header("Content-MD5", contentMd5) .header("Content-Type", "application/octet-stream") .body(fileByte).execute(); logger.debug(response.toString()); if (response.getStatus() != 200) { throw new RuntimeException("上传分段 uploadPartFile end; 上传失败!"); } logger.debug("上传分段 uploadPartFile end"); } /** * 8.获取列举已上传段的授权 * * @param client * @param assetResponse * @param uploadId * @return */ private ShowAssetTempAuthorityResponse listUploadedPartAuthority(VodClient client, com.ld.vps.bean.AssetInfo assetResponse, String uploadId) { logger.debug("获取列举已上传段的授权 listUploadedPartAuthority start"); ShowAssetTempAuthorityRequest request = new ShowAssetTempAuthorityRequest(); request.setXSdkDate(getXSdkDate()); request.withHttpVerb("GET"); request.withBucket(assetResponse.getBucket()); request.withObjectKey(assetResponse.getObject()); request.withUploadId(uploadId); ShowAssetTempAuthorityResponse response = client.showAssetTempAuthority(request); logger.debug("获取列举已上传段的授权 listUploadedPartAuthority end; response: " + response.toString()); return response; } /** * 9.查询已上传的分段 * * @param signStr * @return */ public String listUploadedPart(String signStr) throws DocumentException { logger.debug("查询已上传的分段 listUploadedPart start"); int partNumberMarker = 0; Element mergerRoot = DocumentHelper.createElement("CompleteMultipartUpload"); while (true) { //列举分段 HttpResponse response = HttpRequest.get(signStr + "&part-number-marker=" + partNumberMarker).execute(); logger.debug("listUploadedPartResponse:" + response.body()); Document responseDocument = DocumentHelper.parseText(response.body()); Element rootResponse = responseDocument.getRootElement(); List elementsResponse = rootResponse.elements("Part"); Element partNumberMarkerElement = rootResponse.element("NextPartNumberMarker"); for (Element e : elementsResponse) { Element te = DocumentHelper.createElement("Part"); te.add(e.element("PartNumber").createCopy()); te.add(e.element("ETag").createCopy()); mergerRoot.add(te); } partNumberMarker = Integer.valueOf(partNumberMarkerElement.getText()); if (partNumberMarker % 1000 != 0) { break; } } logger.debug(mergerRoot.asXML()); logger.debug("查询已上传的分段 listUploadedPart end"); return mergerRoot.asXML(); } /** * 10.获取合并段授权 * * @param client * @param assetResponse * @param uploadId * @return */ public ShowAssetTempAuthorityResponse mergeUploadedPartAuthority(VodClient client, com.ld.vps.bean.AssetInfo assetResponse, String uploadId) { logger.debug("获取合并段授权 mergeUploadedPartAuthority start"); ShowAssetTempAuthorityRequest request = new ShowAssetTempAuthorityRequest(); request.setXSdkDate(getXSdkDate()); request.withHttpVerb("POST"); request.withBucket(assetResponse.getBucket()); request.withObjectKey(assetResponse.getObject()); request.withUploadId(uploadId); ShowAssetTempAuthorityResponse response = client.showAssetTempAuthority(request); logger.debug("获取合并段授权 mergeUploadedPartAuthority end; response: " + response.toString()); return response; } /** * 11.合并分段 */ public void mergeUploadedPart(String signStr, String partInfo) { logger.debug("合并分段 mergeUploadedPart start"); // 请求消息头中增加“Content-Type”,值设置为“application/xml”。 HttpResponse response = HttpRequest.post(signStr) .header("Content-Type", "application/xml") .body(partInfo) .execute(); logger.debug(response.toString()); if (response.getStatus() != 200) { throw new RuntimeException("合并分段 mergeUploadedPart end; 合并段失败!"); } logger.debug("合并分段 mergeUploadedPart end"); } /** * 12.确认上传完成 */ public void confirmUploaded(VodClient client, com.ld.vps.bean.AssetInfo assetResponse) { logger.debug("确认上传完成 confirmUploaded start"); ConfirmAssetUploadRequest request = new ConfirmAssetUploadRequest(); request.setXSdkDate(getXSdkDate()); ConfirmAssetUploadReq body = new ConfirmAssetUploadReq(); body.withStatus(ConfirmAssetUploadReq.StatusEnum.fromValue("CREATED")); body.withAssetId(assetResponse.getAssetId()); request.withBody(body); ConfirmAssetUploadResponse response = client.confirmAssetUpload(request); logger.debug("上传完成,assetId:" + response.toString()); } }