ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

hls-transcoding.ts 5.6 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import { MutexInterface } from 'async-mutex'
  2. import { Job } from 'bullmq'
  3. import { ensureDir, move } from 'fs-extra/esm'
  4. import { join } from 'path'
  5. import { pick } from '@peertube/peertube-core-utils'
  6. import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
  7. import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
  8. import { sequelizeTypescript } from '@server/initializers/database.js'
  9. import { MVideo } from '@server/types/models/index.js'
  10. import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
  11. import { CONFIG } from '../../initializers/config.js'
  12. import { VideoFileModel } from '../../models/video/video-file.js'
  13. import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist.js'
  14. import { renameVideoFileInPlaylist, updatePlaylistAfterFileChange } from '../hls.js'
  15. import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths.js'
  16. import { buildNewFile } from '../video-file.js'
  17. import { VideoPathManager } from '../video-path-manager.js'
  18. import { buildFFmpegVOD } from './shared/index.js'
  19. // Concat TS segments from a live video to a fragmented mp4 HLS playlist
  20. export async function generateHlsPlaylistResolutionFromTS (options: {
  21. video: MVideo
  22. concatenatedTsFilePath: string
  23. resolution: number
  24. fps: number
  25. isAAC: boolean
  26. inputFileMutexReleaser: MutexInterface.Releaser
  27. }) {
  28. return generateHlsPlaylistCommon({
  29. type: 'hls-from-ts' as 'hls-from-ts',
  30. inputPath: options.concatenatedTsFilePath,
  31. ...pick(options, [ 'video', 'resolution', 'fps', 'inputFileMutexReleaser', 'isAAC' ])
  32. })
  33. }
  34. // Generate an HLS playlist from an input file, and update the master playlist
  35. export function generateHlsPlaylistResolution (options: {
  36. video: MVideo
  37. videoInputPath: string
  38. resolution: number
  39. fps: number
  40. copyCodecs: boolean
  41. inputFileMutexReleaser: MutexInterface.Releaser
  42. job?: Job
  43. }) {
  44. return generateHlsPlaylistCommon({
  45. type: 'hls' as 'hls',
  46. inputPath: options.videoInputPath,
  47. ...pick(options, [ 'video', 'resolution', 'fps', 'copyCodecs', 'inputFileMutexReleaser', 'job' ])
  48. })
  49. }
  50. export async function onHLSVideoFileTranscoding (options: {
  51. video: MVideo
  52. videoOutputPath: string
  53. m3u8OutputPath: string
  54. filesLockedInParent?: boolean // default false
  55. }) {
  56. const { video, videoOutputPath, m3u8OutputPath, filesLockedInParent = false } = options
  57. // Create or update the playlist
  58. const playlist = await retryTransactionWrapper(() => {
  59. return sequelizeTypescript.transaction(async transaction => {
  60. return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
  61. })
  62. })
  63. const newVideoFile = await buildNewFile({ mode: 'hls', path: videoOutputPath })
  64. newVideoFile.videoStreamingPlaylistId = playlist.id
  65. const mutexReleaser = !filesLockedInParent
  66. ? await VideoPathManager.Instance.lockFiles(video.uuid)
  67. : null
  68. try {
  69. await video.reload()
  70. const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
  71. await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
  72. // Move playlist file
  73. const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(
  74. video,
  75. getHlsResolutionPlaylistFilename(newVideoFile.filename)
  76. )
  77. await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true })
  78. // Move video file
  79. await move(videoOutputPath, videoFilePath, { overwrite: true })
  80. await renameVideoFileInPlaylist(resolutionPlaylistPath, newVideoFile.filename)
  81. // Update video duration if it was not set (in case of a live for example)
  82. if (!video.duration) {
  83. video.duration = await getVideoStreamDuration(videoFilePath)
  84. await video.save()
  85. }
  86. await createTorrentAndSetInfoHash(playlist, newVideoFile)
  87. const oldFile = await VideoFileModel.loadHLSFile({
  88. playlistId: playlist.id,
  89. fps: newVideoFile.fps,
  90. resolution: newVideoFile.resolution
  91. })
  92. if (oldFile) {
  93. await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
  94. await oldFile.destroy()
  95. }
  96. const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
  97. await updatePlaylistAfterFileChange(video, playlist)
  98. return { resolutionPlaylistPath, videoFile: savedVideoFile }
  99. } finally {
  100. if (mutexReleaser) mutexReleaser()
  101. }
  102. }
  103. // ---------------------------------------------------------------------------
  104. async function generateHlsPlaylistCommon (options: {
  105. type: 'hls' | 'hls-from-ts'
  106. video: MVideo
  107. inputPath: string
  108. resolution: number
  109. fps: number
  110. inputFileMutexReleaser: MutexInterface.Releaser
  111. copyCodecs?: boolean
  112. isAAC?: boolean
  113. job?: Job
  114. }) {
  115. const { type, video, inputPath, resolution, fps, copyCodecs, isAAC, job, inputFileMutexReleaser } = options
  116. const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
  117. const videoTranscodedBasePath = join(transcodeDirectory, type)
  118. await ensureDir(videoTranscodedBasePath)
  119. const videoFilename = generateHLSVideoFilename(resolution)
  120. const videoOutputPath = join(videoTranscodedBasePath, videoFilename)
  121. const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
  122. const m3u8OutputPath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
  123. const transcodeOptions = {
  124. type,
  125. inputPath,
  126. outputPath: m3u8OutputPath,
  127. resolution,
  128. fps,
  129. copyCodecs,
  130. isAAC,
  131. inputFileMutexReleaser,
  132. hlsPlaylist: {
  133. videoFilename
  134. }
  135. }
  136. await buildFFmpegVOD(job).transcode(transcodeOptions)
  137. await onHLSVideoFileTranscoding({
  138. video,
  139. videoOutputPath,
  140. m3u8OutputPath,
  141. filesLockedInParent: !inputFileMutexReleaser
  142. })
  143. }