video-transcoding.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import { Job } from 'bull'
  2. import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
  3. import { basename, extname as extnameUtil, join } from 'path'
  4. import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
  5. import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models'
  6. import { VideoResolution } from '../../shared/models/videos'
  7. import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
  8. import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
  9. import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../helpers/ffprobe-utils'
  10. import { logger } from '../helpers/logger'
  11. import { CONFIG } from '../initializers/config'
  12. import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
  13. import { VideoFileModel } from '../models/video/video-file'
  14. import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
  15. import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
  16. import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
  17. import { availableEncoders } from './video-transcoding-profiles'
  18. /**
  19. *
  20. * Functions that run transcoding functions, update the database, cleanup files, create torrent files...
  21. * Mainly called by the job queue
  22. *
  23. */
  24. // Optimize the original video file and replace it. The resolution is not changed.
  25. async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile: MVideoFile, job?: Job) {
  26. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  27. const newExtname = '.mp4'
  28. const videoInputPath = getVideoFilePath(video, inputVideoFile)
  29. const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
  30. const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
  31. ? 'quick-transcode'
  32. : 'video'
  33. const transcodeOptions: TranscodeOptions = {
  34. type: transcodeType,
  35. inputPath: videoInputPath,
  36. outputPath: videoTranscodedPath,
  37. availableEncoders,
  38. profile: 'default',
  39. resolution: inputVideoFile.resolution,
  40. job
  41. }
  42. // Could be very long!
  43. await transcode(transcodeOptions)
  44. try {
  45. await remove(videoInputPath)
  46. // Important to do this before getVideoFilename() to take in account the new file extension
  47. inputVideoFile.extname = newExtname
  48. const videoOutputPath = getVideoFilePath(video, inputVideoFile)
  49. await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
  50. return transcodeType
  51. } catch (err) {
  52. // Auto destruction...
  53. video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
  54. throw err
  55. }
  56. }
  57. // Transcode the original video file to a lower resolution.
  58. async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean, job: Job) {
  59. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  60. const extname = '.mp4'
  61. // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
  62. const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
  63. const newVideoFile = new VideoFileModel({
  64. resolution,
  65. extname,
  66. size: 0,
  67. videoId: video.id
  68. })
  69. const videoOutputPath = getVideoFilePath(video, newVideoFile)
  70. const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile))
  71. const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
  72. ? {
  73. type: 'only-audio' as 'only-audio',
  74. inputPath: videoInputPath,
  75. outputPath: videoTranscodedPath,
  76. availableEncoders,
  77. profile: 'default',
  78. resolution,
  79. job
  80. }
  81. : {
  82. type: 'video' as 'video',
  83. inputPath: videoInputPath,
  84. outputPath: videoTranscodedPath,
  85. availableEncoders,
  86. profile: 'default',
  87. resolution,
  88. isPortraitMode: isPortrait,
  89. job
  90. }
  91. await transcode(transcodeOptions)
  92. return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
  93. }
  94. // Merge an image with an audio file to create a video
  95. async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution, job: Job) {
  96. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  97. const newExtname = '.mp4'
  98. const inputVideoFile = video.getMinQualityFile()
  99. const audioInputPath = getVideoFilePath(video, inputVideoFile)
  100. const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
  101. // If the user updates the video preview during transcoding
  102. const previewPath = video.getPreview().getPath()
  103. const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
  104. await copyFile(previewPath, tmpPreviewPath)
  105. const transcodeOptions = {
  106. type: 'merge-audio' as 'merge-audio',
  107. inputPath: tmpPreviewPath,
  108. outputPath: videoTranscodedPath,
  109. availableEncoders,
  110. profile: 'default',
  111. audioPath: audioInputPath,
  112. resolution,
  113. job
  114. }
  115. try {
  116. await transcode(transcodeOptions)
  117. await remove(audioInputPath)
  118. await remove(tmpPreviewPath)
  119. } catch (err) {
  120. await remove(tmpPreviewPath)
  121. throw err
  122. }
  123. // Important to do this before getVideoFilename() to take in account the new file extension
  124. inputVideoFile.extname = newExtname
  125. const videoOutputPath = getVideoFilePath(video, inputVideoFile)
  126. // ffmpeg generated a new video file, so update the video duration
  127. // See https://trac.ffmpeg.org/ticket/5456
  128. video.duration = await getDurationFromVideoFile(videoTranscodedPath)
  129. await video.save()
  130. return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
  131. }
  132. // Concat TS segments from a live video to a fragmented mp4 HLS playlist
  133. async function generateHlsPlaylistFromTS (options: {
  134. video: MVideoWithFile
  135. concatenatedTsFilePath: string
  136. resolution: VideoResolution
  137. isPortraitMode: boolean
  138. isAAC: boolean
  139. }) {
  140. return generateHlsPlaylistCommon({
  141. video: options.video,
  142. resolution: options.resolution,
  143. isPortraitMode: options.isPortraitMode,
  144. inputPath: options.concatenatedTsFilePath,
  145. type: 'hls-from-ts' as 'hls-from-ts',
  146. isAAC: options.isAAC
  147. })
  148. }
  149. // Generate an HLS playlist from an input file, and update the master playlist
  150. function generateHlsPlaylist (options: {
  151. video: MVideoWithFile
  152. videoInputPath: string
  153. resolution: VideoResolution
  154. copyCodecs: boolean
  155. isPortraitMode: boolean
  156. job?: Job
  157. }) {
  158. return generateHlsPlaylistCommon({
  159. video: options.video,
  160. resolution: options.resolution,
  161. copyCodecs: options.copyCodecs,
  162. isPortraitMode: options.isPortraitMode,
  163. inputPath: options.videoInputPath,
  164. type: 'hls' as 'hls',
  165. job: options.job
  166. })
  167. }
  168. function getEnabledResolutions (type: 'vod' | 'live') {
  169. const transcoding = type === 'vod'
  170. ? CONFIG.TRANSCODING
  171. : CONFIG.LIVE.TRANSCODING
  172. return Object.keys(transcoding.RESOLUTIONS)
  173. .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
  174. .map(r => parseInt(r, 10))
  175. }
  176. // ---------------------------------------------------------------------------
  177. export {
  178. generateHlsPlaylist,
  179. generateHlsPlaylistFromTS,
  180. optimizeOriginalVideofile,
  181. transcodeNewResolution,
  182. mergeAudioVideofile,
  183. getEnabledResolutions
  184. }
  185. // ---------------------------------------------------------------------------
  186. async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
  187. const stats = await stat(transcodingPath)
  188. const fps = await getVideoFileFPS(transcodingPath)
  189. const metadata = await getMetadataFromFile(transcodingPath)
  190. await move(transcodingPath, outputPath, { overwrite: true })
  191. videoFile.size = stats.size
  192. videoFile.fps = fps
  193. videoFile.metadata = metadata
  194. await createTorrentAndSetInfoHash(video, videoFile)
  195. await VideoFileModel.customUpsert(videoFile, 'video', undefined)
  196. video.VideoFiles = await video.$get('VideoFiles')
  197. return video
  198. }
  199. async function generateHlsPlaylistCommon (options: {
  200. type: 'hls' | 'hls-from-ts'
  201. video: MVideoWithFile
  202. inputPath: string
  203. resolution: VideoResolution
  204. copyCodecs?: boolean
  205. isAAC?: boolean
  206. isPortraitMode: boolean
  207. job?: Job
  208. }) {
  209. const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
  210. const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
  211. await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
  212. const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
  213. const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
  214. const transcodeOptions = {
  215. type,
  216. inputPath,
  217. outputPath,
  218. availableEncoders,
  219. profile: 'default',
  220. resolution,
  221. copyCodecs,
  222. isPortraitMode,
  223. isAAC,
  224. hlsPlaylist: {
  225. videoFilename
  226. },
  227. job
  228. }
  229. await transcode(transcodeOptions)
  230. const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
  231. const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
  232. videoId: video.id,
  233. playlistUrl,
  234. segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive),
  235. p2pMediaLoaderInfohashes: [],
  236. p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
  237. type: VideoStreamingPlaylistType.HLS
  238. }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ]
  239. videoStreamingPlaylist.Video = video
  240. const newVideoFile = new VideoFileModel({
  241. resolution,
  242. extname: extnameUtil(videoFilename),
  243. size: 0,
  244. fps: -1,
  245. videoStreamingPlaylistId: videoStreamingPlaylist.id
  246. })
  247. const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile)
  248. const stats = await stat(videoFilePath)
  249. newVideoFile.size = stats.size
  250. newVideoFile.fps = await getVideoFileFPS(videoFilePath)
  251. newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
  252. await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
  253. await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
  254. videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
  255. videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(
  256. playlistUrl, videoStreamingPlaylist.VideoFiles
  257. )
  258. await videoStreamingPlaylist.save()
  259. video.setHLSPlaylist(videoStreamingPlaylist)
  260. await updateMasterHLSPlaylist(video)
  261. await updateSha256VODSegments(video)
  262. return outputPath
  263. }