Fix audio issues with live replay

pull/3401/head
Chocobozzz 2020-12-04 15:10:13 +01:00
parent 49bcdb0d66
commit 3851e732c4
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
4 changed files with 49 additions and 68 deletions

View File

@ -455,11 +455,10 @@ async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTr
async function buildHLSVODFromTSCommand (command: ffmpeg.FfmpegCommand, options: HLSFromTSTranscodeOptions) { async function buildHLSVODFromTSCommand (command: ffmpeg.FfmpegCommand, options: HLSFromTSTranscodeOptions) {
const videoPath = getHLSVideoPath(options) const videoPath = getHLSVideoPath(options)
command.inputOption('-safe 0') command.outputOption('-c copy')
command.inputOption('-f concat') // Required for example when copying an AAC stream from an MPEG-TS
// Since it's a bitstream filter, we don't need to reencode the audio
command.outputOption('-c:v copy') command.outputOption('-bsf:a aac_adtstoasc')
command.audioFilter('aresample=async=1:first_pts=0')
addCommonHLSVODCommandOptions(command, videoPath) addCommonHLSVODCommandOptions(command, videoPath)

View File

@ -73,8 +73,8 @@ async function saveLive (video: MVideo, live: MVideoLive) {
for (const file of rootFiles) { for (const file of rootFiles) {
// Move remaining files in the replay directory // Move remaining files in the replay directory
if (file.endsWith('.ts') || file.endsWith('.m3u8')) { if (file.endsWith('.ts')) {
await copy(join(hlsDirectory, file), join(replayDirectory, file)) await LiveManager.Instance.addSegmentToReplay(hlsDirectory, join(hlsDirectory, file))
} }
if (file.endsWith('.m3u8') && file !== 'master.m3u8') { if (file.endsWith('.m3u8') && file !== 'master.m3u8') {
@ -100,23 +100,17 @@ async function saveLive (video: MVideo, live: MVideoLive) {
await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id)
hlsPlaylist.VideoFiles = [] hlsPlaylist.VideoFiles = []
const replayFiles = await readdir(replayDirectory)
let durationDone: boolean let durationDone: boolean
for (const playlistFile of playlistFiles) { for (const playlistFile of playlistFiles) {
const playlistPath = join(replayDirectory, playlistFile) const concatenatedTsFile = LiveManager.Instance.buildConcatenatedName(playlistFile)
const { videoFileResolution, isPortraitMode } = await getVideoFileResolution(playlistPath) const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile)
// Playlist name is for example 3.m3u8 const { videoFileResolution, isPortraitMode } = await getVideoFileResolution(concatenatedTsFilePath)
// Segments names are 3-0.ts 3-1.ts etc
const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-'
const segmentFiles = replayFiles.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts'))
const outputPath = await generateHlsPlaylistFromTS({ const outputPath = await generateHlsPlaylistFromTS({
video: videoWithFiles, video: videoWithFiles,
replayDirectory, concatenatedTsFilePath,
segmentFiles,
resolution: videoFileResolution, resolution: videoFileResolution,
isPortraitMode isPortraitMode
}) })

View File

@ -1,7 +1,7 @@
import * as chokidar from 'chokidar' import * as chokidar from 'chokidar'
import { FfmpegCommand } from 'fluent-ffmpeg' import { FfmpegCommand } from 'fluent-ffmpeg'
import { copy, ensureDir, stat } from 'fs-extra' import { appendFile, copy, ensureDir, readFile, stat } from 'fs-extra'
import { basename, join } from 'path' import { basename, join } from 'path'
import { isTestInstance } from '@server/helpers/core-utils' import { isTestInstance } from '@server/helpers/core-utils'
import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils'
@ -24,6 +24,7 @@ import { PeerTubeSocket } from './peertube-socket'
import { isAbleToUploadVideo } from './user' import { isAbleToUploadVideo } from './user'
import { getHLSDirectory } from './video-paths' import { getHLSDirectory } from './video-paths'
import { availableEncoders } from './video-transcoding-profiles' import { availableEncoders } from './video-transcoding-profiles'
import * as Bluebird from 'bluebird'
import memoizee = require('memoizee') import memoizee = require('memoizee')
@ -158,6 +159,32 @@ class LiveManager {
this.segmentsSha256.delete(videoUUID) this.segmentsSha256.delete(videoUUID)
} }
addSegmentToReplay (hlsVideoPath: string, segmentPath: string) {
const segmentName = basename(segmentPath)
const dest = join(hlsVideoPath, VIDEO_LIVE.REPLAY_DIRECTORY, this.buildConcatenatedName(segmentName))
return readFile(segmentPath)
.then(data => appendFile(dest, data))
.catch(err => logger.error('Cannot copy segment %s to repay directory.', segmentPath, { err }))
}
buildConcatenatedName (segmentOrPlaylistPath: string) {
const num = basename(segmentOrPlaylistPath).match(/^(\d+)(-|\.)/)
return 'concat-' + num[1] + '.ts'
}
private processSegments (hlsVideoPath: string, videoUUID: string, videoLive: MVideoLive, segmentPaths: string[]) {
Bluebird.mapSeries(segmentPaths, async previousSegment => {
// Add sha hash of previous segments, because ffmpeg should have finished generating them
await this.addSegmentSha(videoUUID, previousSegment)
if (videoLive.saveReplay) {
await this.addSegmentToReplay(hlsVideoPath, previousSegment)
}
}).catch(err => logger.error('Cannot process segments in %s', hlsVideoPath, { err }))
}
private getContext () { private getContext () {
return context return context
} }
@ -302,28 +329,13 @@ class LiveManager {
const segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {} const segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {}
const playlistIdMatcher = /^([\d+])-/ const playlistIdMatcher = /^([\d+])-/
const processSegments = (segmentsToProcess: string[]) => {
// Add sha hash of previous segments, because ffmpeg should have finished generating them
for (const previousSegment of segmentsToProcess) {
this.addSegmentSha(videoUUID, previousSegment)
.catch(err => logger.error('Cannot add sha segment of video %s -> %s.', videoUUID, previousSegment, { err }))
if (videoLive.saveReplay) {
const segmentName = basename(previousSegment)
copy(previousSegment, join(outPath, VIDEO_LIVE.REPLAY_DIRECTORY, segmentName))
.catch(err => logger.error('Cannot copy segment %s to repay directory.', previousSegment, { err }))
}
}
}
const addHandler = segmentPath => { const addHandler = segmentPath => {
logger.debug('Live add handler of %s.', segmentPath) logger.debug('Live add handler of %s.', segmentPath)
const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] const playlistId = basename(segmentPath).match(playlistIdMatcher)[0]
const segmentsToProcess = segmentsToProcessPerPlaylist[playlistId] || [] const segmentsToProcess = segmentsToProcessPerPlaylist[playlistId] || []
processSegments(segmentsToProcess) this.processSegments(outPath, videoUUID, videoLive, segmentsToProcess)
segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ]
@ -400,7 +412,7 @@ class LiveManager {
.then(() => { .then(() => {
// Process remaining segments hash // Process remaining segments hash
for (const key of Object.keys(segmentsToProcessPerPlaylist)) { for (const key of Object.keys(segmentsToProcessPerPlaylist)) {
processSegments(segmentsToProcessPerPlaylist[key]) this.processSegments(outPath, videoUUID, videoLive, segmentsToProcessPerPlaylist[key])
} }
}) })
.catch(err => logger.error('Cannot close watchers of %s or process remaining hash segments.', outPath, { err })) .catch(err => logger.error('Cannot close watchers of %s or process remaining hash segments.', outPath, { err }))

View File

@ -1,4 +1,4 @@
import { copyFile, ensureDir, move, remove, stat, writeFile } from 'fs-extra' import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
import { basename, extname as extnameUtil, join } from 'path' import { basename, extname as extnameUtil, join } from 'path'
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models'
@ -166,41 +166,17 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
// Concat TS segments from a live video to a fragmented mp4 HLS playlist // Concat TS segments from a live video to a fragmented mp4 HLS playlist
async function generateHlsPlaylistFromTS (options: { async function generateHlsPlaylistFromTS (options: {
video: MVideoWithFile video: MVideoWithFile
replayDirectory: string concatenatedTsFilePath: string
segmentFiles: string[]
resolution: VideoResolution resolution: VideoResolution
isPortraitMode: boolean isPortraitMode: boolean
}) { }) {
const concatFilePath = join(options.replayDirectory, 'concat.txt') return generateHlsPlaylistCommon({
function cleaner () {
remove(concatFilePath)
.catch(err => logger.error('Cannot remove concat file in %s.', options.replayDirectory, { err }))
}
// First concat the ts files to a mp4 file
const content = options.segmentFiles.map(f => 'file ' + f)
.join('\n')
await writeFile(concatFilePath, content + '\n')
try {
const outputPath = await generateHlsPlaylistCommon({
video: options.video, video: options.video,
resolution: options.resolution, resolution: options.resolution,
isPortraitMode: options.isPortraitMode, isPortraitMode: options.isPortraitMode,
inputPath: concatFilePath, inputPath: options.concatenatedTsFilePath,
type: 'hls-from-ts' as 'hls-from-ts' type: 'hls-from-ts' as 'hls-from-ts'
}) })
cleaner()
return outputPath
} catch (err) {
cleaner()
throw err
}
} }
// Generate an HLS playlist from an input file, and update the master playlist // Generate an HLS playlist from an input file, and update the master playlist