Fix replay saving

This commit is contained in:
Chocobozzz 2020-10-27 16:06:24 +01:00 committed by Chocobozzz
parent da0310f821
commit 31c82cd914
5 changed files with 69 additions and 19 deletions

View File

@ -8,6 +8,7 @@ import { CONFIG } from '../initializers/config'
import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
import { processImage } from './image-utils' import { processImage } from './image-utils'
import { logger } from './logger' import { logger } from './logger'
import { concat } from 'lodash'
/** /**
* A toolbox to play with audio * A toolbox to play with audio
@ -424,17 +425,40 @@ function runLiveMuxing (rtmpUrl: string, outPath: string, deleteSegments: boolea
return command return command
} }
function hlsPlaylistToFragmentedMP4 (playlistPath: string, outputPath: string) { async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: string[], outputPath: string) {
const command = getFFmpeg(playlistPath) 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.outputOption('-c copy')
command.output(outputPath) command.output(outputPath)
command.run() command.run()
function cleaner () {
remove(concatFile)
.catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err }))
}
return new Promise<string>((res, rej) => { return new Promise<string>((res, rej) => {
command.on('error', err => rej(err)) command.on('error', err => {
command.on('end', () => res()) cleaner()
rej(err)
})
command.on('end', () => {
cleaner()
res()
})
}) })
} }

View File

@ -154,7 +154,7 @@ const JOB_CONCURRENCY: { [id in JobType]: number } = {
'videos-views': 1, 'videos-views': 1,
'activitypub-refresher': 1, 'activitypub-refresher': 1,
'video-redundancy': 1, 'video-redundancy': 1,
'video-live-ending': 1 'video-live-ending': 10
} }
const JOB_TTL: { [id in JobType]: number } = { const JOB_TTL: { [id in JobType]: number } = {
'activitypub-http-broadcast': 60000 * 10, // 10 minutes 'activitypub-http-broadcast': 60000 * 10, // 10 minutes
@ -736,6 +736,8 @@ if (isTestInstance() === true) {
OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2 OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2
PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000 PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
VIDEO_LIVE.CLEANUP_DELAY = 10000
} }
updateWebserverUrls() updateWebserverUrls()

View File

@ -7,7 +7,7 @@ import { generateHlsPlaylist } from '@server/lib/video-transcoding'
import { VideoModel } from '@server/models/video/video' import { VideoModel } from '@server/models/video/video'
import { VideoLiveModel } from '@server/models/video/video-live' import { VideoLiveModel } from '@server/models/video/video-live'
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 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 { VideoLiveEndingPayload, VideoState } from '@shared/models'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
@ -27,7 +27,7 @@ async function processVideoLiveEnding (job: Bull.Job) {
return cleanupLive(video, streamingPlaylist) return cleanupLive(video, streamingPlaylist)
} }
return saveLive(video, streamingPlaylist) return saveLive(video, live)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -38,33 +38,47 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function saveLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { async function saveLive (video: MVideo, live: MVideoLive) {
const videoFiles = await streamingPlaylist.get('VideoFiles')
const hlsDirectory = getHLSDirectory(video, false) const hlsDirectory = getHLSDirectory(video, false)
const files = await readdir(hlsDirectory)
for (const videoFile of videoFiles) { const playlistFiles = files.filter(f => f.endsWith('.m3u8') && f !== 'master.m3u8')
const playlistPath = join(hlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(videoFile.resolution)) const resolutions: number[] = []
const mp4TmpName = buildMP4TmpName(videoFile.resolution) for (const playlistFile of playlistFiles) {
await hlsPlaylistToFragmentedMP4(playlistPath, mp4TmpName) const playlistPath = join(hlsDirectory, playlistFile)
const { videoFileResolution } = await getVideoFileResolution(playlistPath)
const mp4TmpName = buildMP4TmpName(videoFileResolution)
// 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 cleanupLiveFiles(hlsDirectory)
await live.destroy()
video.isLive = false video.isLive = false
video.state = VideoState.TO_TRANSCODE video.state = VideoState.TO_TRANSCODE
await video.save() await video.save()
const videoWithFiles = await VideoModel.loadWithFiles(video.id) const videoWithFiles = await VideoModel.loadWithFiles(video.id)
for (const videoFile of videoFiles) { for (const resolution of resolutions) {
const videoInputPath = buildMP4TmpName(videoFile.resolution) const videoInputPath = buildMP4TmpName(resolution)
const { isPortraitMode } = await getVideoFileResolution(videoInputPath) const { isPortraitMode } = await getVideoFileResolution(videoInputPath)
await generateHlsPlaylist({ await generateHlsPlaylist({
video: videoWithFiles, video: videoWithFiles,
videoInputPath, videoInputPath,
resolution: videoFile.resolution, resolution: resolution,
copyCodecs: true, copyCodecs: true,
isPortraitMode isPortraitMode
}) })
@ -103,5 +117,5 @@ async function cleanupLiveFiles (hlsDirectory: string) {
} }
function buildMP4TmpName (resolution: number) { function buildMP4TmpName (resolution: number) {
return resolution + 'tmp.mp4' return resolution + '-tmp.mp4'
} }

View File

@ -710,7 +710,7 @@ export class UserModel extends Model<UserModel> {
required: true, required: true,
include: [ include: [
{ {
attributes: [ 'id', 'videoId' ], attributes: [],
model: VideoLiveModel.unscoped(), model: VideoLiveModel.unscoped(),
required: true, required: true,
where: { where: {
@ -726,7 +726,7 @@ export class UserModel extends Model<UserModel> {
] ]
} }
return UserModel.findOne(query) return UserModel.unscoped().findOne(query)
} }
static generateUserQuotaBaseSQL (options: { static generateUserQuotaBaseSQL (options: {

View File

@ -128,6 +128,7 @@ import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
import { VideoTagModel } from './video-tag' import { VideoTagModel } from './video-tag'
import { VideoViewModel } from './video-view' import { VideoViewModel } from './video-view'
import { LiveManager } from '@server/lib/live-manager' import { LiveManager } from '@server/lib/live-manager'
import { VideoLiveModel } from './video-live'
export enum ScopeNames { export enum ScopeNames {
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@ -725,6 +726,15 @@ export class VideoModel extends Model<VideoModel> {
}) })
VideoBlacklist: VideoBlacklistModel VideoBlacklist: VideoBlacklistModel
@HasOne(() => VideoLiveModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
VideoLive: VideoLiveModel
@HasOne(() => VideoImportModel, { @HasOne(() => VideoImportModel, {
foreignKey: { foreignKey: {
name: 'videoId', name: 'videoId',