web-transcoding.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import { buildAspectRatio } from '@peertube/peertube-core-utils'
  2. import { TranscodeVODOptionsType, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
  3. import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
  4. import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
  5. import { VideoModel } from '@server/models/video/video.js'
  6. import { MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
  7. import { Job } from 'bullmq'
  8. import { move, remove } from 'fs-extra/esm'
  9. import { copyFile } from 'fs/promises'
  10. import { basename, join } from 'path'
  11. import { CONFIG } from '../../initializers/config.js'
  12. import { VideoFileModel } from '../../models/video/video-file.js'
  13. import { JobQueue } from '../job-queue/index.js'
  14. import { generateWebVideoFilename } from '../paths.js'
  15. import { buildNewFile, saveNewOriginalFileIfNeeded } from '../video-file.js'
  16. import { buildStoryboardJobIfNeeded } from '../video-jobs.js'
  17. import { VideoPathManager } from '../video-path-manager.js'
  18. import { buildFFmpegVOD } from './shared/index.js'
  19. import { buildOriginalFileResolution } from './transcoding-resolutions.js'
  20. // Optimize the original video file and replace it. The resolution is not changed.
  21. export async function optimizeOriginalVideofile (options: {
  22. video: MVideoFullLight
  23. inputVideoFile: MVideoFile
  24. quickTranscode: boolean
  25. job: Job
  26. }) {
  27. const { video, inputVideoFile, quickTranscode, job } = options
  28. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  29. const newExtname = '.mp4'
  30. // Will be released by our transcodeVOD function once ffmpeg is ran
  31. const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
  32. try {
  33. await video.reload()
  34. await inputVideoFile.reload()
  35. const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
  36. const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => {
  37. const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
  38. const transcodeType: TranscodeVODOptionsType = quickTranscode
  39. ? 'quick-transcode'
  40. : 'video'
  41. const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
  42. const fps = computeOutputFPS({ inputFPS: inputVideoFile.fps, resolution })
  43. // Could be very long!
  44. await buildFFmpegVOD(job).transcode({
  45. type: transcodeType,
  46. inputPath: videoInputPath,
  47. outputPath: videoOutputPath,
  48. inputFileMutexReleaser,
  49. resolution,
  50. fps
  51. })
  52. const { videoFile } = await onWebVideoFileTranscoding({ video, videoOutputPath, deleteWebInputVideoFile: inputVideoFile })
  53. return { transcodeType, videoFile }
  54. })
  55. return result
  56. } finally {
  57. inputFileMutexReleaser()
  58. }
  59. }
  60. // Transcode the original/old/source video file to a lower resolution compatible with web browsers
  61. export async function transcodeNewWebVideoResolution (options: {
  62. video: MVideoFullLight
  63. resolution: number
  64. fps: number
  65. job: Job
  66. }) {
  67. const { video: videoArg, resolution, fps, job } = options
  68. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  69. const newExtname = '.mp4'
  70. const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid)
  71. try {
  72. const video = await VideoModel.loadFull(videoArg.uuid)
  73. const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
  74. const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
  75. const filename = generateWebVideoFilename(resolution, newExtname)
  76. const videoOutputPath = join(transcodeDirectory, filename)
  77. const transcodeOptions = {
  78. type: 'video' as 'video',
  79. inputPath: videoInputPath,
  80. outputPath: videoOutputPath,
  81. inputFileMutexReleaser,
  82. resolution,
  83. fps
  84. }
  85. await buildFFmpegVOD(job).transcode(transcodeOptions)
  86. return onWebVideoFileTranscoding({ video, videoOutputPath })
  87. })
  88. return result
  89. } finally {
  90. inputFileMutexReleaser()
  91. }
  92. }
  93. // Merge an image with an audio file to create a video
  94. export async function mergeAudioVideofile (options: {
  95. video: MVideoFullLight
  96. resolution: number
  97. fps: number
  98. job: Job
  99. }) {
  100. const { video: videoArg, resolution, fps, job } = options
  101. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  102. const newExtname = '.mp4'
  103. const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid)
  104. try {
  105. const video = await VideoModel.loadFull(videoArg.uuid)
  106. const inputVideoFile = video.getMinQualityFile()
  107. const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
  108. const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => {
  109. const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
  110. // If the user updates the video preview during transcoding
  111. const previewPath = video.getPreview().getPath()
  112. const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
  113. await copyFile(previewPath, tmpPreviewPath)
  114. const transcodeOptions = {
  115. type: 'merge-audio' as 'merge-audio',
  116. inputPath: tmpPreviewPath,
  117. outputPath: videoOutputPath,
  118. inputFileMutexReleaser,
  119. audioPath: audioInputPath,
  120. resolution,
  121. fps
  122. }
  123. try {
  124. await buildFFmpegVOD(job).transcode(transcodeOptions)
  125. await remove(tmpPreviewPath)
  126. } catch (err) {
  127. await remove(tmpPreviewPath)
  128. throw err
  129. }
  130. await onWebVideoFileTranscoding({
  131. video,
  132. videoOutputPath,
  133. deleteWebInputVideoFile: inputVideoFile,
  134. wasAudioFile: true
  135. })
  136. })
  137. return result
  138. } finally {
  139. inputFileMutexReleaser()
  140. }
  141. }
  142. export async function onWebVideoFileTranscoding (options: {
  143. video: MVideoFullLight
  144. videoOutputPath: string
  145. wasAudioFile?: boolean // default false
  146. deleteWebInputVideoFile?: MVideoFile
  147. }) {
  148. const { video, videoOutputPath, wasAudioFile, deleteWebInputVideoFile } = options
  149. const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
  150. const videoFile = await buildNewFile({ mode: 'web-video', path: videoOutputPath })
  151. videoFile.videoId = video.id
  152. try {
  153. await video.reload()
  154. // ffmpeg generated a new video file, so update the video duration
  155. // See https://trac.ffmpeg.org/ticket/5456
  156. if (wasAudioFile) {
  157. video.duration = await getVideoStreamDuration(videoOutputPath)
  158. video.aspectRatio = buildAspectRatio({ width: videoFile.width, height: videoFile.height })
  159. await video.save()
  160. }
  161. const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
  162. await move(videoOutputPath, outputPath, { overwrite: true })
  163. await createTorrentAndSetInfoHash(video, videoFile)
  164. if (deleteWebInputVideoFile) {
  165. await saveNewOriginalFileIfNeeded(video, deleteWebInputVideoFile)
  166. await video.removeWebVideoFile(deleteWebInputVideoFile)
  167. await deleteWebInputVideoFile.destroy()
  168. }
  169. const existingFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
  170. if (existingFile) await video.removeWebVideoFile(existingFile)
  171. await VideoFileModel.customUpsert(videoFile, 'video', undefined)
  172. video.VideoFiles = await video.$get('VideoFiles')
  173. if (wasAudioFile) {
  174. await JobQueue.Instance.createJob(buildStoryboardJobIfNeeded({ video, federate: false }))
  175. }
  176. return { video, videoFile }
  177. } finally {
  178. mutexReleaser()
  179. }
  180. }