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 { generateHLSVideoFilename, generateWebTorrentVideoFilename, getVideoFilePath } from '../video-paths'
+import {
+ generateHLSMasterPlaylistFilename,
+ generateHlsSha256SegmentsFilename,
+ generateHLSVideoFilename,
+ generateWebTorrentVideoFilename,
+ getHlsResolutionPlaylistFilename
+} from '../paths'
+import { VideoPathManager } from '../video-path-manager'
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 resolution = toEven(inputVideoFile.resolution)
+ const resolution = toEven(inputVideoFile.resolution)
- const transcodeOptions: TranscodeOptions = {
- type: transcodeType,
+ const transcodeOptions: TranscodeOptions = {
+ type: transcodeType,
- inputPath: videoInputPath,
- outputPath: videoTranscodedPath,
-
- availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
- profile: CONFIG.TRANSCODING.PROFILE,
+ inputPath: videoInputPath,
+ outputPath: videoTranscodedPath,
- 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 = generateWebTorrentVideoFilename(resolution, newExtname)
+ inputVideoFile.storage = VideoStorage.FILE_SYSTEM
- const videoOutputPath = getVideoFilePath(video, inputVideoFile)
+ const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
- await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
-
- 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: generateWebTorrentVideoFilename(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 = generateWebTorrentVideoFilename(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
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
video.VideoFiles = await video.$get('VideoFiles')
- return video
+ return { video, videoFile }
}
async function generateHlsPlaylistCommon (options: {
await ensureDir(videoTranscodedBasePath)
const videoFilename = generateHLSVideoFilename(resolution)
- const playlistFilename = VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)
- const playlistFileTranscodePath = join(videoTranscodedBasePath, playlistFilename)
+ 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,
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)
size: 0,
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 })
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 }
}