]>
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 { | |
0c9668f7 C |
75 | await video.reload() |
76 | ||
77 | const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, videoFile) | |
78 | await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) | |
79 | ||
80 | // Move playlist file | |
81 | const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, basename(m3u8OutputPath)) | |
82 | await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true }) | |
83 | // Move video file | |
84 | await move(videoOutputPath, videoFilePath, { overwrite: true }) | |
85 | ||
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) | |
89 | await video.save() | |
90 | } | |
91 | ||
92 | const stats = await stat(videoFilePath) | |
93 | ||
94 | videoFile.size = stats.size | |
95 | videoFile.fps = await getVideoStreamFPS(videoFilePath) | |
96 | videoFile.metadata = await buildFileMetadata(videoFilePath) | |
97 | ||
98 | await createTorrentAndSetInfoHash(playlist, videoFile) | |
99 | ||
100 | const oldFile = await VideoFileModel.loadHLSFile({ | |
101 | playlistId: playlist.id, | |
102 | fps: videoFile.fps, | |
103 | resolution: videoFile.resolution | |
104 | }) | |
105 | ||
106 | if (oldFile) { | |
107 | await video.removeStreamingPlaylistVideoFile(playlist, oldFile) | |
108 | await oldFile.destroy() | |
109 | } | |
110 | ||
111 | const savedVideoFile = await VideoFileModel.customUpsert(videoFile, 'streaming-playlist', undefined) | |
112 | ||
113 | await updatePlaylistAfterFileChange(video, playlist) | |
114 | ||
115 | return { resolutionPlaylistPath, videoFile: savedVideoFile } | |
116 | } finally { | |
117 | mutexReleaser() | |
118 | } | |
119 | } | |
120 | ||
121 | // --------------------------------------------------------------------------- | |
122 | ||
123 | async function generateHlsPlaylistCommon (options: { | |
124 | type: 'hls' | 'hls-from-ts' | |
125 | video: MVideo | |
126 | inputPath: string | |
127 | ||
128 | resolution: VideoResolution | |
129 | fps: number | |
130 | ||
131 | inputFileMutexReleaser: MutexInterface.Releaser | |
132 | ||
133 | copyCodecs?: boolean | |
134 | isAAC?: boolean | |
135 | ||
136 | job?: Job | |
137 | }) { | |
138 | const { type, video, inputPath, resolution, fps, copyCodecs, isAAC, job, inputFileMutexReleaser } = options | |
139 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | |
140 | ||
141 | const videoTranscodedBasePath = join(transcodeDirectory, type) | |
142 | await ensureDir(videoTranscodedBasePath) | |
143 | ||
144 | const videoFilename = generateHLSVideoFilename(resolution) | |
145 | const videoOutputPath = join(videoTranscodedBasePath, videoFilename) | |
146 | ||
147 | const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename) | |
148 | const m3u8OutputPath = join(videoTranscodedBasePath, resolutionPlaylistFilename) | |
149 | ||
150 | const transcodeOptions = { | |
151 | type, | |
152 | ||
153 | inputPath, | |
154 | outputPath: m3u8OutputPath, | |
155 | ||
156 | resolution, | |
157 | fps, | |
158 | copyCodecs, | |
159 | ||
160 | isAAC, | |
161 | ||
162 | inputFileMutexReleaser, | |
163 | ||
164 | hlsPlaylist: { | |
165 | videoFilename | |
166 | } | |
167 | } | |
168 | ||
169 | await buildFFmpegVOD(job).transcode(transcodeOptions) | |
170 | ||
171 | const newVideoFile = new VideoFileModel({ | |
172 | resolution, | |
173 | extname: extnameUtil(videoFilename), | |
174 | size: 0, | |
175 | filename: videoFilename, | |
176 | fps: -1 | |
177 | }) | |
178 | ||
179 | await onHLSVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath, m3u8OutputPath }) | |
180 | } |