diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 76 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 67 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 131 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 32 | ||||
-rw-r--r-- | server/tests/api/live/live.ts | 2 |
5 files changed, 176 insertions, 132 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 085635b5a..c6b8a0eb0 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -110,7 +110,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima | |||
110 | // Transcode meta function | 110 | // Transcode meta function |
111 | // --------------------------------------------------------------------------- | 111 | // --------------------------------------------------------------------------- |
112 | 112 | ||
113 | type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio' | 113 | type TranscodeOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio' |
114 | 114 | ||
115 | interface BaseTranscodeOptions { | 115 | interface BaseTranscodeOptions { |
116 | type: TranscodeOptionsType | 116 | type: TranscodeOptionsType |
@@ -134,6 +134,14 @@ interface HLSTranscodeOptions extends BaseTranscodeOptions { | |||
134 | } | 134 | } |
135 | } | 135 | } |
136 | 136 | ||
137 | interface HLSFromTSTranscodeOptions extends BaseTranscodeOptions { | ||
138 | type: 'hls-from-ts' | ||
139 | |||
140 | hlsPlaylist: { | ||
141 | videoFilename: string | ||
142 | } | ||
143 | } | ||
144 | |||
137 | interface QuickTranscodeOptions extends BaseTranscodeOptions { | 145 | interface QuickTranscodeOptions extends BaseTranscodeOptions { |
138 | type: 'quick-transcode' | 146 | type: 'quick-transcode' |
139 | } | 147 | } |
@@ -153,6 +161,7 @@ interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions { | |||
153 | 161 | ||
154 | type TranscodeOptions = | 162 | type TranscodeOptions = |
155 | HLSTranscodeOptions | 163 | HLSTranscodeOptions |
164 | | HLSFromTSTranscodeOptions | ||
156 | | VideoTranscodeOptions | 165 | | VideoTranscodeOptions |
157 | | MergeAudioTranscodeOptions | 166 | | MergeAudioTranscodeOptions |
158 | | OnlyAudioTranscodeOptions | 167 | | OnlyAudioTranscodeOptions |
@@ -163,6 +172,7 @@ const builders: { | |||
163 | } = { | 172 | } = { |
164 | 'quick-transcode': buildQuickTranscodeCommand, | 173 | 'quick-transcode': buildQuickTranscodeCommand, |
165 | 'hls': buildHLSVODCommand, | 174 | 'hls': buildHLSVODCommand, |
175 | 'hls-from-ts': buildHLSVODFromTSCommand, | ||
166 | 'merge-audio': buildAudioMergeCommand, | 176 | 'merge-audio': buildAudioMergeCommand, |
167 | 'only-audio': buildOnlyAudioCommand, | 177 | 'only-audio': buildOnlyAudioCommand, |
168 | 'video': buildx264VODCommand | 178 | 'video': buildx264VODCommand |
@@ -292,31 +302,6 @@ function getLiveMuxingCommand (rtmpUrl: string, outPath: string) { | |||
292 | return command | 302 | return command |
293 | } | 303 | } |
294 | 304 | ||
295 | async function hlsPlaylistToFragmentedMP4 (replayDirectory: string, segmentFiles: string[], outputPath: string) { | ||
296 | const concatFilePath = join(replayDirectory, 'concat.txt') | ||
297 | |||
298 | function cleaner () { | ||
299 | remove(concatFilePath) | ||
300 | .catch(err => logger.error('Cannot remove concat file in %s.', replayDirectory, { err })) | ||
301 | } | ||
302 | |||
303 | // First concat the ts files to a mp4 file | ||
304 | const content = segmentFiles.map(f => 'file ' + f) | ||
305 | .join('\n') | ||
306 | |||
307 | await writeFile(concatFilePath, content + '\n') | ||
308 | |||
309 | const command = getFFmpeg(concatFilePath) | ||
310 | command.inputOption('-safe 0') | ||
311 | command.inputOption('-f concat') | ||
312 | |||
313 | command.outputOption('-c:v copy') | ||
314 | command.audioFilter('aresample=async=1:first_pts=0') | ||
315 | command.output(outputPath) | ||
316 | |||
317 | return runCommand(command, cleaner) | ||
318 | } | ||
319 | |||
320 | function buildStreamSuffix (base: string, streamNum?: number) { | 305 | function buildStreamSuffix (base: string, streamNum?: number) { |
321 | if (streamNum !== undefined) { | 306 | if (streamNum !== undefined) { |
322 | return `${base}:${streamNum}` | 307 | return `${base}:${streamNum}` |
@@ -336,8 +321,7 @@ export { | |||
336 | generateImageFromVideoFile, | 321 | generateImageFromVideoFile, |
337 | TranscodeOptions, | 322 | TranscodeOptions, |
338 | TranscodeOptionsType, | 323 | TranscodeOptionsType, |
339 | transcode, | 324 | transcode |
340 | hlsPlaylistToFragmentedMP4 | ||
341 | } | 325 | } |
342 | 326 | ||
343 | // --------------------------------------------------------------------------- | 327 | // --------------------------------------------------------------------------- |
@@ -447,6 +431,16 @@ function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | |||
447 | return command | 431 | return command |
448 | } | 432 | } |
449 | 433 | ||
434 | function addCommonHLSVODCommandOptions (command: ffmpeg.FfmpegCommand, outputPath: string) { | ||
435 | return command.outputOption('-hls_time 4') | ||
436 | .outputOption('-hls_list_size 0') | ||
437 | .outputOption('-hls_playlist_type vod') | ||
438 | .outputOption('-hls_segment_filename ' + outputPath) | ||
439 | .outputOption('-hls_segment_type fmp4') | ||
440 | .outputOption('-f hls') | ||
441 | .outputOption('-hls_flags single_file') | ||
442 | } | ||
443 | |||
450 | async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { | 444 | async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { |
451 | const videoPath = getHLSVideoPath(options) | 445 | const videoPath = getHLSVideoPath(options) |
452 | 446 | ||
@@ -454,19 +448,27 @@ async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTr | |||
454 | else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command) | 448 | else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command) |
455 | else command = await buildx264VODCommand(command, options) | 449 | else command = await buildx264VODCommand(command, options) |
456 | 450 | ||
457 | command = command.outputOption('-hls_time 4') | 451 | addCommonHLSVODCommandOptions(command, videoPath) |
458 | .outputOption('-hls_list_size 0') | 452 | |
459 | .outputOption('-hls_playlist_type vod') | 453 | return command |
460 | .outputOption('-hls_segment_filename ' + videoPath) | 454 | } |
461 | .outputOption('-hls_segment_type fmp4') | 455 | |
462 | .outputOption('-f hls') | 456 | async function buildHLSVODFromTSCommand (command: ffmpeg.FfmpegCommand, options: HLSFromTSTranscodeOptions) { |
463 | .outputOption('-hls_flags single_file') | 457 | const videoPath = getHLSVideoPath(options) |
458 | |||
459 | command.inputOption('-safe 0') | ||
460 | command.inputOption('-f concat') | ||
461 | |||
462 | command.outputOption('-c:v copy') | ||
463 | command.audioFilter('aresample=async=1:first_pts=0') | ||
464 | |||
465 | addCommonHLSVODCommandOptions(command, videoPath) | ||
464 | 466 | ||
465 | return command | 467 | return command |
466 | } | 468 | } |
467 | 469 | ||
468 | async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) { | 470 | async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) { |
469 | if (options.type !== 'hls') return | 471 | if (options.type !== 'hls' && options.type !== 'hls-from-ts') return |
470 | 472 | ||
471 | const fileContent = await readFile(options.outputPath) | 473 | const fileContent = await readFile(options.outputPath) |
472 | 474 | ||
@@ -480,7 +482,7 @@ async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) { | |||
480 | await writeFile(options.outputPath, newContent) | 482 | await writeFile(options.outputPath, newContent) |
481 | } | 483 | } |
482 | 484 | ||
483 | function getHLSVideoPath (options: HLSTranscodeOptions) { | 485 | function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) { |
484 | return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` | 486 | return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` |
485 | } | 487 | } |
486 | 488 | ||
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 | } |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index b020bfa45..9f9e0b069 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,3 +1,6 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { join } from 'path' | ||
3 | import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' | ||
1 | import { | 4 | import { |
2 | AllowNull, | 5 | AllowNull, |
3 | BelongsTo, | 6 | BelongsTo, |
@@ -15,14 +18,19 @@ import { | |||
15 | Table, | 18 | Table, |
16 | UpdatedAt | 19 | UpdatedAt |
17 | } from 'sequelize-typescript' | 20 | } from 'sequelize-typescript' |
21 | import { MAccountId, MChannelId } from '@server/types/models' | ||
22 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
23 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | ||
18 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 24 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
19 | import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' | 25 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' |
26 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' | ||
27 | import { activityPubCollectionPagination } from '../../helpers/activitypub' | ||
28 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
20 | import { | 29 | import { |
21 | isVideoPlaylistDescriptionValid, | 30 | isVideoPlaylistDescriptionValid, |
22 | isVideoPlaylistNameValid, | 31 | isVideoPlaylistNameValid, |
23 | isVideoPlaylistPrivacyValid | 32 | isVideoPlaylistPrivacyValid |
24 | } from '../../helpers/custom-validators/video-playlists' | 33 | } from '../../helpers/custom-validators/video-playlists' |
25 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
26 | import { | 34 | import { |
27 | ACTIVITY_PUB, | 35 | ACTIVITY_PUB, |
28 | CONSTRAINTS_FIELDS, | 36 | CONSTRAINTS_FIELDS, |
@@ -32,18 +40,7 @@ import { | |||
32 | VIDEO_PLAYLIST_TYPES, | 40 | VIDEO_PLAYLIST_TYPES, |
33 | WEBSERVER | 41 | WEBSERVER |
34 | } from '../../initializers/constants' | 42 | } from '../../initializers/constants' |
35 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' | 43 | import { MThumbnail } from '../../types/models/video/thumbnail' |
36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | ||
37 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | ||
38 | import { join } from 'path' | ||
39 | import { VideoPlaylistElementModel } from './video-playlist-element' | ||
40 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | ||
41 | import { activityPubCollectionPagination } from '../../helpers/activitypub' | ||
42 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' | ||
43 | import { ThumbnailModel } from './thumbnail' | ||
44 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
45 | import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' | ||
46 | import * as Bluebird from 'bluebird' | ||
47 | import { | 44 | import { |
48 | MVideoPlaylistAccountThumbnail, | 45 | MVideoPlaylistAccountThumbnail, |
49 | MVideoPlaylistAP, | 46 | MVideoPlaylistAP, |
@@ -52,8 +49,11 @@ import { | |||
52 | MVideoPlaylistFullSummary, | 49 | MVideoPlaylistFullSummary, |
53 | MVideoPlaylistIdWithElements | 50 | MVideoPlaylistIdWithElements |
54 | } from '../../types/models/video/video-playlist' | 51 | } from '../../types/models/video/video-playlist' |
55 | import { MThumbnail } from '../../types/models/video/thumbnail' | 52 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
56 | import { MAccountId, MChannelId } from '@server/types/models' | 53 | import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' |
54 | import { ThumbnailModel } from './thumbnail' | ||
55 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | ||
56 | import { VideoPlaylistElementModel } from './video-playlist-element' | ||
57 | 57 | ||
58 | enum ScopeNames { | 58 | enum ScopeNames { |
59 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 59 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index d0586499b..23f8d2be1 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -430,6 +430,8 @@ describe('Test live', function () { | |||
430 | expect(video.files).to.have.lengthOf(0) | 430 | expect(video.files).to.have.lengthOf(0) |
431 | 431 | ||
432 | const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) | 432 | const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) |
433 | await makeRawRequest(hlsPlaylist.playlistUrl, 200) | ||
434 | await makeRawRequest(hlsPlaylist.segmentsSha256Url, 200) | ||
433 | 435 | ||
434 | expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length) | 436 | expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length) |
435 | 437 | ||