]>
Commit | Line | Data |
---|---|---|
0c9668f7 C |
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' | |
20 | ||
21 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist | |
22 | export async function generateHlsPlaylistResolutionFromTS (options: { | |
23 | video: MVideo | |
24 | concatenatedTsFilePath: string | |
25 | resolution: VideoResolution | |
26 | fps: number | |
27 | isAAC: boolean | |
28 | inputFileMutexReleaser: MutexInterface.Releaser | |
29 | }) { | |
30 | return generateHlsPlaylistCommon({ | |
31 | type: 'hls-from-ts' as 'hls-from-ts', | |
32 | inputPath: options.concatenatedTsFilePath, | |
33 | ||
34 | ...pick(options, [ 'video', 'resolution', 'fps', 'inputFileMutexReleaser', 'isAAC' ]) | |
35 | }) | |
36 | } | |
37 | ||
38 | // Generate an HLS playlist from an input file, and update the master playlist | |
39 | export function generateHlsPlaylistResolution (options: { | |
40 | video: MVideo | |
41 | videoInputPath: string | |
42 | resolution: VideoResolution | |
43 | fps: number | |
44 | copyCodecs: boolean | |
45 | inputFileMutexReleaser: MutexInterface.Releaser | |
46 | job?: Job | |
47 | }) { | |
48 | return generateHlsPlaylistCommon({ | |
49 | type: 'hls' as 'hls', | |
50 | inputPath: options.videoInputPath, | |
51 | ||
52 | ...pick(options, [ 'video', 'resolution', 'fps', 'copyCodecs', 'inputFileMutexReleaser', 'job' ]) | |
53 | }) | |
54 | } | |
55 | ||
56 | export async function onHLSVideoFileTranscoding (options: { | |
57 | video: MVideo | |
58 | videoFile: MVideoFile | |
59 | videoOutputPath: string | |
60 | m3u8OutputPath: string | |
61 | }) { | |
62 | const { video, videoFile, videoOutputPath, m3u8OutputPath } = options | |
63 | ||
64 | // Create or update the playlist | |
65 | const playlist = await retryTransactionWrapper(() => { | |
66 | return sequelizeTypescript.transaction(async transaction => { | |
67 | return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction) | |
68 | }) | |
69 | }) | |
70 | videoFile.videoStreamingPlaylistId = playlist.id | |
71 | ||
72 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | |
73 | ||
74 | try { | |
75 | // VOD transcoding is a long task, refresh video attributes | |
76 | await video.reload() | |
77 | ||
78 | const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, videoFile) | |
79 | await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) | |
80 | ||
81 | // Move playlist file | |
82 | const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, basename(m3u8OutputPath)) | |
83 | await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true }) | |
84 | // Move video file | |
85 | await move(videoOutputPath, videoFilePath, { overwrite: true }) | |
86 | ||
87 | // Update video duration if it was not set (in case of a live for example) | |
88 | if (!video.duration) { | |
89 | video.duration = await getVideoStreamDuration(videoFilePath) | |
90 | await video.save() | |
91 | } | |
92 | ||
93 | const stats = await stat(videoFilePath) | |
94 | ||
95 | videoFile.size = stats.size | |
96 | videoFile.fps = await getVideoStreamFPS(videoFilePath) | |
97 | videoFile.metadata = await buildFileMetadata(videoFilePath) | |
98 | ||
99 | await createTorrentAndSetInfoHash(playlist, videoFile) | |
100 | ||
101 | const oldFile = await VideoFileModel.loadHLSFile({ | |
102 | playlistId: playlist.id, | |
103 | fps: videoFile.fps, | |
104 | resolution: videoFile.resolution | |
105 | }) | |
106 | ||
107 | if (oldFile) { | |
108 | await video.removeStreamingPlaylistVideoFile(playlist, oldFile) | |
109 | await oldFile.destroy() | |
110 | } | |
111 | ||
112 | const savedVideoFile = await VideoFileModel.customUpsert(videoFile, 'streaming-playlist', undefined) | |
113 | ||
114 | await updatePlaylistAfterFileChange(video, playlist) | |
115 | ||
116 | return { resolutionPlaylistPath, videoFile: savedVideoFile } | |
117 | } finally { | |
118 | mutexReleaser() | |
119 | } | |
120 | } | |
121 | ||
122 | // --------------------------------------------------------------------------- | |
123 | ||
124 | async function generateHlsPlaylistCommon (options: { | |
125 | type: 'hls' | 'hls-from-ts' | |
126 | video: MVideo | |
127 | inputPath: string | |
128 | ||
129 | resolution: VideoResolution | |
130 | fps: number | |
131 | ||
132 | inputFileMutexReleaser: MutexInterface.Releaser | |
133 | ||
134 | copyCodecs?: boolean | |
135 | isAAC?: boolean | |
136 | ||
137 | job?: Job | |
138 | }) { | |
139 | const { type, video, inputPath, resolution, fps, copyCodecs, isAAC, job, inputFileMutexReleaser } = options | |
140 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | |
141 | ||
142 | const videoTranscodedBasePath = join(transcodeDirectory, type) | |
143 | await ensureDir(videoTranscodedBasePath) | |
144 | ||
145 | const videoFilename = generateHLSVideoFilename(resolution) | |
146 | const videoOutputPath = join(videoTranscodedBasePath, videoFilename) | |
147 | ||
148 | const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename) | |
149 | const m3u8OutputPath = join(videoTranscodedBasePath, resolutionPlaylistFilename) | |
150 | ||
151 | const transcodeOptions = { | |
152 | type, | |
153 | ||
154 | inputPath, | |
155 | outputPath: m3u8OutputPath, | |
156 | ||
157 | resolution, | |
158 | fps, | |
159 | copyCodecs, | |
160 | ||
161 | isAAC, | |
162 | ||
163 | inputFileMutexReleaser, | |
164 | ||
165 | hlsPlaylist: { | |
166 | videoFilename | |
167 | } | |
168 | } | |
169 | ||
170 | await buildFFmpegVOD(job).transcode(transcodeOptions) | |
171 | ||
172 | const newVideoFile = new VideoFileModel({ | |
173 | resolution, | |
174 | extname: extnameUtil(videoFilename), | |
175 | size: 0, | |
176 | filename: videoFilename, | |
177 | fps: -1 | |
178 | }) | |
179 | ||
180 | await onHLSVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath, m3u8OutputPath }) | |
181 | } |