aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-04-21 09:06:52 +0200
committerChocobozzz <me@florianbigard.com>2022-04-21 11:47:57 +0200
commit4ec52d04dcc5d664612331f8e08d7d90da990415 (patch)
tree4b193f9f8f210caaf2dbe05ef3e37fa3a6fc28f0 /server/lib
parent2024a3b9338d667640aa115da6071ea83d088c50 (diff)
downloadPeerTube-4ec52d04dcc5d664612331f8e08d7d90da990415.tar.gz
PeerTube-4ec52d04dcc5d664612331f8e08d7d90da990415.tar.zst
PeerTube-4ec52d04dcc5d664612331f8e08d7d90da990415.zip
Add ability to save replay of permanent lives
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts159
-rw-r--r--server/lib/live/live-manager.ts69
-rw-r--r--server/lib/live/live-utils.ts4
-rw-r--r--server/lib/live/shared/muxing-session.ts33
-rw-r--r--server/lib/paths.ts7
-rw-r--r--server/lib/transcoding/transcoding.ts10
-rw-r--r--server/lib/video-blacklist.ts3
7 files changed, 190 insertions, 95 deletions
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index f4de4b47c..1e290338c 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -1,25 +1,33 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { pathExists, readdir, remove } from 'fs-extra' 2import { pathExists, readdir, remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { ffprobePromise, getAudioStream, getVideoStreamDuration, getVideoStreamDimensionsInfo } from '@server/helpers/ffmpeg' 4import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamDuration } from '@server/helpers/ffmpeg'
5import { VIDEO_LIVE } from '@server/initializers/constants' 5import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
6import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live' 6import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
7import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveDirectory } from '@server/lib/paths' 7import { cleanupLive, LiveSegmentShaStore } from '@server/lib/live'
8import {
9 generateHLSMasterPlaylistFilename,
10 generateHlsSha256SegmentsFilename,
11 getLiveDirectory,
12 getLiveReplayBaseDirectory
13} from '@server/lib/paths'
8import { generateVideoMiniature } from '@server/lib/thumbnail' 14import { generateVideoMiniature } from '@server/lib/thumbnail'
9import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding' 15import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding'
10import { VideoPathManager } from '@server/lib/video-path-manager'
11import { moveToNextState } from '@server/lib/video-state' 16import { moveToNextState } from '@server/lib/video-state'
12import { VideoModel } from '@server/models/video/video' 17import { VideoModel } from '@server/models/video/video'
13import { VideoFileModel } from '@server/models/video/video-file' 18import { VideoFileModel } from '@server/models/video/video-file'
14import { VideoLiveModel } from '@server/models/video/video-live' 19import { VideoLiveModel } from '@server/models/video/video-live'
15import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 20import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
16import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' 21import { MVideo, MVideoLive, MVideoWithAllFiles } from '@server/types/models'
17import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' 22import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
18import { logger } from '../../../helpers/logger' 23import { logger } from '../../../helpers/logger'
24import { VideoBlacklistModel } from '@server/models/video/video-blacklist'
19 25
20async function processVideoLiveEnding (job: Job) { 26async function processVideoLiveEnding (job: Job) {
21 const payload = job.data as VideoLiveEndingPayload 27 const payload = job.data as VideoLiveEndingPayload
22 28
29 logger.info('Processing video live ending for %s.', payload.videoId, { payload })
30
23 function logError () { 31 function logError () {
24 logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) 32 logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId)
25 } 33 }
@@ -32,19 +40,19 @@ async function processVideoLiveEnding (job: Job) {
32 return 40 return
33 } 41 }
34 42
35 const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
36 if (!streamingPlaylist) {
37 logError()
38 return
39 }
40
41 LiveSegmentShaStore.Instance.cleanupShaSegments(video.uuid) 43 LiveSegmentShaStore.Instance.cleanupShaSegments(video.uuid)
42 44
43 if (live.saveReplay !== true) { 45 if (live.saveReplay !== true) {
44 return cleanupLive(video, streamingPlaylist) 46 return cleanupLiveAndFederate(video)
45 } 47 }
46 48
47 return saveLive(video, live, streamingPlaylist) 49 if (live.permanentLive) {
50 await saveReplayToExternalVideo(video, payload.publishedAt, payload.replayDirectory)
51
52 return cleanupLiveAndFederate(video)
53 }
54
55 return replaceLiveByReplay(video, live, payload.replayDirectory)
48} 56}
49 57
50// --------------------------------------------------------------------------- 58// ---------------------------------------------------------------------------
@@ -55,22 +63,66 @@ export {
55 63
56// --------------------------------------------------------------------------- 64// ---------------------------------------------------------------------------
57 65
58async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MStreamingPlaylist) { 66async function saveReplayToExternalVideo (liveVideo: MVideo, publishedAt: string, replayDirectory: string) {
59 const replayDirectory = VideoPathManager.Instance.getFSHLSOutputPath(video, VIDEO_LIVE.REPLAY_DIRECTORY) 67 await cleanupTMPLiveFiles(getLiveDirectory(liveVideo))
68
69 const video = new VideoModel({
70 name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`,
71 isLive: false,
72 state: VideoState.TO_TRANSCODE,
73 duration: 0,
74
75 remote: liveVideo.remote,
76 category: liveVideo.category,
77 licence: liveVideo.licence,
78 language: liveVideo.language,
79 commentsEnabled: liveVideo.commentsEnabled,
80 downloadEnabled: liveVideo.downloadEnabled,
81 waitTranscoding: liveVideo.waitTranscoding,
82 nsfw: liveVideo.nsfw,
83 description: liveVideo.description,
84 support: liveVideo.support,
85 privacy: liveVideo.privacy,
86 channelId: liveVideo.channelId
87 }) as MVideoWithAllFiles
88
89 video.Thumbnails = []
90 video.VideoFiles = []
91 video.VideoStreamingPlaylists = []
92
93 video.url = getLocalVideoActivityPubUrl(video)
94
95 await video.save()
96
97 // If live is blacklisted, also blacklist the replay
98 const blacklist = await VideoBlacklistModel.loadByVideoId(liveVideo.id)
99 if (blacklist) {
100 await VideoBlacklistModel.create({
101 videoId: video.id,
102 unfederated: blacklist.unfederated,
103 reason: blacklist.reason,
104 type: blacklist.type
105 })
106 }
107
108 await assignReplaysToVideo(video, replayDirectory)
60 109
61 const rootFiles = await readdir(getLiveDirectory(video)) 110 await remove(replayDirectory)
111
112 for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) {
113 const image = await generateVideoMiniature({ video, videoFile: video.getMaxQualityFile(), type })
114 await video.addAndSaveThumbnail(image)
115 }
62 116
63 const playlistFiles = rootFiles.filter(file => { 117 await moveToNextState({ video, isNewVideo: true })
64 return file.endsWith('.m3u8') && file !== streamingPlaylist.playlistFilename 118}
65 })
66 119
120async function replaceLiveByReplay (video: MVideo, live: MVideoLive, replayDirectory: string) {
67 await cleanupTMPLiveFiles(getLiveDirectory(video)) 121 await cleanupTMPLiveFiles(getLiveDirectory(video))
68 122
69 await live.destroy() 123 await live.destroy()
70 124
71 video.isLive = false 125 video.isLive = false
72 // Reinit views
73 video.views = 0
74 video.state = VideoState.TO_TRANSCODE 126 video.state = VideoState.TO_TRANSCODE
75 127
76 await video.save() 128 await video.save()
@@ -87,10 +139,38 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
87 hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename() 139 hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename()
88 await hlsPlaylist.save() 140 await hlsPlaylist.save()
89 141
142 await assignReplaysToVideo(videoWithFiles, replayDirectory)
143
144 await remove(getLiveReplayBaseDirectory(videoWithFiles))
145
146 // Regenerate the thumbnail & preview?
147 if (videoWithFiles.getMiniature().automaticallyGenerated === true) {
148 const miniature = await generateVideoMiniature({
149 video: videoWithFiles,
150 videoFile: videoWithFiles.getMaxQualityFile(),
151 type: ThumbnailType.MINIATURE
152 })
153 await video.addAndSaveThumbnail(miniature)
154 }
155
156 if (videoWithFiles.getPreview().automaticallyGenerated === true) {
157 const preview = await generateVideoMiniature({
158 video: videoWithFiles,
159 videoFile: videoWithFiles.getMaxQualityFile(),
160 type: ThumbnailType.PREVIEW
161 })
162 await video.addAndSaveThumbnail(preview)
163 }
164
165 await moveToNextState({ video: videoWithFiles, isNewVideo: false })
166}
167
168async function assignReplaysToVideo (video: MVideo, replayDirectory: string) {
90 let durationDone = false 169 let durationDone = false
91 170
92 for (const playlistFile of playlistFiles) { 171 const concatenatedTsFiles = await readdir(replayDirectory)
93 const concatenatedTsFile = buildConcatenatedName(playlistFile) 172
173 for (const concatenatedTsFile of concatenatedTsFiles) {
94 const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) 174 const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile)
95 175
96 const probe = await ffprobePromise(concatenatedTsFilePath) 176 const probe = await ffprobePromise(concatenatedTsFilePath)
@@ -99,7 +179,7 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
99 const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) 179 const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe)
100 180
101 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ 181 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({
102 video: videoWithFiles, 182 video,
103 concatenatedTsFilePath, 183 concatenatedTsFilePath,
104 resolution, 184 resolution,
105 isPortraitMode, 185 isPortraitMode,
@@ -107,33 +187,22 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
107 }) 187 })
108 188
109 if (!durationDone) { 189 if (!durationDone) {
110 videoWithFiles.duration = await getVideoStreamDuration(outputPath) 190 video.duration = await getVideoStreamDuration(outputPath)
111 await videoWithFiles.save() 191 await video.save()
112 192
113 durationDone = true 193 durationDone = true
114 } 194 }
115 } 195 }
116 196
117 await remove(replayDirectory) 197 return video
118 198}
119 // Regenerate the thumbnail & preview?
120 if (videoWithFiles.getMiniature().automaticallyGenerated === true) {
121 await generateVideoMiniature({
122 video: videoWithFiles,
123 videoFile: videoWithFiles.getMaxQualityFile(),
124 type: ThumbnailType.MINIATURE
125 })
126 }
127 199
128 if (videoWithFiles.getPreview().automaticallyGenerated === true) { 200async function cleanupLiveAndFederate (video: MVideo) {
129 await generateVideoMiniature({ 201 const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
130 video: videoWithFiles, 202 await cleanupLive(video, streamingPlaylist)
131 videoFile: videoWithFiles.getMaxQualityFile(),
132 type: ThumbnailType.PREVIEW
133 })
134 }
135 203
136 await moveToNextState({ video: videoWithFiles, isNewVideo: false }) 204 const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
205 return federateVideoIfNeeded(fullVideo, false, undefined)
137} 206}
138 207
139async function cleanupTMPLiveFiles (hlsDirectory: string) { 208async function cleanupTMPLiveFiles (hlsDirectory: string) {
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts
index 920d3a5ec..5ffe41ee3 100644
--- a/server/lib/live/live-manager.ts
+++ b/server/lib/live/live-manager.ts
@@ -1,6 +1,7 @@
1 1
2import { readFile } from 'fs-extra' 2import { readdir, readFile } from 'fs-extra'
3import { createServer, Server } from 'net' 3import { createServer, Server } from 'net'
4import { join } from 'path'
4import { createServer as createServerTLS, Server as ServerTLS } from 'tls' 5import { createServer as createServerTLS, Server as ServerTLS } from 'tls'
5import { 6import {
6 computeLowerResolutionsToTranscode, 7 computeLowerResolutionsToTranscode,
@@ -18,10 +19,11 @@ import { VideoModel } from '@server/models/video/video'
18import { VideoLiveModel } from '@server/models/video/video-live' 19import { VideoLiveModel } from '@server/models/video/video-live'
19import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 20import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
20import { MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models' 21import { MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models'
22import { wait } from '@shared/core-utils'
21import { VideoState, VideoStreamingPlaylistType } from '@shared/models' 23import { VideoState, VideoStreamingPlaylistType } from '@shared/models'
22import { federateVideoIfNeeded } from '../activitypub/videos' 24import { federateVideoIfNeeded } from '../activitypub/videos'
23import { JobQueue } from '../job-queue' 25import { JobQueue } from '../job-queue'
24import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../paths' 26import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '../paths'
25import { PeerTubeSocket } from '../peertube-socket' 27import { PeerTubeSocket } from '../peertube-socket'
26import { LiveQuotaStore } from './live-quota-store' 28import { LiveQuotaStore } from './live-quota-store'
27import { LiveSegmentShaStore } from './live-segment-sha-store' 29import { LiveSegmentShaStore } from './live-segment-sha-store'
@@ -322,7 +324,7 @@ class LiveManager {
322 324
323 muxingSession.destroy() 325 muxingSession.destroy()
324 326
325 return this.onAfterMuxingCleanup(videoId) 327 return this.onAfterMuxingCleanup({ videoId })
326 .catch(err => logger.error('Error in end transmuxing.', { err, ...localLTags })) 328 .catch(err => logger.error('Error in end transmuxing.', { err, ...localLTags }))
327 }) 329 })
328 330
@@ -349,12 +351,15 @@ class LiveManager {
349 351
350 live.Video = video 352 live.Video = video
351 353
352 setTimeout(() => { 354 await wait(getLiveSegmentTime(live.latencyMode) * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION)
353 federateVideoIfNeeded(video, false)
354 .catch(err => logger.error('Cannot federate live video %s.', video.url, { err, ...localLTags }))
355 355
356 PeerTubeSocket.Instance.sendVideoLiveNewState(video) 356 try {
357 }, getLiveSegmentTime(live.latencyMode) * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION) 357 await federateVideoIfNeeded(video, false)
358 } catch (err) {
359 logger.error('Cannot federate live video %s.', video.url, { err, ...localLTags })
360 }
361
362 PeerTubeSocket.Instance.sendVideoLiveNewState(video)
358 } catch (err) { 363 } catch (err) {
359 logger.error('Cannot save/federate live video %d.', videoId, { err, ...localLTags }) 364 logger.error('Cannot save/federate live video %d.', videoId, { err, ...localLTags })
360 } 365 }
@@ -364,25 +369,32 @@ class LiveManager {
364 this.videoSessions.delete(videoId) 369 this.videoSessions.delete(videoId)
365 } 370 }
366 371
367 private async onAfterMuxingCleanup (videoUUID: string, cleanupNow = false) { 372 private async onAfterMuxingCleanup (options: {
373 videoId: number | string
374 cleanupNow?: boolean // Default false
375 }) {
376 const { videoId, cleanupNow = false } = options
377
368 try { 378 try {
369 const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoUUID) 379 const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
370 if (!fullVideo) return 380 if (!fullVideo) return
371 381
372 const live = await VideoLiveModel.loadByVideoId(fullVideo.id) 382 const live = await VideoLiveModel.loadByVideoId(fullVideo.id)
373 383
374 if (!live.permanentLive) { 384 JobQueue.Instance.createJob({
375 JobQueue.Instance.createJob({ 385 type: 'video-live-ending',
376 type: 'video-live-ending', 386 payload: {
377 payload: { 387 videoId: fullVideo.id,
378 videoId: fullVideo.id 388 replayDirectory: live.saveReplay
379 } 389 ? await this.findReplayDirectory(fullVideo)
380 }, { delay: cleanupNow ? 0 : VIDEO_LIVE.CLEANUP_DELAY }) 390 : undefined,
381 391 publishedAt: fullVideo.publishedAt.toISOString()
382 fullVideo.state = VideoState.LIVE_ENDED 392 }
383 } else { 393 }, { delay: cleanupNow ? 0 : VIDEO_LIVE.CLEANUP_DELAY })
384 fullVideo.state = VideoState.WAITING_FOR_LIVE 394
385 } 395 fullVideo.state = live.permanentLive
396 ? VideoState.WAITING_FOR_LIVE
397 : VideoState.LIVE_ENDED
386 398
387 await fullVideo.save() 399 await fullVideo.save()
388 400
@@ -390,7 +402,7 @@ class LiveManager {
390 402
391 await federateVideoIfNeeded(fullVideo, false) 403 await federateVideoIfNeeded(fullVideo, false)
392 } catch (err) { 404 } catch (err) {
393 logger.error('Cannot save/federate new video state of live streaming of video %d.', videoUUID, { err, ...lTags(videoUUID) }) 405 logger.error('Cannot save/federate new video state of live streaming of video %d.', videoId, { err, ...lTags(videoId + '') })
394 } 406 }
395 } 407 }
396 408
@@ -398,10 +410,19 @@ class LiveManager {
398 const videoUUIDs = await VideoModel.listPublishedLiveUUIDs() 410 const videoUUIDs = await VideoModel.listPublishedLiveUUIDs()
399 411
400 for (const uuid of videoUUIDs) { 412 for (const uuid of videoUUIDs) {
401 await this.onAfterMuxingCleanup(uuid, true) 413 await this.onAfterMuxingCleanup({ videoId: uuid, cleanupNow: true })
402 } 414 }
403 } 415 }
404 416
417 private async findReplayDirectory (video: MVideo) {
418 const directory = getLiveReplayBaseDirectory(video)
419 const files = await readdir(directory)
420
421 if (files.length === 0) return undefined
422
423 return join(directory, files.sort().reverse()[0])
424 }
425
405 private buildAllResolutionsToTranscode (originResolution: number) { 426 private buildAllResolutionsToTranscode (originResolution: number) {
406 const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED 427 const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED
407 ? computeLowerResolutionsToTranscode(originResolution, 'live') 428 ? computeLowerResolutionsToTranscode(originResolution, 'live')
diff --git a/server/lib/live/live-utils.ts b/server/lib/live/live-utils.ts
index 3bf723b98..46c7fd2f8 100644
--- a/server/lib/live/live-utils.ts
+++ b/server/lib/live/live-utils.ts
@@ -9,12 +9,12 @@ function buildConcatenatedName (segmentOrPlaylistPath: string) {
9 return 'concat-' + num[1] + '.ts' 9 return 'concat-' + num[1] + '.ts'
10} 10}
11 11
12async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { 12async function cleanupLive (video: MVideo, streamingPlaylist?: MStreamingPlaylist) {
13 const hlsDirectory = getLiveDirectory(video) 13 const hlsDirectory = getLiveDirectory(video)
14 14
15 await remove(hlsDirectory) 15 await remove(hlsDirectory)
16 16
17 await streamingPlaylist.destroy() 17 if (streamingPlaylist) await streamingPlaylist.destroy()
18} 18}
19 19
20export { 20export {
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts
index a703f5b5f..588ee8749 100644
--- a/server/lib/live/shared/muxing-session.ts
+++ b/server/lib/live/shared/muxing-session.ts
@@ -11,7 +11,7 @@ import { CONFIG } from '@server/initializers/config'
11import { MEMOIZE_TTL, VIDEO_LIVE } from '@server/initializers/constants' 11import { MEMOIZE_TTL, VIDEO_LIVE } from '@server/initializers/constants'
12import { VideoFileModel } from '@server/models/video/video-file' 12import { VideoFileModel } from '@server/models/video/video-file'
13import { MStreamingPlaylistVideo, MUserId, MVideoLiveVideo } from '@server/types/models' 13import { MStreamingPlaylistVideo, MUserId, MVideoLiveVideo } from '@server/types/models'
14import { getLiveDirectory } from '../../paths' 14import { getLiveDirectory, getLiveReplayBaseDirectory } from '../../paths'
15import { VideoTranscodingProfilesManager } from '../../transcoding/default-transcoding-profiles' 15import { VideoTranscodingProfilesManager } from '../../transcoding/default-transcoding-profiles'
16import { isAbleToUploadVideo } from '../../user' 16import { isAbleToUploadVideo } from '../../user'
17import { LiveQuotaStore } from '../live-quota-store' 17import { LiveQuotaStore } from '../live-quota-store'
@@ -63,6 +63,9 @@ class MuxingSession extends EventEmitter {
63 private readonly videoUUID: string 63 private readonly videoUUID: string
64 private readonly saveReplay: boolean 64 private readonly saveReplay: boolean
65 65
66 private readonly outDirectory: string
67 private readonly replayDirectory: string
68
66 private readonly lTags: LoggerTagsFn 69 private readonly lTags: LoggerTagsFn
67 70
68 private segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {} 71 private segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {}
@@ -110,19 +113,22 @@ class MuxingSession extends EventEmitter {
110 113
111 this.saveReplay = this.videoLive.saveReplay 114 this.saveReplay = this.videoLive.saveReplay
112 115
116 this.outDirectory = getLiveDirectory(this.videoLive.Video)
117 this.replayDirectory = join(getLiveReplayBaseDirectory(this.videoLive.Video), new Date().toISOString())
118
113 this.lTags = loggerTagsFactory('live', this.sessionId, this.videoUUID) 119 this.lTags = loggerTagsFactory('live', this.sessionId, this.videoUUID)
114 } 120 }
115 121
116 async runMuxing () { 122 async runMuxing () {
117 this.createFiles() 123 this.createFiles()
118 124
119 const outPath = await this.prepareDirectories() 125 await this.prepareDirectories()
120 126
121 this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED 127 this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED
122 ? await getLiveTranscodingCommand({ 128 ? await getLiveTranscodingCommand({
123 inputUrl: this.inputUrl, 129 inputUrl: this.inputUrl,
124 130
125 outPath, 131 outPath: this.outDirectory,
126 masterPlaylistName: this.streamingPlaylist.playlistFilename, 132 masterPlaylistName: this.streamingPlaylist.playlistFilename,
127 133
128 latencyMode: this.videoLive.latencyMode, 134 latencyMode: this.videoLive.latencyMode,
@@ -137,15 +143,15 @@ class MuxingSession extends EventEmitter {
137 }) 143 })
138 : getLiveMuxingCommand({ 144 : getLiveMuxingCommand({
139 inputUrl: this.inputUrl, 145 inputUrl: this.inputUrl,
140 outPath, 146 outPath: this.outDirectory,
141 masterPlaylistName: this.streamingPlaylist.playlistFilename, 147 masterPlaylistName: this.streamingPlaylist.playlistFilename,
142 latencyMode: this.videoLive.latencyMode 148 latencyMode: this.videoLive.latencyMode
143 }) 149 })
144 150
145 logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags()) 151 logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags())
146 152
147 this.watchTSFiles(outPath) 153 this.watchTSFiles(this.outDirectory)
148 this.watchMasterFile(outPath) 154 this.watchMasterFile(this.outDirectory)
149 155
150 let ffmpegShellCommand: string 156 let ffmpegShellCommand: string
151 this.ffmpegCommand.on('start', cmdline => { 157 this.ffmpegCommand.on('start', cmdline => {
@@ -155,10 +161,10 @@ class MuxingSession extends EventEmitter {
155 }) 161 })
156 162
157 this.ffmpegCommand.on('error', (err, stdout, stderr) => { 163 this.ffmpegCommand.on('error', (err, stdout, stderr) => {
158 this.onFFmpegError({ err, stdout, stderr, outPath, ffmpegShellCommand }) 164 this.onFFmpegError({ err, stdout, stderr, outPath: this.outDirectory, ffmpegShellCommand })
159 }) 165 })
160 166
161 this.ffmpegCommand.on('end', () => this.onFFmpegEnded(outPath)) 167 this.ffmpegCommand.on('end', () => this.onFFmpegEnded(this.outDirectory))
162 168
163 this.ffmpegCommand.run() 169 this.ffmpegCommand.run()
164 } 170 }
@@ -304,16 +310,11 @@ class MuxingSession extends EventEmitter {
304 } 310 }
305 311
306 private async prepareDirectories () { 312 private async prepareDirectories () {
307 const outPath = getLiveDirectory(this.videoLive.Video) 313 await ensureDir(this.outDirectory)
308 await ensureDir(outPath)
309
310 const replayDirectory = join(outPath, VIDEO_LIVE.REPLAY_DIRECTORY)
311 314
312 if (this.videoLive.saveReplay === true) { 315 if (this.videoLive.saveReplay === true) {
313 await ensureDir(replayDirectory) 316 await ensureDir(this.replayDirectory)
314 } 317 }
315
316 return outPath
317 } 318 }
318 319
319 private isDurationConstraintValid (streamingStartTime: number) { 320 private isDurationConstraintValid (streamingStartTime: number) {
@@ -364,7 +365,7 @@ class MuxingSession extends EventEmitter {
364 365
365 private async addSegmentToReplay (hlsVideoPath: string, segmentPath: string) { 366 private async addSegmentToReplay (hlsVideoPath: string, segmentPath: string) {
366 const segmentName = basename(segmentPath) 367 const segmentName = basename(segmentPath)
367 const dest = join(hlsVideoPath, VIDEO_LIVE.REPLAY_DIRECTORY, buildConcatenatedName(segmentName)) 368 const dest = join(this.replayDirectory, buildConcatenatedName(segmentName))
368 369
369 try { 370 try {
370 const data = await readFile(segmentPath) 371 const data = await readFile(segmentPath)
diff --git a/server/lib/paths.ts b/server/lib/paths.ts
index 5a85bea42..b29854700 100644
--- a/server/lib/paths.ts
+++ b/server/lib/paths.ts
@@ -1,6 +1,6 @@
1import { join } from 'path' 1import { join } from 'path'
2import { CONFIG } from '@server/initializers/config' 2import { CONFIG } from '@server/initializers/config'
3import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants' 3import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, VIDEO_LIVE } from '@server/initializers/constants'
4import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' 4import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models'
5import { removeFragmentedMP4Ext } from '@shared/core-utils' 5import { removeFragmentedMP4Ext } from '@shared/core-utils'
6import { buildUUID } from '@shared/extra-utils' 6import { buildUUID } from '@shared/extra-utils'
@@ -21,6 +21,10 @@ function getLiveDirectory (video: MVideoUUID) {
21 return getHLSDirectory(video) 21 return getHLSDirectory(video)
22} 22}
23 23
24function getLiveReplayBaseDirectory (video: MVideoUUID) {
25 return join(getLiveDirectory(video), VIDEO_LIVE.REPLAY_DIRECTORY)
26}
27
24function getHLSDirectory (video: MVideoUUID) { 28function getHLSDirectory (video: MVideoUUID) {
25 return join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) 29 return join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
26} 30}
@@ -74,6 +78,7 @@ export {
74 78
75 getHLSDirectory, 79 getHLSDirectory,
76 getLiveDirectory, 80 getLiveDirectory,
81 getLiveReplayBaseDirectory,
77 getHLSRedundancyDirectory, 82 getHLSRedundancyDirectory,
78 83
79 generateHLSMasterPlaylistFilename, 84 generateHLSMasterPlaylistFilename,
diff --git a/server/lib/transcoding/transcoding.ts b/server/lib/transcoding/transcoding.ts
index d55364e25..9a15f8613 100644
--- a/server/lib/transcoding/transcoding.ts
+++ b/server/lib/transcoding/transcoding.ts
@@ -3,13 +3,13 @@ import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
3import { basename, extname as extnameUtil, join } from 'path' 3import { basename, extname as extnameUtil, join } from 'path'
4import { toEven } from '@server/helpers/core-utils' 4import { toEven } from '@server/helpers/core-utils'
5import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 5import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
6import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 6import { MStreamingPlaylistFilesVideo, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
7import { VideoResolution, VideoStorage } from '../../../shared/models/videos' 7import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
8import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 8import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
9import { 9import {
10 buildFileMetadata,
10 canDoQuickTranscode, 11 canDoQuickTranscode,
11 getVideoStreamDuration, 12 getVideoStreamDuration,
12 buildFileMetadata,
13 getVideoStreamFPS, 13 getVideoStreamFPS,
14 transcodeVOD, 14 transcodeVOD,
15 TranscodeVODOptions, 15 TranscodeVODOptions,
@@ -191,7 +191,7 @@ function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolutio
191 191
192// Concat TS segments from a live video to a fragmented mp4 HLS playlist 192// Concat TS segments from a live video to a fragmented mp4 HLS playlist
193async function generateHlsPlaylistResolutionFromTS (options: { 193async function generateHlsPlaylistResolutionFromTS (options: {
194 video: MVideoFullLight 194 video: MVideo
195 concatenatedTsFilePath: string 195 concatenatedTsFilePath: string
196 resolution: VideoResolution 196 resolution: VideoResolution
197 isPortraitMode: boolean 197 isPortraitMode: boolean
@@ -209,7 +209,7 @@ async function generateHlsPlaylistResolutionFromTS (options: {
209 209
210// Generate an HLS playlist from an input file, and update the master playlist 210// Generate an HLS playlist from an input file, and update the master playlist
211function generateHlsPlaylistResolution (options: { 211function generateHlsPlaylistResolution (options: {
212 video: MVideoFullLight 212 video: MVideo
213 videoInputPath: string 213 videoInputPath: string
214 resolution: VideoResolution 214 resolution: VideoResolution
215 copyCodecs: boolean 215 copyCodecs: boolean
@@ -265,7 +265,7 @@ async function onWebTorrentVideoFileTranscoding (
265 265
266async function generateHlsPlaylistCommon (options: { 266async function generateHlsPlaylistCommon (options: {
267 type: 'hls' | 'hls-from-ts' 267 type: 'hls' | 'hls-from-ts'
268 video: MVideoFullLight 268 video: MVideo
269 inputPath: string 269 inputPath: string
270 resolution: VideoResolution 270 resolution: VideoResolution
271 copyCodecs?: boolean 271 copyCodecs?: boolean
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
index 0984c0d7a..91f44cb11 100644
--- a/server/lib/video-blacklist.ts
+++ b/server/lib/video-blacklist.ts
@@ -73,8 +73,7 @@ async function blacklistVideo (videoInstance: MVideoAccountLight, options: Video
73 unfederated: options.unfederate === true, 73 unfederated: options.unfederate === true,
74 reason: options.reason, 74 reason: options.reason,
75 type: VideoBlacklistType.MANUAL 75 type: VideoBlacklistType.MANUAL
76 } 76 })
77 )
78 blacklist.Video = videoInstance 77 blacklist.Video = videoInstance
79 78
80 if (options.unfederate === true) { 79 if (options.unfederate === true) {