aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/helpers/ffmpeg-utils.ts9
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts18
-rw-r--r--server/lib/live-manager.ts48
-rw-r--r--server/lib/video-transcoding.ts42
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
455async function buildHLSVODFromTSCommand (command: ffmpeg.FfmpegCommand, options: HLSFromTSTranscodeOptions) { 455async 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
2import * as chokidar from 'chokidar' 2import * as chokidar from 'chokidar'
3import { FfmpegCommand } from 'fluent-ffmpeg' 3import { FfmpegCommand } from 'fluent-ffmpeg'
4import { copy, ensureDir, stat } from 'fs-extra' 4import { appendFile, copy, ensureDir, readFile, stat } from 'fs-extra'
5import { basename, join } from 'path' 5import { basename, join } from 'path'
6import { isTestInstance } from '@server/helpers/core-utils' 6import { isTestInstance } from '@server/helpers/core-utils'
7import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' 7import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils'
@@ -24,6 +24,7 @@ import { PeerTubeSocket } from './peertube-socket'
24import { isAbleToUploadVideo } from './user' 24import { isAbleToUploadVideo } from './user'
25import { getHLSDirectory } from './video-paths' 25import { getHLSDirectory } from './video-paths'
26import { availableEncoders } from './video-transcoding-profiles' 26import { availableEncoders } from './video-transcoding-profiles'
27import * as Bluebird from 'bluebird'
27 28
28import memoizee = require('memoizee') 29import 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 @@
1import { copyFile, ensureDir, move, remove, stat, writeFile } from 'fs-extra' 1import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
2import { basename, extname as extnameUtil, join } from 'path' 2import { basename, extname as extnameUtil, join } from 'path'
3import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 3import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
4import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' 4import { 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
167async function generateHlsPlaylistFromTS (options: { 167async 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