aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/helpers/ffmpeg-utils.ts32
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts38
-rw-r--r--server/models/account/user.ts4
-rw-r--r--server/models/video/video.ts10
5 files changed, 69 insertions, 19 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 2f167a580..b063cedcb 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -8,6 +8,7 @@ import { CONFIG } from '../initializers/config'
8import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' 8import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
9import { processImage } from './image-utils' 9import { processImage } from './image-utils'
10import { logger } from './logger' 10import { logger } from './logger'
11import { concat } from 'lodash'
11 12
12/** 13/**
13 * A toolbox to play with audio 14 * A toolbox to play with audio
@@ -424,17 +425,40 @@ function runLiveMuxing (rtmpUrl: string, outPath: string, deleteSegments: boolea
424 return command 425 return command
425} 426}
426 427
427function hlsPlaylistToFragmentedMP4 (playlistPath: string, outputPath: string) { 428async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: string[], outputPath: string) {
428 const command = getFFmpeg(playlistPath) 429 const concatFile = 'concat.txt'
430 const concatFilePath = join(hlsDirectory, concatFile)
431 const content = segmentFiles.map(f => 'file ' + f)
432 .join('\n')
433
434 await writeFile(concatFilePath, content + '\n')
435
436 const command = getFFmpeg(concatFilePath)
437 command.inputOption('-safe 0')
438 command.inputOption('-f concat')
429 439
430 command.outputOption('-c copy') 440 command.outputOption('-c copy')
431 command.output(outputPath) 441 command.output(outputPath)
432 442
433 command.run() 443 command.run()
434 444
445 function cleaner () {
446 remove(concatFile)
447 .catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err }))
448 }
449
435 return new Promise<string>((res, rej) => { 450 return new Promise<string>((res, rej) => {
436 command.on('error', err => rej(err)) 451 command.on('error', err => {
437 command.on('end', () => res()) 452 cleaner()
453
454 rej(err)
455 })
456
457 command.on('end', () => {
458 cleaner()
459
460 res()
461 })
438 }) 462 })
439} 463}
440 464
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 065012b32..f0d614112 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -154,7 +154,7 @@ const JOB_CONCURRENCY: { [id in JobType]: number } = {
154 'videos-views': 1, 154 'videos-views': 1,
155 'activitypub-refresher': 1, 155 'activitypub-refresher': 1,
156 'video-redundancy': 1, 156 'video-redundancy': 1,
157 'video-live-ending': 1 157 'video-live-ending': 10
158} 158}
159const JOB_TTL: { [id in JobType]: number } = { 159const JOB_TTL: { [id in JobType]: number } = {
160 'activitypub-http-broadcast': 60000 * 10, // 10 minutes 160 'activitypub-http-broadcast': 60000 * 10, // 10 minutes
@@ -736,6 +736,8 @@ if (isTestInstance() === true) {
736 OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2 736 OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2
737 737
738 PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000 738 PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
739
740 VIDEO_LIVE.CLEANUP_DELAY = 10000
739} 741}
740 742
741updateWebserverUrls() 743updateWebserverUrls()
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index 1a9a36129..cd5bb1d1c 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -7,7 +7,7 @@ import { generateHlsPlaylist } from '@server/lib/video-transcoding'
7import { VideoModel } from '@server/models/video/video' 7import { VideoModel } from '@server/models/video/video'
8import { VideoLiveModel } from '@server/models/video/video-live' 8import { VideoLiveModel } from '@server/models/video/video-live'
9import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 9import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
10import { MStreamingPlaylist, MVideo } from '@server/types/models' 10import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models'
11import { VideoLiveEndingPayload, VideoState } from '@shared/models' 11import { VideoLiveEndingPayload, VideoState } from '@shared/models'
12import { logger } from '../../../helpers/logger' 12import { logger } from '../../../helpers/logger'
13 13
@@ -27,7 +27,7 @@ async function processVideoLiveEnding (job: Bull.Job) {
27 return cleanupLive(video, streamingPlaylist) 27 return cleanupLive(video, streamingPlaylist)
28 } 28 }
29 29
30 return saveLive(video, streamingPlaylist) 30 return saveLive(video, live)
31} 31}
32 32
33// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
@@ -38,33 +38,47 @@ export {
38 38
39// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
40 40
41async function saveLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { 41async function saveLive (video: MVideo, live: MVideoLive) {
42 const videoFiles = await streamingPlaylist.get('VideoFiles')
43 const hlsDirectory = getHLSDirectory(video, false) 42 const hlsDirectory = getHLSDirectory(video, false)
43 const files = await readdir(hlsDirectory)
44
45 const playlistFiles = files.filter(f => f.endsWith('.m3u8') && f !== 'master.m3u8')
46 const resolutions: number[] = []
47
48 for (const playlistFile of playlistFiles) {
49 const playlistPath = join(hlsDirectory, playlistFile)
50 const { videoFileResolution } = await getVideoFileResolution(playlistPath)
44 51
45 for (const videoFile of videoFiles) { 52 const mp4TmpName = buildMP4TmpName(videoFileResolution)
46 const playlistPath = join(hlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(videoFile.resolution))
47 53
48 const mp4TmpName = buildMP4TmpName(videoFile.resolution) 54 // Playlist name is for example 3.m3u8
49 await hlsPlaylistToFragmentedMP4(playlistPath, mp4TmpName) 55 // Segments names are 3-0.ts 3-1.ts etc
56 const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-'
57
58 const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts'))
59 await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpName)
60
61 resolutions.push(videoFileResolution)
50 } 62 }
51 63
52 await cleanupLiveFiles(hlsDirectory) 64 await cleanupLiveFiles(hlsDirectory)
53 65
66 await live.destroy()
67
54 video.isLive = false 68 video.isLive = false
55 video.state = VideoState.TO_TRANSCODE 69 video.state = VideoState.TO_TRANSCODE
56 await video.save() 70 await video.save()
57 71
58 const videoWithFiles = await VideoModel.loadWithFiles(video.id) 72 const videoWithFiles = await VideoModel.loadWithFiles(video.id)
59 73
60 for (const videoFile of videoFiles) { 74 for (const resolution of resolutions) {
61 const videoInputPath = buildMP4TmpName(videoFile.resolution) 75 const videoInputPath = buildMP4TmpName(resolution)
62 const { isPortraitMode } = await getVideoFileResolution(videoInputPath) 76 const { isPortraitMode } = await getVideoFileResolution(videoInputPath)
63 77
64 await generateHlsPlaylist({ 78 await generateHlsPlaylist({
65 video: videoWithFiles, 79 video: videoWithFiles,
66 videoInputPath, 80 videoInputPath,
67 resolution: videoFile.resolution, 81 resolution: resolution,
68 copyCodecs: true, 82 copyCodecs: true,
69 isPortraitMode 83 isPortraitMode
70 }) 84 })
@@ -103,5 +117,5 @@ async function cleanupLiveFiles (hlsDirectory: string) {
103} 117}
104 118
105function buildMP4TmpName (resolution: number) { 119function buildMP4TmpName (resolution: number) {
106 return resolution + 'tmp.mp4' 120 return resolution + '-tmp.mp4'
107} 121}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index e850d1e6d..f64568c54 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -710,7 +710,7 @@ export class UserModel extends Model<UserModel> {
710 required: true, 710 required: true,
711 include: [ 711 include: [
712 { 712 {
713 attributes: [ 'id', 'videoId' ], 713 attributes: [],
714 model: VideoLiveModel.unscoped(), 714 model: VideoLiveModel.unscoped(),
715 required: true, 715 required: true,
716 where: { 716 where: {
@@ -726,7 +726,7 @@ export class UserModel extends Model<UserModel> {
726 ] 726 ]
727 } 727 }
728 728
729 return UserModel.findOne(query) 729 return UserModel.unscoped().findOne(query)
730 } 730 }
731 731
732 static generateUserQuotaBaseSQL (options: { 732 static generateUserQuotaBaseSQL (options: {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 8493ab802..78fec5585 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -128,6 +128,7 @@ import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
128import { VideoTagModel } from './video-tag' 128import { VideoTagModel } from './video-tag'
129import { VideoViewModel } from './video-view' 129import { VideoViewModel } from './video-view'
130import { LiveManager } from '@server/lib/live-manager' 130import { LiveManager } from '@server/lib/live-manager'
131import { VideoLiveModel } from './video-live'
131 132
132export enum ScopeNames { 133export enum ScopeNames {
133 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 134 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -725,6 +726,15 @@ export class VideoModel extends Model<VideoModel> {
725 }) 726 })
726 VideoBlacklist: VideoBlacklistModel 727 VideoBlacklist: VideoBlacklistModel
727 728
729 @HasOne(() => VideoLiveModel, {
730 foreignKey: {
731 name: 'videoId',
732 allowNull: false
733 },
734 onDelete: 'cascade'
735 })
736 VideoLive: VideoLiveModel
737
728 @HasOne(() => VideoImportModel, { 738 @HasOne(() => VideoImportModel, {
729 foreignKey: { 739 foreignKey: {
730 name: 'videoId', 740 name: 'videoId',