2
1

video-transcoding.ts 7.4 KB

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