video-file.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, isAudioFile } from '@peertube/peertube-ffmpeg'
  2. import { FileStorage, VideoFileMetadata, VideoResolution } from '@peertube/peertube-models'
  3. import { getFileSize, getLowercaseExtension } from '@peertube/peertube-node-utils'
  4. import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
  5. import { CONFIG } from '@server/initializers/config.js'
  6. import { MIMETYPES } from '@server/initializers/constants.js'
  7. import { VideoFileModel } from '@server/models/video/video-file.js'
  8. import { VideoSourceModel } from '@server/models/video/video-source.js'
  9. import { MVideo, MVideoFile, MVideoId, MVideoWithAllFiles } from '@server/types/models/index.js'
  10. import { FfprobeData } from 'fluent-ffmpeg'
  11. import { move, remove } from 'fs-extra'
  12. import { lTags } from './object-storage/shared/index.js'
  13. import { storeOriginalVideoFile } from './object-storage/videos.js'
  14. import { generateHLSVideoFilename, generateWebVideoFilename } from './paths.js'
  15. import { VideoPathManager } from './video-path-manager.js'
  16. export async function buildNewFile (options: {
  17. path: string
  18. mode: 'web-video' | 'hls'
  19. ffprobe?: FfprobeData
  20. }) {
  21. const { path, mode, ffprobe: probeArg } = options
  22. const probe = probeArg ?? await ffprobePromise(path)
  23. const size = await getFileSize(path)
  24. const videoFile = new VideoFileModel({
  25. extname: getLowercaseExtension(path),
  26. size,
  27. metadata: await buildFileMetadata(path, probe)
  28. })
  29. if (await isAudioFile(path, probe)) {
  30. videoFile.fps = 0
  31. videoFile.resolution = VideoResolution.H_NOVIDEO
  32. videoFile.width = 0
  33. videoFile.height = 0
  34. } else {
  35. const dimensions = await getVideoStreamDimensionsInfo(path, probe)
  36. videoFile.fps = await getVideoStreamFPS(path, probe)
  37. videoFile.resolution = dimensions.resolution
  38. videoFile.width = dimensions.width
  39. videoFile.height = dimensions.height
  40. }
  41. videoFile.filename = mode === 'web-video'
  42. ? generateWebVideoFilename(videoFile.resolution, videoFile.extname)
  43. : generateHLSVideoFilename(videoFile.resolution)
  44. return videoFile
  45. }
  46. // ---------------------------------------------------------------------------
  47. export async function removeHLSPlaylist (video: MVideoWithAllFiles) {
  48. const hls = video.getHLSPlaylist()
  49. if (!hls) return
  50. const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
  51. try {
  52. await video.removeStreamingPlaylistFiles(hls)
  53. await hls.destroy()
  54. video.VideoStreamingPlaylists = video.VideoStreamingPlaylists.filter(p => p.id !== hls.id)
  55. } finally {
  56. videoFileMutexReleaser()
  57. }
  58. }
  59. export async function removeHLSFile (video: MVideoWithAllFiles, fileToDeleteId: number) {
  60. logger.info('Deleting HLS file %d of %s.', fileToDeleteId, video.url, lTags(video.uuid))
  61. const hls = video.getHLSPlaylist()
  62. const files = hls.VideoFiles
  63. if (files.length === 1) {
  64. await removeHLSPlaylist(video)
  65. return undefined
  66. }
  67. const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
  68. try {
  69. const toDelete = files.find(f => f.id === fileToDeleteId)
  70. await video.removeStreamingPlaylistVideoFile(video.getHLSPlaylist(), toDelete)
  71. await toDelete.destroy()
  72. hls.VideoFiles = hls.VideoFiles.filter(f => f.id !== toDelete.id)
  73. } finally {
  74. videoFileMutexReleaser()
  75. }
  76. return hls
  77. }
  78. // ---------------------------------------------------------------------------
  79. export async function removeAllWebVideoFiles (video: MVideoWithAllFiles) {
  80. const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
  81. try {
  82. for (const file of video.VideoFiles) {
  83. await video.removeWebVideoFile(file)
  84. await file.destroy()
  85. }
  86. video.VideoFiles = []
  87. } finally {
  88. videoFileMutexReleaser()
  89. }
  90. return video
  91. }
  92. export async function removeWebVideoFile (video: MVideoWithAllFiles, fileToDeleteId: number) {
  93. const files = video.VideoFiles
  94. if (files.length === 1) {
  95. return removeAllWebVideoFiles(video)
  96. }
  97. const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
  98. try {
  99. const toDelete = files.find(f => f.id === fileToDeleteId)
  100. await video.removeWebVideoFile(toDelete)
  101. await toDelete.destroy()
  102. video.VideoFiles = files.filter(f => f.id !== toDelete.id)
  103. } finally {
  104. videoFileMutexReleaser()
  105. }
  106. return video
  107. }
  108. // ---------------------------------------------------------------------------
  109. export async function buildFileMetadata (path: string, existingProbe?: FfprobeData) {
  110. const metadata = existingProbe || await ffprobePromise(path)
  111. return new VideoFileMetadata(metadata)
  112. }
  113. export function getVideoFileMimeType (extname: string, isAudio: boolean) {
  114. return isAudio && extname === '.mp4' // We use .mp4 even for audio file only
  115. ? MIMETYPES.AUDIO.EXT_MIMETYPE['.m4a']
  116. : MIMETYPES.VIDEO.EXT_MIMETYPE[extname]
  117. }
  118. // ---------------------------------------------------------------------------
  119. export async function createVideoSource (options: {
  120. inputFilename: string
  121. inputProbe: FfprobeData
  122. inputPath: string
  123. video: MVideoId
  124. createdAt?: Date
  125. }) {
  126. const { inputFilename, inputPath, inputProbe, video, createdAt } = options
  127. const videoSource = new VideoSourceModel({
  128. inputFilename,
  129. videoId: video.id,
  130. createdAt
  131. })
  132. if (inputPath) {
  133. const probe = inputProbe ?? await ffprobePromise(inputPath)
  134. if (await isAudioFile(inputPath, probe)) {
  135. videoSource.fps = 0
  136. videoSource.resolution = VideoResolution.H_NOVIDEO
  137. videoSource.width = 0
  138. videoSource.height = 0
  139. } else {
  140. const dimensions = await getVideoStreamDimensionsInfo(inputPath, probe)
  141. videoSource.fps = await getVideoStreamFPS(inputPath, probe)
  142. videoSource.resolution = dimensions.resolution
  143. videoSource.width = dimensions.width
  144. videoSource.height = dimensions.height
  145. }
  146. videoSource.metadata = await buildFileMetadata(inputPath, probe)
  147. videoSource.size = await getFileSize(inputPath)
  148. }
  149. return videoSource.save()
  150. }
  151. export async function saveNewOriginalFileIfNeeded (video: MVideo, videoFile: MVideoFile) {
  152. if (!CONFIG.TRANSCODING.ORIGINAL_FILE.KEEP) return
  153. const videoSource = await VideoSourceModel.loadLatest(video.id)
  154. // Already have saved an original file
  155. if (!videoSource || videoSource.keptOriginalFilename) return
  156. videoSource.keptOriginalFilename = videoFile.filename
  157. const lTags = loggerTagsFactory(video.uuid)
  158. logger.info(`Storing original video file ${videoSource.keptOriginalFilename} of video ${video.name}`, lTags())
  159. const sourcePath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
  160. if (CONFIG.OBJECT_STORAGE.ENABLED) {
  161. const fileUrl = await storeOriginalVideoFile(sourcePath, videoSource.keptOriginalFilename)
  162. await remove(sourcePath)
  163. videoSource.storage = FileStorage.OBJECT_STORAGE
  164. videoSource.fileUrl = fileUrl
  165. } else {
  166. const destinationPath = VideoPathManager.Instance.getFSOriginalVideoFilePath(videoSource.keptOriginalFilename)
  167. await move(sourcePath, destinationPath)
  168. videoSource.storage = FileStorage.FILE_SYSTEM
  169. }
  170. await videoSource.save()
  171. // Delete previously kept video files
  172. const allSources = await VideoSourceModel.listAll(video.id)
  173. for (const oldSource of allSources) {
  174. if (!oldSource.keptOriginalFilename) continue
  175. if (oldSource.id === videoSource.id) continue
  176. try {
  177. await video.removeOriginalFile(oldSource)
  178. } catch (err) {
  179. logger.error('Cannot delete old original file ' + oldSource.keptOriginalFilename, { err, ...lTags() })
  180. }
  181. }
  182. }