video-transcoding.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
  2. import { basename, extname as extnameUtil, join } from 'path'
  3. import {
  4. canDoQuickTranscode,
  5. getDurationFromVideoFile,
  6. getVideoFileFPS,
  7. transcode,
  8. TranscodeOptions,
  9. TranscodeOptionsType
  10. } from '../helpers/ffmpeg-utils'
  11. import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
  12. import { logger } from '../helpers/logger'
  13. import { VideoResolution } from '../../shared/models/videos'
  14. import { VideoFileModel } from '../models/video/video-file'
  15. import { updateMasterHLSPlaylist, updateSha256Segments } from './hls'
  16. import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
  17. import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
  18. import { CONFIG } from '../initializers/config'
  19. import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/typings/models'
  20. import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
  21. import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
  22. /**
  23. * Optimize the original video file and replace it. The resolution is not changed.
  24. */
  25. async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
  26. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  27. const newExtname = '.mp4'
  28. const inputVideoFile = inputVideoFileArg || video.getMaxQualityFile()
  29. const videoInputPath = getVideoFilePath(video, inputVideoFile)
  30. const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
  31. const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
  32. ? 'quick-transcode'
  33. : 'video'
  34. const transcodeOptions: TranscodeOptions = {
  35. type: transcodeType,
  36. inputPath: videoInputPath,
  37. outputPath: videoTranscodedPath,
  38. resolution: inputVideoFile.resolution
  39. }
  40. // Could be very long!
  41. await transcode(transcodeOptions)
  42. try {
  43. await remove(videoInputPath)
  44. // Important to do this before getVideoFilename() to take in account the new file extension
  45. inputVideoFile.extname = newExtname
  46. const videoOutputPath = getVideoFilePath(video, inputVideoFile)
  47. await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
  48. } catch (err) {
  49. // Auto destruction...
  50. video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
  51. throw err
  52. }
  53. }
  54. /**
  55. * Transcode the original video file to a lower resolution.
  56. */
  57. async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
  58. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  59. const extname = '.mp4'
  60. // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
  61. const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
  62. const newVideoFile = new VideoFileModel({
  63. resolution,
  64. extname,
  65. size: 0,
  66. videoId: video.id
  67. })
  68. const videoOutputPath = getVideoFilePath(video, newVideoFile)
  69. const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile))
  70. const transcodeOptions = {
  71. type: 'video' as 'video',
  72. inputPath: videoInputPath,
  73. outputPath: videoTranscodedPath,
  74. resolution,
  75. isPortraitMode: isPortrait
  76. }
  77. await transcode(transcodeOptions)
  78. return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
  79. }
  80. async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution) {
  81. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  82. const newExtname = '.mp4'
  83. const inputVideoFile = video.getMaxQualityFile()
  84. const audioInputPath = getVideoFilePath(video, inputVideoFile)
  85. const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
  86. // If the user updates the video preview during transcoding
  87. const previewPath = video.getPreview().getPath()
  88. const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
  89. await copyFile(previewPath, tmpPreviewPath)
  90. const transcodeOptions = {
  91. type: 'merge-audio' as 'merge-audio',
  92. inputPath: tmpPreviewPath,
  93. outputPath: videoTranscodedPath,
  94. audioPath: audioInputPath,
  95. resolution
  96. }
  97. try {
  98. await transcode(transcodeOptions)
  99. await remove(audioInputPath)
  100. await remove(tmpPreviewPath)
  101. } catch (err) {
  102. await remove(tmpPreviewPath)
  103. throw err
  104. }
  105. // Important to do this before getVideoFilename() to take in account the new file extension
  106. inputVideoFile.extname = newExtname
  107. const videoOutputPath = getVideoFilePath(video, inputVideoFile)
  108. // ffmpeg generated a new video file, so update the video duration
  109. // See https://trac.ffmpeg.org/ticket/5456
  110. video.duration = await getDurationFromVideoFile(videoTranscodedPath)
  111. await video.save()
  112. return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
  113. }
  114. async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, copyCodecs: boolean, isPortraitMode: boolean) {
  115. const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
  116. await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
  117. const videoFileInput = copyCodecs
  118. ? video.getWebTorrentFile(resolution)
  119. : video.getMaxQualityFile()
  120. const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist()
  121. const videoInputPath = getVideoFilePath(videoOrStreamingPlaylist, videoFileInput)
  122. const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
  123. const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
  124. const transcodeOptions = {
  125. type: 'hls' as 'hls',
  126. inputPath: videoInputPath,
  127. outputPath,
  128. resolution,
  129. copyCodecs,
  130. isPortraitMode,
  131. hlsPlaylist: {
  132. videoFilename
  133. }
  134. }
  135. logger.debug('Will run transcode.', { transcodeOptions })
  136. await transcode(transcodeOptions)
  137. const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
  138. const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
  139. videoId: video.id,
  140. playlistUrl,
  141. segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid),
  142. p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, video.VideoFiles),
  143. p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
  144. type: VideoStreamingPlaylistType.HLS
  145. }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ]
  146. videoStreamingPlaylist.Video = video
  147. const newVideoFile = new VideoFileModel({
  148. resolution,
  149. extname: extnameUtil(videoFilename),
  150. size: 0,
  151. fps: -1,
  152. videoStreamingPlaylistId: videoStreamingPlaylist.id
  153. })
  154. const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile)
  155. const stats = await stat(videoFilePath)
  156. newVideoFile.size = stats.size
  157. newVideoFile.fps = await getVideoFileFPS(videoFilePath)
  158. await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
  159. const updatedVideoFile = await newVideoFile.save()
  160. videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') as VideoFileModel[]
  161. videoStreamingPlaylist.VideoFiles.push(updatedVideoFile)
  162. video.setHLSPlaylist(videoStreamingPlaylist)
  163. await updateMasterHLSPlaylist(video)
  164. await updateSha256Segments(video)
  165. return video
  166. }
  167. // ---------------------------------------------------------------------------
  168. export {
  169. generateHlsPlaylist,
  170. optimizeOriginalVideofile,
  171. transcodeNewResolution,
  172. mergeAudioVideofile
  173. }
  174. // ---------------------------------------------------------------------------
  175. async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
  176. const stats = await stat(transcodingPath)
  177. const fps = await getVideoFileFPS(transcodingPath)
  178. await move(transcodingPath, outputPath)
  179. videoFile.size = stats.size
  180. videoFile.fps = fps
  181. await createTorrentAndSetInfoHash(video, videoFile)
  182. const updatedVideoFile = await videoFile.save()
  183. // Add it if this is a new created file
  184. if (video.VideoFiles.some(f => f.id === videoFile.id) === false) {
  185. video.VideoFiles.push(updatedVideoFile)
  186. }
  187. return video
  188. }