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.

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