X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Flib%2Ftranscoding%2Fvideo-transcoding.ts;h=9942a067b08e40d75aea435524e0214e4dd9fe28;hb=743dab5517d4501f6b35cfc795de6c8b6f41ebb3;hp=5df192575f1ea6548f8291ca703b133a400d199f;hpb=1e4d2cb5aef11898585fae4053da4ebd0a69b480;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/lib/transcoding/video-transcoding.ts b/server/lib/transcoding/video-transcoding.ts index 5df192575..9942a067b 100644 --- a/server/lib/transcoding/video-transcoding.ts +++ b/server/lib/transcoding/video-transcoding.ts @@ -1,19 +1,26 @@ import { Job } from 'bull' import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' import { basename, extname as extnameUtil, join } from 'path' +import { toEven } from '@server/helpers/core-utils' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models' -import { VideoResolution } from '../../../shared/models/videos' +import { VideoResolution, VideoStorage } from '../../../shared/models/videos' import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils' import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' -import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' -import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../../initializers/constants' +import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' import { VideoFileModel } from '../../models/video/video-file' import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' -import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from '../video-paths' +import { + generateHLSMasterPlaylistFilename, + generateHlsSha256SegmentsFilename, + generateHLSVideoFilename, + generateWebTorrentVideoFilename, + getHlsResolutionPlaylistFilename +} from '../paths' +import { VideoPathManager } from '../video-path-manager' import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' /** @@ -24,157 +31,155 @@ import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' */ // Optimize the original video file and replace it. The resolution is not changed. -async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) { +function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) { const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const newExtname = '.mp4' - const videoInputPath = getVideoFilePath(video, inputVideoFile) - const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) + return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => { + const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) - const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) - ? 'quick-transcode' - : 'video' + const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) + ? 'quick-transcode' + : 'video' - const transcodeOptions: TranscodeOptions = { - type: transcodeType, + const resolution = toEven(inputVideoFile.resolution) - inputPath: videoInputPath, - outputPath: videoTranscodedPath, + const transcodeOptions: TranscodeOptions = { + type: transcodeType, - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE, + inputPath: videoInputPath, + outputPath: videoTranscodedPath, - resolution: inputVideoFile.resolution, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), + profile: CONFIG.TRANSCODING.PROFILE, - job - } + resolution, - // Could be very long! - await transcode(transcodeOptions) + job + } - try { - await remove(videoInputPath) + // Could be very long! + await transcode(transcodeOptions) // Important to do this before getVideoFilename() to take in account the new filename inputVideoFile.extname = newExtname - inputVideoFile.filename = generateVideoFilename(video, false, inputVideoFile.resolution, newExtname) - - const videoOutputPath = getVideoFilePath(video, inputVideoFile) + inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) + inputVideoFile.storage = VideoStorage.FILE_SYSTEM - await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) + const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) - return transcodeType - } catch (err) { - // Auto destruction... - video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) + const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) + await remove(videoInputPath) - throw err - } + return { transcodeType, videoFile } + }) } -// Transcode the original video file to a lower resolution. -async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) { +// Transcode the original video file to a lower resolution +// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed +function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) { const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const extname = '.mp4' - // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed - const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile()) + return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => { + const newVideoFile = new VideoFileModel({ + resolution, + extname, + filename: generateWebTorrentVideoFilename(resolution, extname), + size: 0, + videoId: video.id + }) - const newVideoFile = new VideoFileModel({ - resolution, - extname, - filename: generateVideoFilename(video, false, resolution, extname), - size: 0, - videoId: video.id - }) + const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile) + const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) - const videoOutputPath = getVideoFilePath(video, newVideoFile) - const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) + const transcodeOptions = resolution === VideoResolution.H_NOVIDEO + ? { + type: 'only-audio' as 'only-audio', - const transcodeOptions = resolution === VideoResolution.H_NOVIDEO - ? { - type: 'only-audio' as 'only-audio', + inputPath: videoInputPath, + outputPath: videoTranscodedPath, - inputPath: videoInputPath, - outputPath: videoTranscodedPath, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), + profile: CONFIG.TRANSCODING.PROFILE, - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE, + resolution, - resolution, + job + } + : { + type: 'video' as 'video', + inputPath: videoInputPath, + outputPath: videoTranscodedPath, - job - } - : { - type: 'video' as 'video', - inputPath: videoInputPath, - outputPath: videoTranscodedPath, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), + profile: CONFIG.TRANSCODING.PROFILE, - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE, - - resolution, - isPortraitMode: isPortrait, + resolution, + isPortraitMode: isPortrait, - job - } + job + } - await transcode(transcodeOptions) + await transcode(transcodeOptions) - return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) + return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) + }) } // Merge an image with an audio file to create a video -async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) { +function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) { const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const newExtname = '.mp4' const inputVideoFile = video.getMinQualityFile() - const audioInputPath = getVideoFilePath(video, inputVideoFile) - const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) + return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async audioInputPath => { + const videoTranscodedPath = 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) + // 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', + const transcodeOptions = { + type: 'merge-audio' as 'merge-audio', - inputPath: tmpPreviewPath, - outputPath: videoTranscodedPath, + inputPath: tmpPreviewPath, + outputPath: videoTranscodedPath, - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), + profile: CONFIG.TRANSCODING.PROFILE, - audioPath: audioInputPath, - resolution, + audioPath: audioInputPath, + resolution, - job - } + job + } - try { - await transcode(transcodeOptions) + try { + await transcode(transcodeOptions) - await remove(audioInputPath) - await remove(tmpPreviewPath) - } catch (err) { - await remove(tmpPreviewPath) - throw err - } + await remove(audioInputPath) + await remove(tmpPreviewPath) + } catch (err) { + await remove(tmpPreviewPath) + throw err + } - // Important to do this before getVideoFilename() to take in account the new file extension - inputVideoFile.extname = newExtname - inputVideoFile.filename = generateVideoFilename(video, false, inputVideoFile.resolution, newExtname) + // Important to do this before getVideoFilename() to take in account the new file extension + inputVideoFile.extname = newExtname + inputVideoFile.resolution = resolution + inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) - const videoOutputPath = getVideoFilePath(video, inputVideoFile) - // ffmpeg generated a new video file, so update the video duration - // See https://trac.ffmpeg.org/ticket/5456 - video.duration = await getDurationFromVideoFile(videoTranscodedPath) - await video.save() + const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) + // ffmpeg generated a new video file, so update the video duration + // See https://trac.ffmpeg.org/ticket/5456 + video.duration = await getDurationFromVideoFile(videoTranscodedPath) + await video.save() - return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) + return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) + }) } // Concat TS segments from a live video to a fragmented mp4 HLS playlist @@ -248,7 +253,7 @@ async function onWebTorrentVideoFileTranscoding ( await VideoFileModel.customUpsert(videoFile, 'video', undefined) video.VideoFiles = await video.$get('VideoFiles') - return video + return { video, videoFile } } async function generateHlsPlaylistCommon (options: { @@ -268,15 +273,15 @@ async function generateHlsPlaylistCommon (options: { const videoTranscodedBasePath = join(transcodeDirectory, type) await ensureDir(videoTranscodedBasePath) - const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution) - const playlistFilename = VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution) - const playlistFileTranscodePath = join(videoTranscodedBasePath, playlistFilename) + const videoFilename = generateHLSVideoFilename(resolution) + const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename) + const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename) const transcodeOptions = { type, inputPath, - outputPath: playlistFileTranscodePath, + outputPath: resolutionPlaylistFileTranscodePath, availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), profile: CONFIG.TRANSCODING.PROFILE, @@ -296,19 +301,23 @@ async function generateHlsPlaylistCommon (options: { await transcode(transcodeOptions) - const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) - // Create or update the playlist - const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({ - videoId: video.id, - playlistUrl, - segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive), - p2pMediaLoaderInfohashes: [], - p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, + const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video) + + if (!playlist.playlistFilename) { + playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive) + } + + if (!playlist.segmentsSha256Filename) { + playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive) + } + + playlist.p2pMediaLoaderInfohashes = [] + playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION - type: VideoStreamingPlaylistType.HLS - }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ] - videoStreamingPlaylist.Video = video + playlist.type = VideoStreamingPlaylistType.HLS + + await playlist.save() // Build the new playlist file const extname = extnameUtil(videoFilename) @@ -316,20 +325,19 @@ async function generateHlsPlaylistCommon (options: { resolution, extname, size: 0, - filename: generateVideoFilename(video, true, resolution, extname), + filename: videoFilename, fps: -1, - videoStreamingPlaylistId: videoStreamingPlaylist.id + videoStreamingPlaylistId: playlist.id }) - const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile) + const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) // Move files from tmp transcoded directory to the appropriate place - const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) - await ensureDir(baseHlsDirectory) + await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) // Move playlist file - const playlistPath = join(baseHlsDirectory, playlistFilename) - await move(playlistFileTranscodePath, playlistPath, { overwrite: true }) + const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename) + await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) // Move video file await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) @@ -339,20 +347,20 @@ async function generateHlsPlaylistCommon (options: { newVideoFile.fps = await getVideoFileFPS(videoFilePath) newVideoFile.metadata = await getMetadataFromFile(videoFilePath) - await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile) + await createTorrentAndSetInfoHash(playlist, newVideoFile) + + const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) - await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) - videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') + const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo + playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles') + playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles) - videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes( - playlistUrl, videoStreamingPlaylist.VideoFiles - ) - await videoStreamingPlaylist.save() + await playlist.save() - video.setHLSPlaylist(videoStreamingPlaylist) + video.setHLSPlaylist(playlist) - await updateMasterHLSPlaylist(video) - await updateSha256VODSegments(video) + await updateMasterHLSPlaylist(video, playlistWithFiles) + await updateSha256VODSegments(video, playlistWithFiles) - return playlistPath + return { resolutionPlaylistPath, videoFile: savedVideoFile } }