diff options
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 32 | ||||
-rw-r--r-- | server/initializers/constants.ts | 4 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 38 | ||||
-rw-r--r-- | server/models/account/user.ts | 4 | ||||
-rw-r--r-- | server/models/video/video.ts | 10 |
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' | |||
8 | import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 8 | import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
9 | import { processImage } from './image-utils' | 9 | import { processImage } from './image-utils' |
10 | import { logger } from './logger' | 10 | import { logger } from './logger' |
11 | import { 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 | ||
427 | function hlsPlaylistToFragmentedMP4 (playlistPath: string, outputPath: string) { | 428 | async 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 | } |
159 | const JOB_TTL: { [id in JobType]: number } = { | 159 | const 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 | ||
741 | updateWebserverUrls() | 743 | updateWebserverUrls() |
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' | |||
7 | import { VideoModel } from '@server/models/video/video' | 7 | import { VideoModel } from '@server/models/video/video' |
8 | import { VideoLiveModel } from '@server/models/video/video-live' | 8 | import { VideoLiveModel } from '@server/models/video/video-live' |
9 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 9 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
10 | import { MStreamingPlaylist, MVideo } from '@server/types/models' | 10 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' |
11 | import { VideoLiveEndingPayload, VideoState } from '@shared/models' | 11 | import { VideoLiveEndingPayload, VideoState } from '@shared/models' |
12 | import { logger } from '../../../helpers/logger' | 12 | import { 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 | ||
41 | async function saveLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { | 41 | async 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 | ||
105 | function buildMP4TmpName (resolution: number) { | 119 | function 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' | |||
128 | import { VideoTagModel } from './video-tag' | 128 | import { VideoTagModel } from './video-tag' |
129 | import { VideoViewModel } from './video-view' | 129 | import { VideoViewModel } from './video-view' |
130 | import { LiveManager } from '@server/lib/live-manager' | 130 | import { LiveManager } from '@server/lib/live-manager' |
131 | import { VideoLiveModel } from './video-live' | ||
131 | 132 | ||
132 | export enum ScopeNames { | 133 | export 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', |