import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
import { processImage } from './image-utils'
import { logger } from './logger'
+import { concat } from 'lodash'
/**
* A toolbox to play with audio
return command
}
-function hlsPlaylistToFragmentedMP4 (playlistPath: string, outputPath: string) {
- const command = getFFmpeg(playlistPath)
+async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: string[], outputPath: string) {
+ const concatFile = 'concat.txt'
+ const concatFilePath = join(hlsDirectory, concatFile)
+ const content = segmentFiles.map(f => 'file ' + f)
+ .join('\n')
+
+ await writeFile(concatFilePath, content + '\n')
+
+ const command = getFFmpeg(concatFilePath)
+ command.inputOption('-safe 0')
+ command.inputOption('-f concat')
command.outputOption('-c copy')
command.output(outputPath)
command.run()
+ function cleaner () {
+ remove(concatFile)
+ .catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err }))
+ }
+
return new Promise<string>((res, rej) => {
- command.on('error', err => rej(err))
- command.on('end', () => res())
+ command.on('error', err => {
+ cleaner()
+
+ rej(err)
+ })
+
+ command.on('end', () => {
+ cleaner()
+
+ res()
+ })
})
}
import { VideoModel } from '@server/models/video/video'
import { VideoLiveModel } from '@server/models/video/video-live'
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
-import { MStreamingPlaylist, MVideo } from '@server/types/models'
+import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models'
import { VideoLiveEndingPayload, VideoState } from '@shared/models'
import { logger } from '../../../helpers/logger'
return cleanupLive(video, streamingPlaylist)
}
- return saveLive(video, streamingPlaylist)
+ return saveLive(video, live)
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-async function saveLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) {
- const videoFiles = await streamingPlaylist.get('VideoFiles')
+async function saveLive (video: MVideo, live: MVideoLive) {
const hlsDirectory = getHLSDirectory(video, false)
+ const files = await readdir(hlsDirectory)
+
+ const playlistFiles = files.filter(f => f.endsWith('.m3u8') && f !== 'master.m3u8')
+ const resolutions: number[] = []
+
+ for (const playlistFile of playlistFiles) {
+ const playlistPath = join(hlsDirectory, playlistFile)
+ const { videoFileResolution } = await getVideoFileResolution(playlistPath)
- for (const videoFile of videoFiles) {
- const playlistPath = join(hlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(videoFile.resolution))
+ const mp4TmpName = buildMP4TmpName(videoFileResolution)
- const mp4TmpName = buildMP4TmpName(videoFile.resolution)
- await hlsPlaylistToFragmentedMP4(playlistPath, mp4TmpName)
+ // Playlist name is for example 3.m3u8
+ // Segments names are 3-0.ts 3-1.ts etc
+ const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-'
+
+ const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts'))
+ await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpName)
+
+ resolutions.push(videoFileResolution)
}
await cleanupLiveFiles(hlsDirectory)
+ await live.destroy()
+
video.isLive = false
video.state = VideoState.TO_TRANSCODE
await video.save()
const videoWithFiles = await VideoModel.loadWithFiles(video.id)
- for (const videoFile of videoFiles) {
- const videoInputPath = buildMP4TmpName(videoFile.resolution)
+ for (const resolution of resolutions) {
+ const videoInputPath = buildMP4TmpName(resolution)
const { isPortraitMode } = await getVideoFileResolution(videoInputPath)
await generateHlsPlaylist({
video: videoWithFiles,
videoInputPath,
- resolution: videoFile.resolution,
+ resolution: resolution,
copyCodecs: true,
isPortraitMode
})
}
function buildMP4TmpName (resolution: number) {
- return resolution + 'tmp.mp4'
+ return resolution + '-tmp.mp4'
}