ニジカ投稿局 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.
 
 
 
 
 

157 lines
5.2 KiB

  1. import { buildAspectRatio } from '@peertube/peertube-core-utils'
  2. import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
  3. import { VideoStudioEditionPayload, VideoStudioTask, VideoStudioTaskPayload } from '@peertube/peertube-models'
  4. import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
  5. import { createTorrentAndSetInfoHashFromPath } from '@server/helpers/webtorrent.js'
  6. import { CONFIG } from '@server/initializers/config.js'
  7. import { VideoCaptionModel } from '@server/models/video/video-caption.js'
  8. import { MUser, MVideo, MVideoFile, MVideoFullLight, MVideoWithAllFiles } from '@server/types/models/index.js'
  9. import { move, remove } from 'fs-extra/esm'
  10. import { join } from 'path'
  11. import { JobQueue } from './job-queue/index.js'
  12. import { VideoStudioTranscodingJobHandler } from './runners/index.js'
  13. import { getTranscodingJobPriority } from './transcoding/transcoding-priority.js'
  14. import { createTranscriptionTaskIfNeeded } from './video-captions.js'
  15. import { buildNewFile, removeHLSPlaylist, removeWebVideoFile } from './video-file.js'
  16. import { buildStoryboardJobIfNeeded } from './video-jobs.js'
  17. import { VideoPathManager } from './video-path-manager.js'
  18. const lTags = loggerTagsFactory('video-studio')
  19. export function buildTaskFileFieldname (indice: number, fieldName = 'file') {
  20. return `tasks[${indice}][options][${fieldName}]`
  21. }
  22. export function getTaskFileFromReq (files: Express.Multer.File[], indice: number, fieldName = 'file') {
  23. return files.find(f => f.fieldname === buildTaskFileFieldname(indice, fieldName))
  24. }
  25. export function getStudioTaskFilePath (filename: string) {
  26. return join(CONFIG.STORAGE.TMP_PERSISTENT_DIR, filename)
  27. }
  28. export async function safeCleanupStudioTMPFiles (tasks: VideoStudioTaskPayload[]) {
  29. logger.info('Removing studio task files', { tasks, ...lTags() })
  30. for (const task of tasks) {
  31. try {
  32. if (task.name === 'add-intro' || task.name === 'add-outro') {
  33. await remove(task.options.file)
  34. } else if (task.name === 'add-watermark') {
  35. await remove(task.options.file)
  36. }
  37. } catch (err) {
  38. logger.error('Cannot remove studio file', { err })
  39. }
  40. }
  41. }
  42. // ---------------------------------------------------------------------------
  43. export async function approximateIntroOutroAdditionalSize (
  44. video: MVideoFullLight,
  45. tasks: VideoStudioTask[],
  46. fileFinder: (i: number) => string
  47. ) {
  48. let additionalDuration = 0
  49. for (let i = 0; i < tasks.length; i++) {
  50. const task = tasks[i]
  51. if (task.name !== 'add-intro' && task.name !== 'add-outro') continue
  52. const filePath = fileFinder(i)
  53. additionalDuration += await getVideoStreamDuration(filePath)
  54. }
  55. return (video.getMaxQualityFile().size / video.duration) * additionalDuration
  56. }
  57. // ---------------------------------------------------------------------------
  58. export async function createVideoStudioJob (options: {
  59. video: MVideo
  60. user: MUser
  61. payload: VideoStudioEditionPayload
  62. }) {
  63. const { video, user, payload } = options
  64. const priority = await getTranscodingJobPriority({ user, type: 'studio', fallback: 0 })
  65. if (CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED) {
  66. await new VideoStudioTranscodingJobHandler().create({ video, tasks: payload.tasks, priority })
  67. return
  68. }
  69. await JobQueue.Instance.createJob({ type: 'video-studio-edition', payload, priority })
  70. }
  71. export async function onVideoStudioEnded (options: {
  72. editionResultPath: string
  73. tasks: VideoStudioTaskPayload[]
  74. video: MVideoFullLight
  75. }) {
  76. const { video, tasks, editionResultPath } = options
  77. const newFile = await buildNewFile({ path: editionResultPath, mode: 'web-video' })
  78. newFile.videoId = video.id
  79. const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile)
  80. await move(editionResultPath, outputPath)
  81. await safeCleanupStudioTMPFiles(tasks)
  82. await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath)
  83. await removeAllFiles(video, newFile)
  84. await newFile.save()
  85. video.duration = await getVideoStreamDuration(outputPath)
  86. video.aspectRatio = buildAspectRatio({ width: newFile.width, height: newFile.height })
  87. await video.save()
  88. await JobQueue.Instance.createSequentialJobFlow(
  89. buildStoryboardJobIfNeeded({ video, federate: false }),
  90. {
  91. type: 'federate-video' as 'federate-video',
  92. payload: {
  93. videoUUID: video.uuid,
  94. isNewVideoForFederation: false
  95. }
  96. },
  97. {
  98. type: 'transcoding-job-builder' as 'transcoding-job-builder',
  99. payload: {
  100. videoUUID: video.uuid,
  101. optimizeJob: {
  102. isNewVideo: false
  103. }
  104. }
  105. }
  106. )
  107. if (video.language && CONFIG.VIDEO_TRANSCRIPTION.ENABLED) {
  108. const caption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, video.language)
  109. if (caption?.automaticallyGenerated) {
  110. await createTranscriptionTaskIfNeeded(video)
  111. }
  112. }
  113. }
  114. // ---------------------------------------------------------------------------
  115. // Private
  116. // ---------------------------------------------------------------------------
  117. async function removeAllFiles (video: MVideoWithAllFiles, webVideoFileException: MVideoFile) {
  118. await removeHLSPlaylist(video)
  119. for (const file of video.VideoFiles) {
  120. if (file.id === webVideoFileException.id) continue
  121. await removeWebVideoFile(video, file.id)
  122. }
  123. }