diff options
Diffstat (limited to 'server/lib/transcoding/hls-transcoding.ts')
-rw-r--r-- | server/lib/transcoding/hls-transcoding.ts | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/server/lib/transcoding/hls-transcoding.ts b/server/lib/transcoding/hls-transcoding.ts new file mode 100644 index 000000000..cffa859c7 --- /dev/null +++ b/server/lib/transcoding/hls-transcoding.ts | |||
@@ -0,0 +1,181 @@ | |||
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 | } | ||