Fix replay saving
This commit is contained in:
parent
da0310f821
commit
31c82cd914
|
@ -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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue