mirror of https://github.com/Chocobozzz/PeerTube
Fix audio issues with live replay
parent
49bcdb0d66
commit
3851e732c4
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 }))
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue