import { pick } from '@peertube/peertube-core-utils' import { getVideoStreamDuration, HLSFromTSTranscodeOptions, HLSTranscodeOptions } from '@peertube/peertube-ffmpeg' import { retryTransactionWrapper } from '@server/helpers/database-utils.js' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js' import { sequelizeTypescript } from '@server/initializers/database.js' import { MVideo } from '@server/types/models/index.js' import { MutexInterface } from 'async-mutex' import { Job } from 'bullmq' import { ensureDir, move } from 'fs-extra/esm' import { join } from 'path' import { CONFIG } from '../../initializers/config.js' import { VideoFileModel } from '../../models/video/video-file.js' import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist.js' import { renameVideoFileInPlaylist, updateM3U8AndShaPlaylist } from '../hls.js' import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths.js' import { buildNewFile } from '../video-file.js' import { VideoPathManager } from '../video-path-manager.js' import { buildFFmpegVOD } from './shared/index.js' // Concat TS segments from a live video to a fragmented mp4 HLS playlist export async function generateHlsPlaylistResolutionFromTS (options: { video: MVideo concatenatedTsFilePath: string resolution: number fps: number isAAC: boolean inputFileMutexReleaser: MutexInterface.Releaser }) { return generateHlsPlaylistCommon({ type: 'hls-from-ts' as 'hls-from-ts', videoInputPath: options.concatenatedTsFilePath, ...pick(options, [ 'video', 'resolution', 'fps', 'inputFileMutexReleaser', 'isAAC' ]) }) } // Generate an HLS playlist from an input file, and update the master playlist export function generateHlsPlaylistResolution (options: { video: MVideo videoInputPath: string separatedAudioInputPath: string resolution: number fps: number copyCodecs: boolean inputFileMutexReleaser: MutexInterface.Releaser separatedAudio: boolean job?: Job }) { return generateHlsPlaylistCommon({ type: 'hls' as 'hls', ...pick(options, [ 'videoInputPath', 'separatedAudioInputPath', 'video', 'resolution', 'fps', 'copyCodecs', 'separatedAudio', 'inputFileMutexReleaser', 'job' ]) }) } export async function onHLSVideoFileTranscoding (options: { video: MVideo videoOutputPath: string m3u8OutputPath: string filesLockedInParent?: boolean // default false }) { const { video, videoOutputPath, m3u8OutputPath, filesLockedInParent = false } = options // Create or update the playlist const playlist = await retryTransactionWrapper(() => { return sequelizeTypescript.transaction(async transaction => { return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction) }) }) const newVideoFile = await buildNewFile({ mode: 'hls', path: videoOutputPath }) newVideoFile.videoStreamingPlaylistId = playlist.id const mutexReleaser = !filesLockedInParent ? await VideoPathManager.Instance.lockFiles(video.uuid) : null try { await video.reload() const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) // Move playlist file const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath( video, getHlsResolutionPlaylistFilename(newVideoFile.filename) ) await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true }) // Move video file await move(videoOutputPath, videoFilePath, { overwrite: true }) await renameVideoFileInPlaylist(resolutionPlaylistPath, newVideoFile.filename) // Update video duration if it was not set (in case of a live for example) if (!video.duration) { video.duration = await getVideoStreamDuration(videoFilePath) await video.save() } await createTorrentAndSetInfoHash(playlist, newVideoFile) const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution }) if (oldFile) { await video.removeStreamingPlaylistVideoFile(playlist, oldFile) await oldFile.destroy() } const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) await updateM3U8AndShaPlaylist(video, playlist) return { resolutionPlaylistPath, videoFile: savedVideoFile } } finally { if (mutexReleaser) mutexReleaser() } } // --------------------------------------------------------------------------- // Private // --------------------------------------------------------------------------- async function generateHlsPlaylistCommon (options: { type: 'hls' | 'hls-from-ts' video: MVideo videoInputPath: string separatedAudioInputPath?: string resolution: number fps: number inputFileMutexReleaser: MutexInterface.Releaser separatedAudio?: boolean copyCodecs?: boolean isAAC?: boolean job?: Job }) { const { type, video, videoInputPath, separatedAudioInputPath, resolution, fps, copyCodecs, separatedAudio, isAAC, job, inputFileMutexReleaser } = options const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const videoTranscodedBasePath = join(transcodeDirectory, type) await ensureDir(videoTranscodedBasePath) const videoFilename = generateHLSVideoFilename(resolution) const videoOutputPath = join(videoTranscodedBasePath, videoFilename) const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename) const m3u8OutputPath = join(videoTranscodedBasePath, resolutionPlaylistFilename) const transcodeOptions: HLSTranscodeOptions | HLSFromTSTranscodeOptions = { type, videoInputPath, separatedAudioInputPath, outputPath: m3u8OutputPath, resolution, fps, copyCodecs, separatedAudio, isAAC, inputFileMutexReleaser, hlsPlaylist: { videoFilename } } await buildFFmpegVOD(job).transcode(transcodeOptions) await onHLSVideoFileTranscoding({ video, videoOutputPath, m3u8OutputPath, filesLockedInParent: !inputFileMutexReleaser }) }