diff options
Diffstat (limited to 'server/lib')
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 67 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 131 |
2 files changed, 119 insertions, 79 deletions
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 6e1076d8f..55bee0b83 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -1,13 +1,12 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { copy, readdir, remove } from 'fs-extra' | 2 | import { copy, readdir, remove } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' | ||
5 | import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | 4 | import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' |
6 | import { VIDEO_LIVE } from '@server/initializers/constants' | 5 | import { VIDEO_LIVE } from '@server/initializers/constants' |
7 | import { generateVideoMiniature } from '@server/lib/thumbnail' | 6 | import { generateVideoMiniature } from '@server/lib/thumbnail' |
8 | import { publishAndFederateIfNeeded } from '@server/lib/video' | 7 | import { publishAndFederateIfNeeded } from '@server/lib/video' |
9 | import { getHLSDirectory } from '@server/lib/video-paths' | 8 | import { getHLSDirectory } from '@server/lib/video-paths' |
10 | import { generateHlsPlaylist } from '@server/lib/video-transcoding' | 9 | import { generateHlsPlaylistFromTS } from '@server/lib/video-transcoding' |
11 | import { VideoModel } from '@server/models/video/video' | 10 | import { VideoModel } from '@server/models/video/video' |
12 | import { VideoFileModel } from '@server/models/video/video-file' | 11 | import { VideoFileModel } from '@server/models/video/video-file' |
13 | import { VideoLiveModel } from '@server/models/video/video-live' | 12 | import { VideoLiveModel } from '@server/models/video/video-live' |
@@ -71,32 +70,6 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
71 | } | 70 | } |
72 | } | 71 | } |
73 | 72 | ||
74 | const replayFiles = await readdir(replayDirectory) | ||
75 | |||
76 | const resolutions: number[] = [] | ||
77 | let duration: number | ||
78 | |||
79 | for (const playlistFile of playlistFiles) { | ||
80 | const playlistPath = join(replayDirectory, playlistFile) | ||
81 | const { videoFileResolution } = await getVideoFileResolution(playlistPath) | ||
82 | |||
83 | // Put the final mp4 in the hls directory, and not in the replay directory | ||
84 | const mp4TmpPath = buildMP4TmpPath(hlsDirectory, videoFileResolution) | ||
85 | |||
86 | // Playlist name is for example 3.m3u8 | ||
87 | // Segments names are 3-0.ts 3-1.ts etc | ||
88 | const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' | ||
89 | |||
90 | const segmentFiles = replayFiles.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) | ||
91 | await hlsPlaylistToFragmentedMP4(replayDirectory, segmentFiles, mp4TmpPath) | ||
92 | |||
93 | if (!duration) { | ||
94 | duration = await getDurationFromVideoFile(mp4TmpPath) | ||
95 | } | ||
96 | |||
97 | resolutions.push(videoFileResolution) | ||
98 | } | ||
99 | |||
100 | await cleanupLiveFiles(hlsDirectory) | 73 | await cleanupLiveFiles(hlsDirectory) |
101 | 74 | ||
102 | await live.destroy() | 75 | await live.destroy() |
@@ -105,7 +78,6 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
105 | // Reinit views | 78 | // Reinit views |
106 | video.views = 0 | 79 | video.views = 0 |
107 | video.state = VideoState.TO_TRANSCODE | 80 | video.state = VideoState.TO_TRANSCODE |
108 | video.duration = duration | ||
109 | 81 | ||
110 | await video.save() | 82 | await video.save() |
111 | 83 | ||
@@ -116,21 +88,35 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
116 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) | 88 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) |
117 | hlsPlaylist.VideoFiles = [] | 89 | hlsPlaylist.VideoFiles = [] |
118 | 90 | ||
119 | for (const resolution of resolutions) { | 91 | const replayFiles = await readdir(replayDirectory) |
120 | const videoInputPath = buildMP4TmpPath(hlsDirectory, resolution) | 92 | let duration: number |
121 | const { isPortraitMode } = await getVideoFileResolution(videoInputPath) | ||
122 | 93 | ||
123 | await generateHlsPlaylist({ | 94 | for (const playlistFile of playlistFiles) { |
95 | const playlistPath = join(replayDirectory, playlistFile) | ||
96 | const { videoFileResolution, isPortraitMode } = await getVideoFileResolution(playlistPath) | ||
97 | |||
98 | // Playlist name is for example 3.m3u8 | ||
99 | // Segments names are 3-0.ts 3-1.ts etc | ||
100 | const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' | ||
101 | |||
102 | const segmentFiles = replayFiles.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) | ||
103 | |||
104 | const outputPath = await generateHlsPlaylistFromTS({ | ||
124 | video: videoWithFiles, | 105 | video: videoWithFiles, |
125 | videoInputPath, | 106 | replayDirectory, |
126 | resolution: resolution, | 107 | segmentFiles, |
127 | copyCodecs: true, | 108 | resolution: videoFileResolution, |
128 | isPortraitMode | 109 | isPortraitMode |
129 | }) | 110 | }) |
130 | 111 | ||
131 | await remove(videoInputPath) | 112 | if (!duration) { |
113 | videoWithFiles.duration = await getDurationFromVideoFile(outputPath) | ||
114 | await videoWithFiles.save() | ||
115 | } | ||
132 | } | 116 | } |
133 | 117 | ||
118 | await remove(replayDirectory) | ||
119 | |||
134 | // Regenerate the thumbnail & preview? | 120 | // Regenerate the thumbnail & preview? |
135 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { | 121 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { |
136 | await generateVideoMiniature(videoWithFiles, videoWithFiles.getMaxQualityFile(), ThumbnailType.MINIATURE) | 122 | await generateVideoMiniature(videoWithFiles, videoWithFiles.getMaxQualityFile(), ThumbnailType.MINIATURE) |
@@ -161,8 +147,7 @@ async function cleanupLiveFiles (hlsDirectory: string) { | |||
161 | filename.endsWith('.m3u8') || | 147 | filename.endsWith('.m3u8') || |
162 | filename.endsWith('.mpd') || | 148 | filename.endsWith('.mpd') || |
163 | filename.endsWith('.m4s') || | 149 | filename.endsWith('.m4s') || |
164 | filename.endsWith('.tmp') || | 150 | filename.endsWith('.tmp') |
165 | filename === VIDEO_LIVE.REPLAY_DIRECTORY | ||
166 | ) { | 151 | ) { |
167 | const p = join(hlsDirectory, filename) | 152 | const p = join(hlsDirectory, filename) |
168 | 153 | ||
@@ -171,7 +156,3 @@ async function cleanupLiveFiles (hlsDirectory: string) { | |||
171 | } | 156 | } |
172 | } | 157 | } |
173 | } | 158 | } |
174 | |||
175 | function buildMP4TmpPath (basePath: string, resolution: number) { | ||
176 | return join(basePath, resolution + '-tmp.mp4') | ||
177 | } | ||
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index e022f2a68..890b23a44 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' | 1 | import { copyFile, ensureDir, move, remove, stat, writeFile } from 'fs-extra' |
2 | import { basename, extname as extnameUtil, join } from 'path' | 2 | import { basename, extname as extnameUtil, join } from 'path' |
3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
4 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' | 4 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' |
@@ -163,15 +163,104 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video | |||
163 | return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 163 | return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) |
164 | } | 164 | } |
165 | 165 | ||
166 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist | ||
167 | async function generateHlsPlaylistFromTS (options: { | ||
168 | video: MVideoWithFile | ||
169 | replayDirectory: string | ||
170 | segmentFiles: string[] | ||
171 | resolution: VideoResolution | ||
172 | isPortraitMode: boolean | ||
173 | }) { | ||
174 | const concatFilePath = join(options.replayDirectory, 'concat.txt') | ||
175 | |||
176 | function cleaner () { | ||
177 | remove(concatFilePath) | ||
178 | .catch(err => logger.error('Cannot remove concat file in %s.', options.replayDirectory, { err })) | ||
179 | } | ||
180 | |||
181 | // First concat the ts files to a mp4 file | ||
182 | const content = options.segmentFiles.map(f => 'file ' + f) | ||
183 | .join('\n') | ||
184 | |||
185 | await writeFile(concatFilePath, content + '\n') | ||
186 | |||
187 | try { | ||
188 | const outputPath = await generateHlsPlaylistCommon({ | ||
189 | video: options.video, | ||
190 | resolution: options.resolution, | ||
191 | isPortraitMode: options.isPortraitMode, | ||
192 | inputPath: concatFilePath, | ||
193 | type: 'hls-from-ts' as 'hls-from-ts' | ||
194 | }) | ||
195 | |||
196 | cleaner() | ||
197 | |||
198 | return outputPath | ||
199 | } catch (err) { | ||
200 | cleaner() | ||
201 | |||
202 | throw err | ||
203 | } | ||
204 | } | ||
205 | |||
166 | // Generate an HLS playlist from an input file, and update the master playlist | 206 | // Generate an HLS playlist from an input file, and update the master playlist |
167 | async function generateHlsPlaylist (options: { | 207 | function generateHlsPlaylist (options: { |
168 | video: MVideoWithFile | 208 | video: MVideoWithFile |
169 | videoInputPath: string | 209 | videoInputPath: string |
170 | resolution: VideoResolution | 210 | resolution: VideoResolution |
171 | copyCodecs: boolean | 211 | copyCodecs: boolean |
172 | isPortraitMode: boolean | 212 | isPortraitMode: boolean |
173 | }) { | 213 | }) { |
174 | const { video, videoInputPath, resolution, copyCodecs, isPortraitMode } = options | 214 | return generateHlsPlaylistCommon({ |
215 | video: options.video, | ||
216 | resolution: options.resolution, | ||
217 | copyCodecs: options.copyCodecs, | ||
218 | isPortraitMode: options.isPortraitMode, | ||
219 | inputPath: options.videoInputPath, | ||
220 | type: 'hls' as 'hls' | ||
221 | }) | ||
222 | } | ||
223 | |||
224 | // --------------------------------------------------------------------------- | ||
225 | |||
226 | export { | ||
227 | generateHlsPlaylist, | ||
228 | generateHlsPlaylistFromTS, | ||
229 | optimizeOriginalVideofile, | ||
230 | transcodeNewResolution, | ||
231 | mergeAudioVideofile | ||
232 | } | ||
233 | |||
234 | // --------------------------------------------------------------------------- | ||
235 | |||
236 | async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) { | ||
237 | const stats = await stat(transcodingPath) | ||
238 | const fps = await getVideoFileFPS(transcodingPath) | ||
239 | const metadata = await getMetadataFromFile(transcodingPath) | ||
240 | |||
241 | await move(transcodingPath, outputPath, { overwrite: true }) | ||
242 | |||
243 | videoFile.size = stats.size | ||
244 | videoFile.fps = fps | ||
245 | videoFile.metadata = metadata | ||
246 | |||
247 | await createTorrentAndSetInfoHash(video, videoFile) | ||
248 | |||
249 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | ||
250 | video.VideoFiles = await video.$get('VideoFiles') | ||
251 | |||
252 | return video | ||
253 | } | ||
254 | |||
255 | async function generateHlsPlaylistCommon (options: { | ||
256 | type: 'hls' | 'hls-from-ts' | ||
257 | video: MVideoWithFile | ||
258 | inputPath: string | ||
259 | resolution: VideoResolution | ||
260 | copyCodecs?: boolean | ||
261 | isPortraitMode: boolean | ||
262 | }) { | ||
263 | const { type, video, inputPath, resolution, copyCodecs, isPortraitMode } = options | ||
175 | 264 | ||
176 | const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 265 | const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) |
177 | await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)) | 266 | await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)) |
@@ -180,9 +269,9 @@ async function generateHlsPlaylist (options: { | |||
180 | const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution) | 269 | const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution) |
181 | 270 | ||
182 | const transcodeOptions = { | 271 | const transcodeOptions = { |
183 | type: 'hls' as 'hls', | 272 | type, |
184 | 273 | ||
185 | inputPath: videoInputPath, | 274 | inputPath, |
186 | outputPath, | 275 | outputPath, |
187 | 276 | ||
188 | availableEncoders, | 277 | availableEncoders, |
@@ -242,35 +331,5 @@ async function generateHlsPlaylist (options: { | |||
242 | await updateMasterHLSPlaylist(video) | 331 | await updateMasterHLSPlaylist(video) |
243 | await updateSha256VODSegments(video) | 332 | await updateSha256VODSegments(video) |
244 | 333 | ||
245 | return video | 334 | return outputPath |
246 | } | ||
247 | |||
248 | // --------------------------------------------------------------------------- | ||
249 | |||
250 | export { | ||
251 | generateHlsPlaylist, | ||
252 | optimizeOriginalVideofile, | ||
253 | transcodeNewResolution, | ||
254 | mergeAudioVideofile | ||
255 | } | ||
256 | |||
257 | // --------------------------------------------------------------------------- | ||
258 | |||
259 | async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) { | ||
260 | const stats = await stat(transcodingPath) | ||
261 | const fps = await getVideoFileFPS(transcodingPath) | ||
262 | const metadata = await getMetadataFromFile(transcodingPath) | ||
263 | |||
264 | await move(transcodingPath, outputPath, { overwrite: true }) | ||
265 | |||
266 | videoFile.size = stats.size | ||
267 | videoFile.fps = fps | ||
268 | videoFile.metadata = metadata | ||
269 | |||
270 | await createTorrentAndSetInfoHash(video, videoFile) | ||
271 | |||
272 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | ||
273 | video.VideoFiles = await video.$get('VideoFiles') | ||
274 | |||
275 | return video | ||
276 | } | 335 | } |