Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

671 linhas
31KB

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