1 import { MutexInterface } from 'async-mutex'
2 import { Job } from 'bullmq'
3 import { ensureDir, move, stat } from 'fs-extra'
4 import { basename, extname as extnameUtil, join } from 'path'
5 import { retryTransactionWrapper } from '@server/helpers/database-utils'
6 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
7 import { sequelizeTypescript } from '@server/initializers/database'
8 import { MVideo, MVideoFile } from '@server/types/models'
9 import { pick } from '@shared/core-utils'
10 import { getVideoStreamDuration, getVideoStreamFPS } from '@shared/ffmpeg'
11 import { VideoResolution } from '@shared/models'
12 import { CONFIG } from '../../initializers/config'
13 import { VideoFileModel } from '../../models/video/video-file'
14 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
15 import { updatePlaylistAfterFileChange } from '../hls'
16 import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths'
17 import { buildFileMetadata } from '../video-file'
18 import { VideoPathManager } from '../video-path-manager'
19 import { buildFFmpegVOD } from './shared'
21 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
22 export async function generateHlsPlaylistResolutionFromTS (options: {
24 concatenatedTsFilePath: string
25 resolution: VideoResolution
28 inputFileMutexReleaser: MutexInterface.Releaser
30 return generateHlsPlaylistCommon({
31 type: 'hls-from-ts' as 'hls-from-ts',
32 inputPath: options.concatenatedTsFilePath,
34 ...pick(options, [ 'video', 'resolution', 'fps', 'inputFileMutexReleaser', 'isAAC' ])
38 // Generate an HLS playlist from an input file, and update the master playlist
39 export function generateHlsPlaylistResolution (options: {
41 videoInputPath: string
42 resolution: VideoResolution
45 inputFileMutexReleaser: MutexInterface.Releaser
48 return generateHlsPlaylistCommon({
50 inputPath: options.videoInputPath,
52 ...pick(options, [ 'video', 'resolution', 'fps', 'copyCodecs', 'inputFileMutexReleaser', 'job' ])
56 export async function onHLSVideoFileTranscoding (options: {
59 videoOutputPath: string
60 m3u8OutputPath: string
62 const { video, videoFile, videoOutputPath, m3u8OutputPath } = options
64 // Create or update the playlist
65 const playlist = await retryTransactionWrapper(() => {
66 return sequelizeTypescript.transaction(async transaction => {
67 return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
70 videoFile.videoStreamingPlaylistId = playlist.id
72 const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
77 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, videoFile)
78 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
81 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, basename(m3u8OutputPath))
82 await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true })
84 await move(videoOutputPath, videoFilePath, { overwrite: true })
86 // Update video duration if it was not set (in case of a live for example)
87 if (!video.duration) {
88 video.duration = await getVideoStreamDuration(videoFilePath)
92 const stats = await stat(videoFilePath)
94 videoFile.size = stats.size
95 videoFile.fps = await getVideoStreamFPS(videoFilePath)
96 videoFile.metadata = await buildFileMetadata(videoFilePath)
98 await createTorrentAndSetInfoHash(playlist, videoFile)
100 const oldFile = await VideoFileModel.loadHLSFile({
101 playlistId: playlist.id,
103 resolution: videoFile.resolution
107 await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
108 await oldFile.destroy()
111 const savedVideoFile = await VideoFileModel.customUpsert(videoFile, 'streaming-playlist', undefined)
113 await updatePlaylistAfterFileChange(video, playlist)
115 return { resolutionPlaylistPath, videoFile: savedVideoFile }
121 // ---------------------------------------------------------------------------
123 async function generateHlsPlaylistCommon (options: {
124 type: 'hls' | 'hls-from-ts'
128 resolution: VideoResolution
131 inputFileMutexReleaser: MutexInterface.Releaser
138 const { type, video, inputPath, resolution, fps, copyCodecs, isAAC, job, inputFileMutexReleaser } = options
139 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
141 const videoTranscodedBasePath = join(transcodeDirectory, type)
142 await ensureDir(videoTranscodedBasePath)
144 const videoFilename = generateHLSVideoFilename(resolution)
145 const videoOutputPath = join(videoTranscodedBasePath, videoFilename)
147 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
148 const m3u8OutputPath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
150 const transcodeOptions = {
154 outputPath: m3u8OutputPath,
162 inputFileMutexReleaser,
169 await buildFFmpegVOD(job).transcode(transcodeOptions)
171 const newVideoFile = new VideoFileModel({
173 extname: extnameUtil(videoFilename),
175 filename: videoFilename,
179 await onHLSVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath, m3u8OutputPath })