diff options
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 9 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 18 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 48 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 42 |
4 files changed, 49 insertions, 68 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 1093cb483..9d6fe76cb 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -455,11 +455,10 @@ async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTr | |||
455 | async function buildHLSVODFromTSCommand (command: ffmpeg.FfmpegCommand, options: HLSFromTSTranscodeOptions) { | 455 | async function buildHLSVODFromTSCommand (command: ffmpeg.FfmpegCommand, options: HLSFromTSTranscodeOptions) { |
456 | const videoPath = getHLSVideoPath(options) | 456 | const videoPath = getHLSVideoPath(options) |
457 | 457 | ||
458 | command.inputOption('-safe 0') | 458 | command.outputOption('-c copy') |
459 | command.inputOption('-f concat') | 459 | // Required for example when copying an AAC stream from an MPEG-TS |
460 | 460 | // Since it's a bitstream filter, we don't need to reencode the audio | |
461 | command.outputOption('-c:v copy') | 461 | command.outputOption('-bsf:a aac_adtstoasc') |
462 | command.audioFilter('aresample=async=1:first_pts=0') | ||
463 | 462 | ||
464 | addCommonHLSVODCommandOptions(command, videoPath) | 463 | addCommonHLSVODCommandOptions(command, videoPath) |
465 | 464 | ||
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index e3c11caa2..4daf9249b 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -73,8 +73,8 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
73 | 73 | ||
74 | for (const file of rootFiles) { | 74 | for (const file of rootFiles) { |
75 | // Move remaining files in the replay directory | 75 | // Move remaining files in the replay directory |
76 | if (file.endsWith('.ts') || file.endsWith('.m3u8')) { | 76 | if (file.endsWith('.ts')) { |
77 | await copy(join(hlsDirectory, file), join(replayDirectory, file)) | 77 | await LiveManager.Instance.addSegmentToReplay(hlsDirectory, join(hlsDirectory, file)) |
78 | } | 78 | } |
79 | 79 | ||
80 | if (file.endsWith('.m3u8') && file !== 'master.m3u8') { | 80 | if (file.endsWith('.m3u8') && file !== 'master.m3u8') { |
@@ -100,23 +100,17 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
100 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) | 100 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) |
101 | hlsPlaylist.VideoFiles = [] | 101 | hlsPlaylist.VideoFiles = [] |
102 | 102 | ||
103 | const replayFiles = await readdir(replayDirectory) | ||
104 | let durationDone: boolean | 103 | let durationDone: boolean |
105 | 104 | ||
106 | for (const playlistFile of playlistFiles) { | 105 | for (const playlistFile of playlistFiles) { |
107 | const playlistPath = join(replayDirectory, playlistFile) | 106 | const concatenatedTsFile = LiveManager.Instance.buildConcatenatedName(playlistFile) |
108 | const { videoFileResolution, isPortraitMode } = await getVideoFileResolution(playlistPath) | 107 | const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) |
109 | 108 | ||
110 | // Playlist name is for example 3.m3u8 | 109 | const { videoFileResolution, isPortraitMode } = await getVideoFileResolution(concatenatedTsFilePath) |
111 | // Segments names are 3-0.ts 3-1.ts etc | ||
112 | const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' | ||
113 | |||
114 | const segmentFiles = replayFiles.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) | ||
115 | 110 | ||
116 | const outputPath = await generateHlsPlaylistFromTS({ | 111 | const outputPath = await generateHlsPlaylistFromTS({ |
117 | video: videoWithFiles, | 112 | video: videoWithFiles, |
118 | replayDirectory, | 113 | concatenatedTsFilePath, |
119 | segmentFiles, | ||
120 | resolution: videoFileResolution, | 114 | resolution: videoFileResolution, |
121 | isPortraitMode | 115 | isPortraitMode |
122 | }) | 116 | }) |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index dcf016169..c2dd116a9 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | 1 | ||
2 | import * as chokidar from 'chokidar' | 2 | import * as chokidar from 'chokidar' |
3 | import { FfmpegCommand } from 'fluent-ffmpeg' | 3 | import { FfmpegCommand } from 'fluent-ffmpeg' |
4 | import { copy, ensureDir, stat } from 'fs-extra' | 4 | import { appendFile, copy, ensureDir, readFile, stat } from 'fs-extra' |
5 | import { basename, join } from 'path' | 5 | import { basename, join } from 'path' |
6 | import { isTestInstance } from '@server/helpers/core-utils' | 6 | import { isTestInstance } from '@server/helpers/core-utils' |
7 | import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' | 7 | import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' |
@@ -24,6 +24,7 @@ import { PeerTubeSocket } from './peertube-socket' | |||
24 | import { isAbleToUploadVideo } from './user' | 24 | import { isAbleToUploadVideo } from './user' |
25 | import { getHLSDirectory } from './video-paths' | 25 | import { getHLSDirectory } from './video-paths' |
26 | import { availableEncoders } from './video-transcoding-profiles' | 26 | import { availableEncoders } from './video-transcoding-profiles' |
27 | import * as Bluebird from 'bluebird' | ||
27 | 28 | ||
28 | import memoizee = require('memoizee') | 29 | import memoizee = require('memoizee') |
29 | 30 | ||
@@ -158,6 +159,32 @@ class LiveManager { | |||
158 | this.segmentsSha256.delete(videoUUID) | 159 | this.segmentsSha256.delete(videoUUID) |
159 | } | 160 | } |
160 | 161 | ||
162 | addSegmentToReplay (hlsVideoPath: string, segmentPath: string) { | ||
163 | const segmentName = basename(segmentPath) | ||
164 | const dest = join(hlsVideoPath, VIDEO_LIVE.REPLAY_DIRECTORY, this.buildConcatenatedName(segmentName)) | ||
165 | |||
166 | return readFile(segmentPath) | ||
167 | .then(data => appendFile(dest, data)) | ||
168 | .catch(err => logger.error('Cannot copy segment %s to repay directory.', segmentPath, { err })) | ||
169 | } | ||
170 | |||
171 | buildConcatenatedName (segmentOrPlaylistPath: string) { | ||
172 | const num = basename(segmentOrPlaylistPath).match(/^(\d+)(-|\.)/) | ||
173 | |||
174 | return 'concat-' + num[1] + '.ts' | ||
175 | } | ||
176 | |||
177 | private processSegments (hlsVideoPath: string, videoUUID: string, videoLive: MVideoLive, segmentPaths: string[]) { | ||
178 | Bluebird.mapSeries(segmentPaths, async previousSegment => { | ||
179 | // Add sha hash of previous segments, because ffmpeg should have finished generating them | ||
180 | await this.addSegmentSha(videoUUID, previousSegment) | ||
181 | |||
182 | if (videoLive.saveReplay) { | ||
183 | await this.addSegmentToReplay(hlsVideoPath, previousSegment) | ||
184 | } | ||
185 | }).catch(err => logger.error('Cannot process segments in %s', hlsVideoPath, { err })) | ||
186 | } | ||
187 | |||
161 | private getContext () { | 188 | private getContext () { |
162 | return context | 189 | return context |
163 | } | 190 | } |
@@ -302,28 +329,13 @@ class LiveManager { | |||
302 | const segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {} | 329 | const segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {} |
303 | const playlistIdMatcher = /^([\d+])-/ | 330 | const playlistIdMatcher = /^([\d+])-/ |
304 | 331 | ||
305 | const processSegments = (segmentsToProcess: string[]) => { | ||
306 | // Add sha hash of previous segments, because ffmpeg should have finished generating them | ||
307 | for (const previousSegment of segmentsToProcess) { | ||
308 | this.addSegmentSha(videoUUID, previousSegment) | ||
309 | .catch(err => logger.error('Cannot add sha segment of video %s -> %s.', videoUUID, previousSegment, { err })) | ||
310 | |||
311 | if (videoLive.saveReplay) { | ||
312 | const segmentName = basename(previousSegment) | ||
313 | |||
314 | copy(previousSegment, join(outPath, VIDEO_LIVE.REPLAY_DIRECTORY, segmentName)) | ||
315 | .catch(err => logger.error('Cannot copy segment %s to repay directory.', previousSegment, { err })) | ||
316 | } | ||
317 | } | ||
318 | } | ||
319 | |||
320 | const addHandler = segmentPath => { | 332 | const addHandler = segmentPath => { |
321 | logger.debug('Live add handler of %s.', segmentPath) | 333 | logger.debug('Live add handler of %s.', segmentPath) |
322 | 334 | ||
323 | const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] | 335 | const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] |
324 | 336 | ||
325 | const segmentsToProcess = segmentsToProcessPerPlaylist[playlistId] || [] | 337 | const segmentsToProcess = segmentsToProcessPerPlaylist[playlistId] || [] |
326 | processSegments(segmentsToProcess) | 338 | this.processSegments(outPath, videoUUID, videoLive, segmentsToProcess) |
327 | 339 | ||
328 | segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] | 340 | segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] |
329 | 341 | ||
@@ -400,7 +412,7 @@ class LiveManager { | |||
400 | .then(() => { | 412 | .then(() => { |
401 | // Process remaining segments hash | 413 | // Process remaining segments hash |
402 | for (const key of Object.keys(segmentsToProcessPerPlaylist)) { | 414 | for (const key of Object.keys(segmentsToProcessPerPlaylist)) { |
403 | processSegments(segmentsToProcessPerPlaylist[key]) | 415 | this.processSegments(outPath, videoUUID, videoLive, segmentsToProcessPerPlaylist[key]) |
404 | } | 416 | } |
405 | }) | 417 | }) |
406 | .catch(err => logger.error('Cannot close watchers of %s or process remaining hash segments.', outPath, { err })) | 418 | .catch(err => logger.error('Cannot close watchers of %s or process remaining hash segments.', outPath, { err })) |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 890b23a44..44ecf4cc9 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { copyFile, ensureDir, move, remove, stat, writeFile } from 'fs-extra' | 1 | import { copyFile, ensureDir, move, remove, stat } 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' |
@@ -166,41 +166,17 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video | |||
166 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist | 166 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist |
167 | async function generateHlsPlaylistFromTS (options: { | 167 | async function generateHlsPlaylistFromTS (options: { |
168 | video: MVideoWithFile | 168 | video: MVideoWithFile |
169 | replayDirectory: string | 169 | concatenatedTsFilePath: string |
170 | segmentFiles: string[] | ||
171 | resolution: VideoResolution | 170 | resolution: VideoResolution |
172 | isPortraitMode: boolean | 171 | isPortraitMode: boolean |
173 | }) { | 172 | }) { |
174 | const concatFilePath = join(options.replayDirectory, 'concat.txt') | 173 | return generateHlsPlaylistCommon({ |
175 | 174 | video: options.video, | |
176 | function cleaner () { | 175 | resolution: options.resolution, |
177 | remove(concatFilePath) | 176 | isPortraitMode: options.isPortraitMode, |
178 | .catch(err => logger.error('Cannot remove concat file in %s.', options.replayDirectory, { err })) | 177 | inputPath: options.concatenatedTsFilePath, |
179 | } | 178 | type: 'hls-from-ts' as 'hls-from-ts' |
180 | 179 | }) | |
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 | } | 180 | } |
205 | 181 | ||
206 | // Generate an HLS playlist from an input file, and update the master playlist | 182 | // Generate an HLS playlist from an input file, and update the master playlist |