video-transcoding.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import * as Bull from 'bull'
  2. import { VideoResolution } from '../../../../shared'
  3. import { logger } from '../../../helpers/logger'
  4. import { VideoModel } from '../../../models/video/video'
  5. import { JobQueue } from '../job-queue'
  6. import { federateVideoIfNeeded } from '../../activitypub'
  7. import { retryTransactionWrapper } from '../../../helpers/database-utils'
  8. import { sequelizeTypescript } from '../../../initializers'
  9. import * as Bluebird from 'bluebird'
  10. import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
  11. import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding'
  12. import { Notifier } from '../../notifier'
  13. import { CONFIG } from '../../../initializers/config'
  14. import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models'
  15. interface BaseTranscodingPayload {
  16. videoUUID: string
  17. isNewVideo?: boolean
  18. }
  19. interface HLSTranscodingPayload extends BaseTranscodingPayload {
  20. type: 'hls'
  21. isPortraitMode?: boolean
  22. resolution: VideoResolution
  23. copyCodecs: boolean
  24. }
  25. interface NewResolutionTranscodingPayload extends BaseTranscodingPayload {
  26. type: 'new-resolution'
  27. isPortraitMode?: boolean
  28. resolution: VideoResolution
  29. }
  30. interface MergeAudioTranscodingPayload extends BaseTranscodingPayload {
  31. type: 'merge-audio'
  32. resolution: VideoResolution
  33. }
  34. interface OptimizeTranscodingPayload extends BaseTranscodingPayload {
  35. type: 'optimize'
  36. }
  37. export type VideoTranscodingPayload = HLSTranscodingPayload | NewResolutionTranscodingPayload
  38. | OptimizeTranscodingPayload | MergeAudioTranscodingPayload
  39. async function processVideoTranscoding (job: Bull.Job) {
  40. const payload = job.data as VideoTranscodingPayload
  41. logger.info('Processing video file in job %d.', job.id)
  42. const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID)
  43. // No video, maybe deleted?
  44. if (!video) {
  45. logger.info('Do not process job %d, video does not exist.', job.id)
  46. return undefined
  47. }
  48. if (payload.type === 'hls') {
  49. await generateHlsPlaylist(video, payload.resolution, payload.copyCodecs, payload.isPortraitMode || false)
  50. await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
  51. } else if (payload.type === 'new-resolution') {
  52. await transcodeNewResolution(video, payload.resolution, payload.isPortraitMode || false)
  53. await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
  54. } else if (payload.type === 'merge-audio') {
  55. await mergeAudioVideofile(video, payload.resolution)
  56. await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
  57. } else {
  58. await optimizeOriginalVideofile(video)
  59. await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload)
  60. }
  61. return video
  62. }
  63. async function onHlsPlaylistGenerationSuccess (video: MVideoFullLight) {
  64. if (video === undefined) return undefined
  65. // We generated the HLS playlist, we don't need the webtorrent files anymore if the admin disabled it
  66. if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
  67. for (const file of video.VideoFiles) {
  68. await video.removeFile(file)
  69. await file.destroy()
  70. }
  71. video.VideoFiles = []
  72. }
  73. return publishAndFederateIfNeeded(video)
  74. }
  75. async function publishNewResolutionIfNeeded (video: MVideoUUID, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) {
  76. await publishAndFederateIfNeeded(video)
  77. await createHlsJobIfEnabled(payload)
  78. }
  79. async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: OptimizeTranscodingPayload) {
  80. if (videoArg === undefined) return undefined
  81. // Outside the transaction (IO on disk)
  82. const { videoFileResolution } = await videoArg.getMaxQualityResolution()
  83. const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
  84. // Maybe the video changed in database, refresh it
  85. let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t)
  86. // Video does not exist anymore
  87. if (!videoDatabase) return undefined
  88. // Create transcoding jobs if there are enabled resolutions
  89. const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution)
  90. logger.info(
  91. 'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
  92. { resolutions: resolutionsEnabled }
  93. )
  94. let videoPublished = false
  95. const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution })
  96. await createHlsJobIfEnabled(hlsPayload)
  97. if (resolutionsEnabled.length !== 0) {
  98. const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = []
  99. for (const resolution of resolutionsEnabled) {
  100. let dataInput: VideoTranscodingPayload
  101. if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
  102. dataInput = {
  103. type: 'new-resolution' as 'new-resolution',
  104. videoUUID: videoDatabase.uuid,
  105. resolution
  106. }
  107. } else if (CONFIG.TRANSCODING.HLS.ENABLED) {
  108. dataInput = {
  109. type: 'hls',
  110. videoUUID: videoDatabase.uuid,
  111. resolution,
  112. isPortraitMode: false,
  113. copyCodecs: false
  114. }
  115. }
  116. const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
  117. tasks.push(p)
  118. }
  119. await Promise.all(tasks)
  120. logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
  121. } else {
  122. // No transcoding to do, it's now published
  123. videoPublished = await videoDatabase.publishIfNeededAndSave(t)
  124. logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy })
  125. }
  126. await federateVideoIfNeeded(videoDatabase, payload.isNewVideo, t)
  127. return { videoDatabase, videoPublished }
  128. })
  129. if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
  130. if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
  131. }
  132. // ---------------------------------------------------------------------------
  133. export {
  134. processVideoTranscoding,
  135. publishNewResolutionIfNeeded
  136. }
  137. // ---------------------------------------------------------------------------
  138. function createHlsJobIfEnabled (payload?: { videoUUID: string, resolution: number, isPortraitMode?: boolean }) {
  139. // Generate HLS playlist?
  140. if (payload && CONFIG.TRANSCODING.HLS.ENABLED) {
  141. const hlsTranscodingPayload = {
  142. type: 'hls' as 'hls',
  143. videoUUID: payload.videoUUID,
  144. resolution: payload.resolution,
  145. isPortraitMode: payload.isPortraitMode,
  146. copyCodecs: true
  147. }
  148. return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload })
  149. }
  150. }
  151. async function publishAndFederateIfNeeded (video: MVideoUUID) {
  152. const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
  153. // Maybe the video changed in database, refresh it
  154. const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
  155. // Video does not exist anymore
  156. if (!videoDatabase) return undefined
  157. // We transcoded the video file in another format, now we can publish it
  158. const videoPublished = await videoDatabase.publishIfNeededAndSave(t)
  159. // If the video was not published, we consider it is a new one for other instances
  160. await federateVideoIfNeeded(videoDatabase, videoPublished, t)
  161. return { videoDatabase, videoPublished }
  162. })
  163. if (videoPublished) {
  164. Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
  165. Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
  166. }
  167. }