|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- import { buildAspectRatio } from '@peertube/peertube-core-utils'
- import { TranscodeVODOptionsType, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
- import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
- import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
- import { VideoModel } from '@server/models/video/video.js'
- import { MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
- import { Job } from 'bullmq'
- import { move, remove } from 'fs-extra/esm'
- import { copyFile } from 'fs/promises'
- import { basename, join } from 'path'
- import { CONFIG } from '../../initializers/config.js'
- import { VideoFileModel } from '../../models/video/video-file.js'
- import { JobQueue } from '../job-queue/index.js'
- import { generateWebVideoFilename } from '../paths.js'
- import { buildNewFile, saveNewOriginalFileIfNeeded } from '../video-file.js'
- import { buildStoryboardJobIfNeeded } from '../video-jobs.js'
- import { VideoPathManager } from '../video-path-manager.js'
- import { buildFFmpegVOD } from './shared/index.js'
- import { buildOriginalFileResolution } from './transcoding-resolutions.js'
-
- // Optimize the original video file and replace it. The resolution is not changed.
- export async function optimizeOriginalVideofile (options: {
- video: MVideoFullLight
- inputVideoFile: MVideoFile
- quickTranscode: boolean
- job: Job
- }) {
- const { video, inputVideoFile, quickTranscode, job } = options
-
- const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
- const newExtname = '.mp4'
-
- // Will be released by our transcodeVOD function once ffmpeg is ran
- const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
-
- try {
- await video.reload()
- await inputVideoFile.reload()
-
- const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
-
- const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => {
- const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
-
- const transcodeType: TranscodeVODOptionsType = quickTranscode
- ? 'quick-transcode'
- : 'video'
-
- const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
- const fps = computeOutputFPS({ inputFPS: inputVideoFile.fps, resolution })
-
- // Could be very long!
- await buildFFmpegVOD(job).transcode({
- type: transcodeType,
-
- inputPath: videoInputPath,
- outputPath: videoOutputPath,
-
- inputFileMutexReleaser,
-
- resolution,
- fps
- })
-
- const { videoFile } = await onWebVideoFileTranscoding({ video, videoOutputPath, deleteWebInputVideoFile: inputVideoFile })
-
- return { transcodeType, videoFile }
- })
-
- return result
- } finally {
- inputFileMutexReleaser()
- }
- }
-
- // Transcode the original/old/source video file to a lower resolution compatible with web browsers
- export async function transcodeNewWebVideoResolution (options: {
- video: MVideoFullLight
- resolution: number
- fps: number
- job: Job
- }) {
- const { video: videoArg, resolution, fps, job } = options
-
- const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
- const newExtname = '.mp4'
-
- const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid)
-
- try {
- const video = await VideoModel.loadFull(videoArg.uuid)
- const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
-
- const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
- const filename = generateWebVideoFilename(resolution, newExtname)
- const videoOutputPath = join(transcodeDirectory, filename)
-
- const transcodeOptions = {
- type: 'video' as 'video',
-
- inputPath: videoInputPath,
- outputPath: videoOutputPath,
-
- inputFileMutexReleaser,
-
- resolution,
- fps
- }
-
- await buildFFmpegVOD(job).transcode(transcodeOptions)
-
- return onWebVideoFileTranscoding({ video, videoOutputPath })
- })
-
- return result
- } finally {
- inputFileMutexReleaser()
- }
- }
-
- // Merge an image with an audio file to create a video
- export async function mergeAudioVideofile (options: {
- video: MVideoFullLight
- resolution: number
- fps: number
- job: Job
- }) {
- const { video: videoArg, resolution, fps, job } = options
-
- const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
- const newExtname = '.mp4'
-
- const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid)
-
- try {
- const video = await VideoModel.loadFull(videoArg.uuid)
- const inputVideoFile = video.getMinQualityFile()
-
- const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
-
- const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => {
- const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
-
- // If the user updates the video preview during transcoding
- const previewPath = video.getPreview().getPath()
- const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
- await copyFile(previewPath, tmpPreviewPath)
-
- const transcodeOptions = {
- type: 'merge-audio' as 'merge-audio',
-
- inputPath: tmpPreviewPath,
- outputPath: videoOutputPath,
-
- inputFileMutexReleaser,
-
- audioPath: audioInputPath,
- resolution,
- fps
- }
-
- try {
- await buildFFmpegVOD(job).transcode(transcodeOptions)
-
- await remove(tmpPreviewPath)
- } catch (err) {
- await remove(tmpPreviewPath)
- throw err
- }
-
- await onWebVideoFileTranscoding({
- video,
- videoOutputPath,
- deleteWebInputVideoFile: inputVideoFile,
- wasAudioFile: true
- })
- })
-
- return result
- } finally {
- inputFileMutexReleaser()
- }
- }
-
- export async function onWebVideoFileTranscoding (options: {
- video: MVideoFullLight
- videoOutputPath: string
- wasAudioFile?: boolean // default false
- deleteWebInputVideoFile?: MVideoFile
- }) {
- const { video, videoOutputPath, wasAudioFile, deleteWebInputVideoFile } = options
-
- const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
-
- const videoFile = await buildNewFile({ mode: 'web-video', path: videoOutputPath })
- videoFile.videoId = video.id
-
- try {
- await video.reload()
-
- // ffmpeg generated a new video file, so update the video duration
- // See https://trac.ffmpeg.org/ticket/5456
- if (wasAudioFile) {
- video.duration = await getVideoStreamDuration(videoOutputPath)
- video.aspectRatio = buildAspectRatio({ width: videoFile.width, height: videoFile.height })
- await video.save()
- }
-
- const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
-
- await move(videoOutputPath, outputPath, { overwrite: true })
-
- await createTorrentAndSetInfoHash(video, videoFile)
-
- if (deleteWebInputVideoFile) {
- await saveNewOriginalFileIfNeeded(video, deleteWebInputVideoFile)
-
- await video.removeWebVideoFile(deleteWebInputVideoFile)
- await deleteWebInputVideoFile.destroy()
- }
-
- const existingFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
- if (existingFile) await video.removeWebVideoFile(existingFile)
-
- await VideoFileModel.customUpsert(videoFile, 'video', undefined)
- video.VideoFiles = await video.$get('VideoFiles')
-
- if (wasAudioFile) {
- await JobQueue.Instance.createJob(buildStoryboardJobIfNeeded({ video, federate: false }))
- }
-
- return { video, videoFile }
- } finally {
- mutexReleaser()
- }
- }
|