You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

747 lines
35KB

  1. # -*- coding: UTF-8 -*-
  2. import json
  3. import oss2
  4. import base64
  5. import time
  6. from aliyunsdkcore import client
  7. from aliyunsdkvod.request.v20170321 import CreateUploadVideoRequest
  8. from aliyunsdkvod.request.v20170321 import RefreshUploadVideoRequest
  9. from aliyunsdkvod.request.v20170321 import CreateUploadImageRequest
  10. from aliyunsdkvod.request.v20170321 import CreateUploadAttachedMediaRequest
  11. from vodsdk.AliyunVodUtils import *
  12. from loguru import logger
  13. VOD_MAX_TITLE_LENGTH = 128
  14. VOD_MAX_DESCRIPTION_LENGTH = 1024
  15. class AliyunVodUploader:
  16. __slots__ = (
  17. '__requestId',
  18. '__accessKeyId',
  19. '__accessKeySecret',
  20. '__ecsRegion',
  21. '__vodApiRegion',
  22. '__connTimeout',
  23. '__bucketClient',
  24. '__maxRetryTimes',
  25. '__vodClient',
  26. '__EnableCrc',
  27. '__multipartThreshold',
  28. '__multipartPartSize',
  29. '__multipartThreadsNum'
  30. )
  31. def __init__(self, accessKeyId, accessKeySecret, requestId, ecsRegionId=None):
  32. """
  33. constructor for VodUpload
  34. :param accessKeyId: string, access key id
  35. :param accessKeySecret: string, access key secret
  36. :param ecsRegion: string, 部署迁移脚本的ECS所在的Region,详细参考:https://help.aliyun.com/document_detail/40654.html,如:cn-beijing
  37. :return
  38. """
  39. self.__requestId = requestId
  40. # LogUtils.init_log(context)
  41. self.__accessKeyId = accessKeyId
  42. self.__accessKeySecret = accessKeySecret
  43. self.__ecsRegion = ecsRegionId
  44. self.__vodApiRegion = None
  45. self.__connTimeout = 60
  46. self.__bucketClient = None
  47. self.__maxRetryTimes = 5
  48. self.__vodClient = None
  49. self.__EnableCrc = True
  50. # 分片上传参数
  51. self.__multipartThreshold = 10 * 1024 * 1024 # 分片上传的阈值,超过此值开启分片上传
  52. self.__multipartPartSize = 10 * 1024 * 1024 # 分片大小,单位byte
  53. self.__multipartThreadsNum = 3 # 分片上传时并行上传的线程数,暂时为串行上传,不支持并行,后续会支持。
  54. # 设置apiRegion为cn-shanghai, 初始化客户端self.__vodClient
  55. self.setApiRegion('cn-shanghai')
  56. logger.info("初始化阿里云视频上传sdk,连接超时时间:{}, 重试次数:{}, requestId:{}", self.__connTimeout,
  57. self.__maxRetryTimes, requestId)
  58. def setApiRegion(self, apiRegion):
  59. """
  60. 设置VoD的接入地址,中国大陆为cn-shanghai,海外支持ap-southeast-1(新加坡)等区域,详情参考:https://help.aliyun.com/document_detail/98194.html
  61. :param apiRegion: 接入地址的Region英文表示
  62. :return:
  63. """
  64. self.__vodApiRegion = apiRegion
  65. self.__vodClient = self.__initVodClient()
  66. def __initVodClient(self):
  67. return client.AcsClient(self.__accessKeyId, self.__accessKeySecret, self.__vodApiRegion,
  68. auto_retry=True, max_retry_time=self.__maxRetryTimes, timeout=self.__connTimeout)
  69. def setMultipartUpload(self, multipartThreshold=10 * 1024 * 1024, multipartPartSize=10 * 1024 * 1024,
  70. multipartThreadsNum=1):
  71. if multipartThreshold > 0:
  72. self.__multipartThreshold = multipartThreshold
  73. if multipartPartSize > 0:
  74. self.__multipartPartSize = multipartPartSize
  75. if multipartThreadsNum > 0:
  76. self.__multipartThreadsNum = multipartThreadsNum
  77. def setEnableCrc(self, isEnable=False):
  78. self.__EnableCrc = True if isEnable else False
  79. @catch_error
  80. def uploadLocalVideo(self, uploadVideoRequest, startUploadCallback=None):
  81. """
  82. 上传本地视频或音频文件到点播,最大支持48.8TB的单个文件,暂不支持断点续传
  83. :param uploadVideoRequest: UploadVideoRequest类的实例,注意filePath为本地文件的绝对路径
  84. :param startUploadCallback为获取到上传地址和凭证(uploadInfo)后开始进行文件上传时的回调,可用于记录上传日志等;uploadId为设置的上传ID,可用于关联导入视频。
  85. :return
  86. """
  87. uploadInfo = self.__createUploadVideo(uploadVideoRequest)
  88. if startUploadCallback:
  89. startUploadCallback(uploadVideoRequest.uploadId, uploadInfo)
  90. headers = self.__getUploadHeaders(uploadVideoRequest)
  91. self.__uploadOssObjectWithRetry(uploadVideoRequest.filePath, uploadInfo['UploadAddress']['FileName'],
  92. uploadInfo, headers)
  93. return uploadInfo
  94. @catch_error
  95. def uploadWebVideo(self, uploadVideoRequest, startUploadCallback=None):
  96. """
  97. 上传网络视频或音频文件到点播,最大支持48.8TB的单个文件(需本地磁盘空间足够);会先下载到本地临时目录,再上传到点播存储
  98. :param uploadVideoRequest: UploadVideoRequest类的实例,注意filePath为网络文件的URL地址
  99. :return
  100. """
  101. # 下载文件
  102. uploadVideoRequest = self.__downloadWebMedia(uploadVideoRequest)
  103. # 上传到点播
  104. uploadInfo = self.__createUploadVideo(uploadVideoRequest)
  105. if startUploadCallback:
  106. startUploadCallback(uploadVideoRequest.uploadId, uploadInfo)
  107. headers = self.__getUploadHeaders(uploadVideoRequest)
  108. self.__uploadOssObjectWithRetry(uploadVideoRequest.filePath, uploadInfo['UploadAddress']['FileName'],
  109. uploadInfo, headers)
  110. # 删除本地临时文件
  111. os.remove(uploadVideoRequest.filePath)
  112. return uploadInfo['VideoId']
  113. @catch_error
  114. def uploadLocalM3u8(self, uploadVideoRequest, sliceFilePaths=None):
  115. """
  116. 上传本地m3u8视频或音频文件到点播,m3u8文件和分片文件默认在同一目录
  117. :param uploadVideoRequest: UploadVideoRequest类的实例,注意filePath为本地m3u8索引文件的绝对路径,
  118. 且m3u8文件的分片信息必须是相对地址,不能含有URL或本地绝对路径
  119. :param sliceFilePaths: list, 分片文件的本地路径列表,例如:['/opt/m3u8_video/sample_001.ts', '/opt/m3u8_video/sample_002.ts']
  120. sliceFilePaths为None时,会按照同一目录去解析分片地址;如不在同一目录等原因导致解析有误,可自行组装分片地址
  121. :return
  122. """
  123. if sliceFilePaths is None:
  124. sliceFilePaths = self.parseLocalM3u8(uploadVideoRequest.filePath)
  125. if (not isinstance(sliceFilePaths, list)) or len(sliceFilePaths) <= 0:
  126. raise AliyunVodException('InvalidM3u8SliceFile', 'M3u8 slice files invalid',
  127. 'sliceFilePaths invalid or m3u8 index file error')
  128. # 上传到点播的m3u8索引文件会重写,以此确保分片地址都为相对地址
  129. downloader = AliyunVodDownloader()
  130. m3u8LocalDir = downloader.getSaveLocalDir() + '/' + AliyunVodUtils.getStringMd5(uploadVideoRequest.fileName)
  131. downloader.setSaveLocalDir(m3u8LocalDir)
  132. m3u8LocalPath = m3u8LocalDir + '/' + os.path.basename(uploadVideoRequest.fileName)
  133. self.__rewriteM3u8File(uploadVideoRequest.filePath, m3u8LocalPath, True)
  134. # 获取上传凭证
  135. uploadVideoRequest.setFilePath(m3u8LocalPath)
  136. uploadInfo = self.__createUploadVideo(uploadVideoRequest)
  137. uploadAddress = uploadInfo['UploadAddress']
  138. headers = self.__getUploadHeaders(uploadVideoRequest)
  139. # 依次上传分片文件
  140. for sliceFilePath in sliceFilePaths:
  141. tempFilePath, sliceFileName = AliyunVodUtils.getFileBriefPath(sliceFilePath)
  142. self.__uploadOssObjectWithRetry(sliceFilePath, uploadAddress['ObjectPrefix'] + sliceFileName, uploadInfo,
  143. headers)
  144. # 上传m3u8文件
  145. self.__uploadOssObjectWithRetry(m3u8LocalPath, uploadAddress['FileName'], uploadInfo, headers)
  146. # 删除重写到本地的m3u8文件
  147. if os.path.exists(m3u8LocalPath):
  148. os.remove(m3u8LocalPath)
  149. if not os.listdir(m3u8LocalDir):
  150. os.rmdir(m3u8LocalDir)
  151. return uploadInfo['VideoId']
  152. @catch_error
  153. def uploadWebM3u8(self, uploadVideoRequest, sliceFileUrls=None):
  154. """
  155. 上传网络m3u8视频或音频文件到点播,需本地磁盘空间足够,会先下载到本地临时目录,再上传到点播存储
  156. :param uploadVideoRequest: UploadVideoRequest类的实例,注意filePath为m3u8网络文件的URL地址
  157. :param sliceFileUrls: list, 分片文件的url,例如:['http://host/sample_001.ts', 'http://host/sample_002.ts']
  158. sliceFileUrls为None时,会按照同一前缀解析分片地址;如分片路径和m3u8索引文件前缀不同等原因导致解析有误,可自行组装分片地址
  159. :return
  160. """
  161. if sliceFileUrls is None:
  162. sliceFileUrls = self.parseWebM3u8(uploadVideoRequest.filePath)
  163. if (not isinstance(sliceFileUrls, list)) or len(sliceFileUrls) <= 0:
  164. raise AliyunVodException('InvalidM3u8SliceFile', 'M3u8 slice urls invalid',
  165. 'sliceFileUrls invalid or m3u8 index file error')
  166. # 下载m3u8文件和所有ts分片文件到本地;上传到点播的m3u8索引文件会重写,以此确保分片地址都为相对地址
  167. downloader = AliyunVodDownloader()
  168. m3u8LocalDir = downloader.getSaveLocalDir() + '/' + AliyunVodUtils.getStringMd5(uploadVideoRequest.fileName)
  169. downloader.setSaveLocalDir(m3u8LocalDir)
  170. m3u8LocalPath = m3u8LocalDir + '/' + os.path.basename(uploadVideoRequest.fileName)
  171. self.__rewriteM3u8File(uploadVideoRequest.filePath, m3u8LocalPath, False)
  172. sliceList = []
  173. for sliceFileUrl in sliceFileUrls:
  174. tempFilePath, sliceFileName = AliyunVodUtils.getFileBriefPath(sliceFileUrl)
  175. err, sliceLocalPath = downloader.downloadFile(sliceFileUrl, sliceFileName)
  176. if sliceLocalPath is None:
  177. raise AliyunVodException('FileDownloadError', 'Download M3u8 File Error', '')
  178. sliceList.append((sliceLocalPath, sliceFileName))
  179. # 获取上传凭证
  180. uploadVideoRequest.setFilePath(m3u8LocalPath)
  181. uploadInfo = self.__createUploadVideo(uploadVideoRequest)
  182. uploadAddress = uploadInfo['UploadAddress']
  183. headers = self.__getUploadHeaders(uploadVideoRequest)
  184. # 依次上传分片文件
  185. for sliceFile in sliceList:
  186. self.__uploadOssObjectWithRetry(sliceFile[0], uploadAddress['ObjectPrefix'] + sliceFile[1], uploadInfo,
  187. headers)
  188. # 上传m3u8文件
  189. self.__uploadOssObjectWithRetry(m3u8LocalPath, uploadAddress['FileName'], uploadInfo, headers)
  190. # 删除下载到本地的m3u8文件和分片文件
  191. if os.path.exists(m3u8LocalPath):
  192. os.remove(m3u8LocalPath)
  193. for sliceFile in sliceList:
  194. if os.path.exists(sliceFile[0]):
  195. os.remove(sliceFile[0])
  196. if not os.listdir(m3u8LocalDir):
  197. os.rmdir(m3u8LocalDir)
  198. return uploadInfo['VideoId']
  199. @catch_error
  200. def uploadImage(self, uploadImageRequest, isLocalFile=True):
  201. """
  202. 上传图片文件到点播,不支持断点续传;该接口可支持上传本地图片或网络图片
  203. :param uploadImageRequest: UploadImageRequest,注意filePath为本地文件的绝对路径或网络文件的URL地址
  204. :param isLocalFile: bool, 是否为本地文件。True:本地文件,False:网络文件
  205. :return
  206. """
  207. # 网络图片需要先下载到本地
  208. if not isLocalFile:
  209. uploadImageRequest = self.__downloadWebMedia(uploadImageRequest)
  210. # 上传到点播
  211. uploadInfo = self.__createUploadImage(uploadImageRequest)
  212. self.__uploadOssObject(uploadImageRequest.filePath, uploadInfo['UploadAddress']['FileName'], uploadInfo, None)
  213. # 删除本地临时文件
  214. if not isLocalFile:
  215. os.remove(uploadImageRequest.filePath)
  216. return uploadInfo['ImageId'], uploadInfo['ImageURL']
  217. @catch_error
  218. def uploadAttachedMedia(self, uploadAttachedRequest, isLocalFile=True):
  219. """
  220. 上传辅助媒资文件(如水印、字幕文件)到点播,不支持断点续传;该接口可支持上传本地或网络文件
  221. :param uploadAttachedRequest: UploadAttachedMediaRequest,注意filePath为本地文件的绝对路径或网络文件的URL地址
  222. :param isLocalFile: bool, 是否为本地文件。True:本地文件,False:网络文件
  223. :return
  224. """
  225. # 网络文件需要先下载到本地
  226. if not isLocalFile:
  227. uploadAttachedRequest = self.__downloadWebMedia(uploadAttachedRequest)
  228. # 上传到点播
  229. uploadInfo = self.__createUploadAttachedMedia(uploadAttachedRequest)
  230. self.__uploadOssObject(uploadAttachedRequest.filePath, uploadInfo['UploadAddress']['FileName'], uploadInfo,
  231. None)
  232. # 删除本地临时文件
  233. if not isLocalFile:
  234. os.remove(uploadAttachedRequest.filePath)
  235. result = {'MediaId': uploadInfo['MediaId'], 'MediaURL': uploadInfo['MediaURL'],
  236. 'FileURL': uploadInfo['FileURL']}
  237. return result
  238. @catch_error
  239. def parseWebM3u8(self, m3u8FileUrl):
  240. """
  241. 解析网络m3u8文件得到所有分片文件地址,原理是将m3u8地址前缀拼接ts分片名称作为后者的下载url,适用于url不带签名或分片与m3u8文件签名相同的情况
  242. 本函数解析时会默认分片文件和m3u8文件位于同一目录,如不是则请自行拼接分片文件的地址列表
  243. :param m3u8FileUrl: string, m3u8网络文件url,例如:http://host/sample.m3u8
  244. :return sliceFileUrls
  245. """
  246. sliceFileUrls = []
  247. res = requests.get(m3u8FileUrl)
  248. res.raise_for_status()
  249. for line in res.iter_lines():
  250. if line.startswith('#'):
  251. continue
  252. sliceFileUrl = AliyunVodUtils.replaceFileName(m3u8FileUrl, line.strip())
  253. sliceFileUrls.append(sliceFileUrl)
  254. return sliceFileUrls
  255. @catch_error
  256. def parseLocalM3u8(self, m3u8FilePath):
  257. """
  258. 解析本地m3u8文件得到所有分片文件地址,原理是将m3u8地址前缀拼接ts分片名称作为后者的本地路径
  259. 本函数解析时会默认分片文件和m3u8文件位于同一目录,如不是则请自行拼接分片文件的地址列表
  260. :param m3u8FilePath: string, m3u8本地文件路径,例如:/opt/videos/sample.m3u8
  261. :return sliceFilePaths
  262. """
  263. sliceFilePaths = []
  264. m3u8FilePath = AliyunVodUtils.toUnicode(m3u8FilePath)
  265. for line in open(m3u8FilePath):
  266. if line.startswith('#'):
  267. continue
  268. sliceFileName = line.strip()
  269. sliceFilePath = AliyunVodUtils.replaceFileName(m3u8FilePath, sliceFileName)
  270. sliceFilePaths.append(sliceFilePath)
  271. return sliceFilePaths
  272. # 定义进度条回调函数;consumedBytes: 已经上传的数据量,totalBytes:总数据量
  273. def uploadProgressCallback(self, consumedBytes, totalBytes):
  274. try:
  275. if totalBytes:
  276. rate = int(100 * (float(consumedBytes) / float(totalBytes)))
  277. else:
  278. rate = 0
  279. logger.info('视频上传中: {} bytes, percent:{}{}, requestId:{}', consumedBytes, format(rate), '%',
  280. self.__requestId)
  281. except Exception as e:
  282. logger.exception("打印视频上传进度回调方法异常: {}", e)
  283. # print("[%s]uploaded %s bytes, percent %s%s" % (
  284. # AliyunVodUtils.getCurrentTimeStr(), consumedBytes, format(rate), '%'))
  285. # sys.stdout.flush()
  286. def __downloadWebMedia(self, request):
  287. # 下载媒体文件到本地临时目录
  288. downloader = AliyunVodDownloader()
  289. localFileName = "%s.%s" % (AliyunVodUtils.getStringMd5(request.fileName), request.mediaExt)
  290. fileUrl = request.filePath
  291. err, localFilePath = downloader.downloadFile(fileUrl, localFileName)
  292. if err < 0:
  293. raise AliyunVodException('FileDownloadError', 'Download File Error', '')
  294. # 重新设置上传请求对象
  295. request.setFilePath(localFilePath)
  296. return request
  297. def __rewriteM3u8File(self, srcM3u8File, dstM3u8File, isSrcLocal=True):
  298. newM3u8Text = ''
  299. if isSrcLocal:
  300. for line in open(AliyunVodUtils.toUnicode(srcM3u8File)):
  301. item = self.__processM3u8Line(line)
  302. if item is not None:
  303. newM3u8Text += item + "\n"
  304. else:
  305. res = requests.get(srcM3u8File)
  306. res.raise_for_status()
  307. for line in res.iter_lines():
  308. item = self.__processM3u8Line(line)
  309. if item is not None:
  310. newM3u8Text += item + "\n"
  311. AliyunVodUtils.mkDir(dstM3u8File)
  312. with open(dstM3u8File, 'w') as f:
  313. f.write(newM3u8Text)
  314. def __processM3u8Line(self, line):
  315. item = line.strip()
  316. if len(item) <= 0:
  317. return None
  318. if item.startswith('#'):
  319. return item
  320. tempFilePath, fileName = AliyunVodUtils.getFileBriefPath(item)
  321. return fileName
  322. def __requestUploadInfo(self, request, mediaType):
  323. request.set_accept_format('JSON')
  324. result = json.loads(self.__vodClient.do_action_with_exception(request).decode('utf-8'))
  325. result['OriUploadAddress'] = result['UploadAddress']
  326. result['OriUploadAuth'] = result['UploadAuth']
  327. result['UploadAddress'] = json.loads(base64.b64decode(result['OriUploadAddress']).decode('utf-8'))
  328. result['UploadAuth'] = json.loads(base64.b64decode(result['OriUploadAuth']).decode('utf-8'))
  329. result['MediaType'] = mediaType
  330. if mediaType == 'video':
  331. result['MediaId'] = result['VideoId']
  332. elif mediaType == 'image':
  333. result['MediaId'] = result['ImageId']
  334. result['MediaURL'] = result['ImageURL']
  335. return result
  336. # 获取视频上传地址和凭证
  337. def __createUploadVideo(self, uploadVideoRequest):
  338. request = CreateUploadVideoRequest.CreateUploadVideoRequest()
  339. title = AliyunVodUtils.subString(uploadVideoRequest.title, VOD_MAX_TITLE_LENGTH)
  340. request.set_Title(title)
  341. request.set_FileName(uploadVideoRequest.fileName)
  342. if uploadVideoRequest.description:
  343. description = AliyunVodUtils.subString(uploadVideoRequest.description, VOD_MAX_DESCRIPTION_LENGTH)
  344. request.set_Description(description)
  345. if uploadVideoRequest.coverURL:
  346. request.set_CoverURL(uploadVideoRequest.coverURL)
  347. if uploadVideoRequest.tags:
  348. request.set_Tags(uploadVideoRequest.tags)
  349. if uploadVideoRequest.cateId:
  350. request.set_CateId(uploadVideoRequest.cateId)
  351. if uploadVideoRequest.templateGroupId:
  352. request.set_TemplateGroupId(uploadVideoRequest.templateGroupId)
  353. if uploadVideoRequest.storageLocation:
  354. request.set_StorageLocation(uploadVideoRequest.storageLocation)
  355. if uploadVideoRequest.userData:
  356. request.set_UserData(uploadVideoRequest.userData)
  357. if uploadVideoRequest.appId:
  358. request.set_AppId(uploadVideoRequest.appId)
  359. if uploadVideoRequest.workflowId:
  360. request.set_WorkflowId(uploadVideoRequest.workflowId)
  361. # 根据request发送请求阿里云
  362. result = self.__requestUploadInfo(request, 'video')
  363. # logger.info("CreateUploadVideo, 获取响应体: {}, requestId:{}", result, self.__requestId)
  364. logger.info("CreateUploadVideo, FilePath: {}, VideoId: {}, requestId:{}", uploadVideoRequest.filePath,
  365. result['VideoId'], self.__requestId)
  366. return result
  367. # 刷新上传凭证
  368. def __refresh_upload_video(self, videoId):
  369. request = RefreshUploadVideoRequest.RefreshUploadVideoRequest();
  370. request.set_VideoId(videoId)
  371. result = self.__requestUploadInfo(request, 'video')
  372. logger.info("RefreshUploadVideo, VideoId:{}, requestId:{}", result['VideoId'], self.__requestId)
  373. return result
  374. # 获取图片上传地址和凭证
  375. def __createUploadImage(self, uploadImageRequest):
  376. request = CreateUploadImageRequest.CreateUploadImageRequest()
  377. request.set_ImageType(uploadImageRequest.imageType)
  378. request.set_ImageExt(uploadImageRequest.imageExt)
  379. if uploadImageRequest.title:
  380. title = AliyunVodUtils.subString(uploadImageRequest.title, VOD_MAX_TITLE_LENGTH)
  381. request.set_Title(title)
  382. if uploadImageRequest.description:
  383. description = AliyunVodUtils.subString(uploadImageRequest.description, VOD_MAX_DESCRIPTION_LENGTH)
  384. request.set_Description(description)
  385. if uploadImageRequest.tags:
  386. request.set_Tags(uploadImageRequest.tags)
  387. if uploadImageRequest.cateId:
  388. request.set_CateId(uploadImageRequest.cateId)
  389. if uploadImageRequest.storageLocation:
  390. request.set_StorageLocation(uploadImageRequest.storageLocation)
  391. if uploadImageRequest.userData:
  392. request.set_UserData(uploadImageRequest.userData)
  393. if uploadImageRequest.appId:
  394. request.set_AppId(uploadImageRequest.appId)
  395. if uploadImageRequest.workflowId:
  396. request.set_WorkflowId(uploadImageRequest.workflowId)
  397. result = self.__requestUploadInfo(request, 'image')
  398. logger.info("CreateUploadImage, FilePath: %s, ImageId: %s, ImageUrl: %s" % (
  399. uploadImageRequest.filePath, result['ImageId'], result['ImageURL']))
  400. return result
  401. def __createUploadAttachedMedia(self, uploadAttachedRequest):
  402. request = CreateUploadAttachedMediaRequest.CreateUploadAttachedMediaRequest()
  403. request.set_BusinessType(uploadAttachedRequest.businessType)
  404. request.set_MediaExt(uploadAttachedRequest.mediaExt)
  405. if uploadAttachedRequest.title:
  406. title = AliyunVodUtils.subString(uploadAttachedRequest.title, VOD_MAX_TITLE_LENGTH)
  407. request.set_Title(title)
  408. if uploadAttachedRequest.description:
  409. description = AliyunVodUtils.subString(uploadAttachedRequest.description, VOD_MAX_DESCRIPTION_LENGTH)
  410. request.set_Description(description)
  411. if uploadAttachedRequest.tags:
  412. request.set_Tags(uploadAttachedRequest.tags)
  413. if uploadAttachedRequest.cateId:
  414. request.set_CateId(uploadAttachedRequest.cateId)
  415. if uploadAttachedRequest.storageLocation:
  416. request.set_StorageLocation(uploadAttachedRequest.storageLocation)
  417. if uploadAttachedRequest.userData:
  418. request.set_UserData(uploadAttachedRequest.userData)
  419. if uploadAttachedRequest.appId:
  420. request.set_AppId(uploadAttachedRequest.appId)
  421. if uploadAttachedRequest.workflowId:
  422. request.set_WorkflowId(uploadAttachedRequest.workflowId)
  423. result = self.__requestUploadInfo(request, 'attached')
  424. logger.info("CreateUploadImage, FilePath: %s, MediaId: %s, MediaURL: %s" % (
  425. uploadAttachedRequest.filePath, result['MediaId'], result['MediaURL']))
  426. return result
  427. def __getUploadHeaders(self, uploadVideoRequest):
  428. if uploadVideoRequest.isShowWatermark is None:
  429. return None
  430. else:
  431. userData = "{\"Vod\":{\"UserData\":{\"IsShowWaterMark\": \"%s\"}}}" % (uploadVideoRequest.isShowWatermark)
  432. return {'x-oss-notification': base64.b64encode(userData, 'utf-8')}
  433. # uploadType,可选:multipart, put, web
  434. def __uploadOssObjectWithRetry(self, filePath, object, uploadInfo, headers=None):
  435. retryTimes = 0
  436. while retryTimes < self.__maxRetryTimes:
  437. try:
  438. return self.__uploadOssObject(filePath, object, uploadInfo, headers)
  439. except OssError as e:
  440. # 上传凭证过期需要重新获取凭证
  441. if e.code == 'SecurityTokenExpired' or e.code == 'InvalidAccessKeyId':
  442. uploadInfo = self.__refresh_upload_video(uploadInfo['MediaId'])
  443. except Exception as e:
  444. raise e
  445. except:
  446. raise AliyunVodException('UnkownError', repr(e), traceback.format_exc())
  447. finally:
  448. retryTimes += 1
  449. else:
  450. raise Exception("重试超过限制")
  451. def __uploadOssObject(self, filePath, object, uploadInfo, headers=None):
  452. self.__createOssClient(uploadInfo['UploadAuth'], uploadInfo['UploadAddress'])
  453. """
  454. p = os.path.dirname(os.path.realpath(__file__))
  455. store = os.path.dirname(p) + '/osstmp'
  456. return oss2.resumable_upload(self.__bucketClient, object, filePath,
  457. store=oss2.ResumableStore(root=store), headers=headers,
  458. multipart_threshold=self.__multipartThreshold, part_size=self.__multipartPartSize,
  459. num_threads=self.__multipartThreadsNum, progress_callback=self.uploadProgressCallback)
  460. """
  461. uploader = _VodResumableUploader(self.__bucketClient, filePath, object, uploadInfo, headers,
  462. self.uploadProgressCallback, self.__refreshUploadAuth,
  463. requestId=self.__requestId)
  464. uploader.setMultipartInfo(self.__multipartThreshold, self.__multipartPartSize, self.__multipartThreadsNum)
  465. uploader.setClientId(self.__accessKeyId)
  466. res = uploader.upload()
  467. uploadAddress = uploadInfo['UploadAddress']
  468. bucketHost = uploadAddress['Endpoint'].replace('://', '://' + uploadAddress['Bucket'] + ".")
  469. logger.info("UploadFile {} Finish, MediaId: {}, FilePath: {}, Destination: {}/{}, requestId:{}",
  470. uploadInfo['MediaType'], uploadInfo['MediaId'], filePath, bucketHost, object, self.__requestId)
  471. return res
  472. # 使用上传凭证和地址信息初始化OSS客户端(注意需要先Base64解码并Json Decode再传入)
  473. # 如果上传的ECS位于点播相同的存储区域(如上海),则可以指定internal为True,通过内网上传更快且免费
  474. def __createOssClient(self, uploadAuth, uploadAddress):
  475. auth = oss2.StsAuth(uploadAuth['AccessKeyId'], uploadAuth['AccessKeySecret'], uploadAuth['SecurityToken'])
  476. endpoint = AliyunVodUtils.convertOssInternal(uploadAddress['Endpoint'], self.__ecsRegion)
  477. self.__bucketClient = oss2.Bucket(auth, endpoint, uploadAddress['Bucket'],
  478. connect_timeout=self.__connTimeout, enable_crc=self.__EnableCrc)
  479. return self.__bucketClient
  480. def __refreshUploadAuth(self, videoId):
  481. uploadInfo = self.__refresh_upload_video(videoId)
  482. uploadAuth = uploadInfo['UploadAuth']
  483. uploadAddress = uploadInfo['UploadAddress']
  484. return self.__createOssClient(uploadAuth, uploadAddress)
  485. from oss2 import SizedFileAdapter, determine_part_size
  486. from oss2.models import PartInfo
  487. from aliyunsdkcore.utils import parameter_helper as helper
  488. class _VodResumableUploader:
  489. def __init__(self, bucket, filePath, object, uploadInfo, headers, progressCallback, refreshAuthCallback,
  490. requestId=None):
  491. self.__bucket = bucket
  492. self.__filePath = filePath
  493. self.__object = object
  494. self.__uploadInfo = uploadInfo
  495. self.__totalSize = None
  496. self.__headers = headers
  497. self.__mtime = os.path.getmtime(filePath)
  498. self.__progressCallback = progressCallback
  499. self.__refreshAuthCallback = refreshAuthCallback
  500. self.__threshold = None
  501. self.__partSize = None
  502. self.__threadsNum = None
  503. self.__uploadId = 0
  504. self.__record = {}
  505. self.__finishedSize = 0
  506. self.__finishedParts = []
  507. self.__filePartHash = None
  508. self.__clientId = None
  509. self.__requestId = requestId
  510. def setMultipartInfo(self, threshold, partSize, threadsNum):
  511. self.__threshold = threshold
  512. self.__partSize = partSize
  513. self.__threadsNum = threadsNum
  514. def setClientId(self, clientId):
  515. self.__clientId = clientId
  516. def upload(self):
  517. self.__totalSize = os.path.getsize(self.__filePath)
  518. logger.info("上传视频路径: {}, 视频大小: {}, requestId:{}", self.__filePath, self.__totalSize, self.__requestId)
  519. if self.__threshold and self.__totalSize <= self.__threshold:
  520. return self.simpleUpload()
  521. else:
  522. return self.multipartUpload()
  523. def simpleUpload(self):
  524. with open(AliyunVodUtils.toUnicode(self.__filePath), 'rb') as f:
  525. result = self.__bucket.put_object(self.__object, f, headers=self.__headers, progress_callback=None)
  526. if self.__uploadInfo['MediaType'] == 'video':
  527. self.__reportUploadProgress('put', 1, self.__totalSize)
  528. return result
  529. def multipartUpload(self):
  530. psize = oss2.determine_part_size(self.__totalSize, preferred_size=self.__partSize)
  531. # 初始化分片
  532. self.__uploadId = self.__bucket.init_multipart_upload(self.__object).upload_id
  533. startTime = time.time()
  534. expireSeconds = 2500 # 上传凭证有效期3000秒,提前刷新
  535. # 逐个上传分片
  536. with open(AliyunVodUtils.toUnicode(self.__filePath), 'rb') as fileObj:
  537. partNumber = 1
  538. offset = 0
  539. while offset < self.__totalSize:
  540. uploadSize = min(psize, self.__totalSize - offset)
  541. # logger.info("UploadPart, FilePath: %s, VideoId: %s, UploadId: %s, PartNumber: %s, PartSize: %s" % (self.__fileName, self.__videoId, self.__uploadId, partNumber, uploadSize))
  542. result = self.__upload_part(partNumber, fileObj, uploadSize)
  543. # print(result.request_id)
  544. self.__finishedParts.append(PartInfo(partNumber, result.etag))
  545. offset += uploadSize
  546. partNumber += 1
  547. # 上传进度回调
  548. self.__progressCallback(offset, self.__totalSize)
  549. if self.__uploadInfo['MediaType'] == 'video':
  550. # 上报上传进度
  551. self.__reportUploadProgress('multipart', partNumber - 1, offset)
  552. # 检测上传凭证是否过期
  553. nowTime = time.time()
  554. if nowTime - startTime >= expireSeconds:
  555. self.__bucket = self.__refreshAuthCallback(self.__uploadInfo['MediaId'])
  556. startTime = nowTime
  557. # 完成分片上传
  558. self.__complete_multipart_upload()
  559. return result
  560. def __upload_part(self, partNumber, fileObj, uploadSize):
  561. retry_num = 0
  562. while True:
  563. try:
  564. return self.__bucket.upload_part(self.__object, self.__uploadId, partNumber,
  565. SizedFileAdapter(fileObj, uploadSize))
  566. except Exception as e:
  567. logger.error("阿里云分片上传异常报错: {}, 当前重试次数:{} requestId:{}", str(e), retry_num + 1, self.__requestId)
  568. if retry_num > 3:
  569. raise Exception("阿里云分片上传异常")
  570. except:
  571. logger.error("阿里云完成分片上传异常报错, 当前重试次数:{}, requestId:{}", retry_num + 1, self.__requestId)
  572. if retry_num > 3:
  573. raise Exception("阿里云分片上传异常")
  574. finally:
  575. retry_num += 1
  576. time.sleep(1)
  577. def __complete_multipart_upload(self):
  578. retry_num = 0
  579. while True:
  580. try:
  581. self.__bucket.complete_multipart_upload(self.__object, self.__uploadId, self.__finishedParts,
  582. headers=self.__headers)
  583. break
  584. except Exception as e:
  585. logger.error("阿里云完成分片上传异常报错: {}, 当前重试次数:{}, requestId:{}", str(e), retry_num + 1, self.__requestId)
  586. if retry_num > 5:
  587. raise Exception("阿里云完成分片上传异常")
  588. except:
  589. logger.error("阿里云完成分片上传异常报错, 当前重试次数:{}, requestId:{}", retry_num + 1, self.__requestId)
  590. if retry_num > 5:
  591. raise Exception("阿里云完成分片上传异常")
  592. finally:
  593. time.sleep(1)
  594. retry_num += 1
  595. def __reportUploadProgress(self, uploadMethod, donePartsCount, doneBytes):
  596. retry_num = 5
  597. current_num = 0
  598. while True:
  599. try:
  600. reportHost = 'vod.cn-shanghai.aliyuncs.com'
  601. sdkVersion = '1.3.1'
  602. reportKey = 'HBL9nnSwhtU2$STX'
  603. uploadPoint = {'upMethod': uploadMethod, 'partSize': self.__partSize, 'doneBytes': doneBytes}
  604. timestamp = int(time.time())
  605. authInfo = AliyunVodUtils.getStringMd5("%s|%s|%s" % (self.__clientId, reportKey, timestamp))
  606. fields = {'Action': 'ReportUploadProgress', 'Format': 'JSON', 'Version': '2017-03-21',
  607. 'Timestamp': helper.get_iso_8061_date(), 'SignatureNonce': helper.get_uuid(),
  608. 'VideoId': self.__uploadInfo['MediaId'], 'Source': 'PythonSDK', 'ClientId': self.__clientId,
  609. 'BusinessType': 'UploadVideo', 'TerminalType': 'PC', 'DeviceModel': 'Server',
  610. 'AppVersion': sdkVersion, 'AuthTimestamp': timestamp, 'AuthInfo': authInfo,
  611. 'FileName': self.__filePath,
  612. 'FileHash': self.__getFilePartHash(self.__clientId, self.__filePath, self.__totalSize),
  613. 'FileSize': self.__totalSize, 'FileCreateTime': timestamp, 'UploadRatio': 0,
  614. 'UploadId': self.__uploadId,
  615. 'DonePartsCount': donePartsCount, 'PartSize': self.__partSize,
  616. 'UploadPoint': json.dumps(uploadPoint),
  617. 'UploadAddress': self.__uploadInfo['OriUploadAddress']
  618. }
  619. requests.post('http://' + reportHost, fields, timeout=30)
  620. break
  621. except Exception as e:
  622. current_num += 1
  623. time.sleep(1)
  624. logger.error("vod上报视频进度异常: {}, 当前重试次数:{}, requestId:{}", repr(e), current_num,
  625. self.__requestId)
  626. if current_num > retry_num:
  627. logger.error("vod上报视频重试失败 {}, requestId:{}", repr(e), self.__requestId)
  628. raise e
  629. def __getFilePartHash(self, clientId, filePath, fileSize):
  630. if self.__filePartHash:
  631. return self.__filePartHash
  632. length = 1 * 1024 * 1024
  633. if fileSize < length:
  634. length = fileSize
  635. try:
  636. fp = open(AliyunVodUtils.toUnicode(filePath), 'rb')
  637. strVal = fp.read(length)
  638. self.__filePartHash = AliyunVodUtils.getStringMd5(strVal, False)
  639. fp.close()
  640. except:
  641. self.__filePartHash = "%s|%s|%s" % (clientId, filePath, self.__mtime)
  642. return self.__filePartHash