X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Flib%2Fjob-queue%2Fhandlers%2Fvideo-live-ending.ts;h=814f313a3bb8d946905bbbce3fb1ffba571e0d28;hb=a687879e94fa5d3ecdd76bec3d94d0e1698ee913;hp=79aa547ba10709fa4aa6034bd7b637992b99f35e;hpb=5333788c08ab6152303829d4624774b5d788ff40;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 79aa547ba..814f313a3 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts @@ -1,60 +1,73 @@ -import { Job } from 'bull' +import { Job } from 'bullmq' import { readdir, remove } from 'fs-extra' import { join } from 'path' -import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamDuration } from '@server/helpers/ffmpeg' import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' -import { cleanupNormalLive, cleanupPermanentLive, cleanupTMPLiveFiles, LiveSegmentShaStore } from '@server/lib/live' -import { - generateHLSMasterPlaylistFilename, - generateHlsSha256SegmentsFilename, - getLiveDirectory, - getLiveReplayBaseDirectory -} from '@server/lib/paths' +import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live' +import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '@server/lib/paths' import { generateVideoMiniature } from '@server/lib/thumbnail' -import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding' +import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding' +import { VideoPathManager } from '@server/lib/video-path-manager' import { moveToNextState } from '@server/lib/video-state' import { VideoModel } from '@server/models/video/video' import { VideoBlacklistModel } from '@server/models/video/video-blacklist' import { VideoFileModel } from '@server/models/video/video-file' import { VideoLiveModel } from '@server/models/video/video-live' +import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' import { VideoLiveSessionModel } from '@server/models/video/video-live-session' import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@server/types/models' +import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@shared/ffmpeg' import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' -import { logger } from '../../../helpers/logger' +import { logger, loggerTagsFactory } from '../../../helpers/logger' + +const lTags = loggerTagsFactory('live', 'job') async function processVideoLiveEnding (job: Job) { const payload = job.data as VideoLiveEndingPayload - logger.info('Processing video live ending for %s.', payload.videoId, { payload }) + logger.info('Processing video live ending for %s.', payload.videoId, { payload, ...lTags() }) function logError () { - logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) + logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId, lTags()) } - const liveVideo = await VideoModel.load(payload.videoId) + const video = await VideoModel.load(payload.videoId) const live = await VideoLiveModel.loadByVideoId(payload.videoId) const liveSession = await VideoLiveSessionModel.load(payload.liveSessionId) - if (!liveVideo || !live || !liveSession) { + if (!video || !live || !liveSession) { logError() return } - LiveSegmentShaStore.Instance.cleanupShaSegments(liveVideo.uuid) + const permanentLive = live.permanentLive + + liveSession.endingProcessed = true + await liveSession.save() - if (live.saveReplay !== true) { - return cleanupLiveAndFederate({ live, video: liveVideo }) + if (liveSession.saveReplay !== true) { + return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId }) } - if (live.permanentLive) { - await saveReplayToExternalVideo({ liveVideo, liveSession, publishedAt: payload.publishedAt, replayDirectory: payload.replayDirectory }) + if (permanentLive) { + await saveReplayToExternalVideo({ + liveVideo: video, + liveSession, + publishedAt: payload.publishedAt, + replayDirectory: payload.replayDirectory + }) - return cleanupLiveAndFederate({ live, video: liveVideo }) + return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId }) } - return replaceLiveByReplay({ liveVideo, live, liveSession, replayDirectory: payload.replayDirectory }) + return replaceLiveByReplay({ + video, + liveSession, + live, + permanentLive, + replayDirectory: payload.replayDirectory + }) } // --------------------------------------------------------------------------- @@ -73,9 +86,9 @@ async function saveReplayToExternalVideo (options: { }) { const { liveVideo, liveSession, publishedAt, replayDirectory } = options - await cleanupTMPLiveFiles(getLiveDirectory(liveVideo)) + const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId) - const video = new VideoModel({ + const replayVideo = new VideoModel({ name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`, isLive: false, state: VideoState.TO_TRANSCODE, @@ -91,69 +104,71 @@ async function saveReplayToExternalVideo (options: { nsfw: liveVideo.nsfw, description: liveVideo.description, support: liveVideo.support, - privacy: liveVideo.privacy, + privacy: replaySettings.privacy, channelId: liveVideo.channelId }) as MVideoWithAllFiles - video.Thumbnails = [] - video.VideoFiles = [] - video.VideoStreamingPlaylists = [] + replayVideo.Thumbnails = [] + replayVideo.VideoFiles = [] + replayVideo.VideoStreamingPlaylists = [] - video.url = getLocalVideoActivityPubUrl(video) + replayVideo.url = getLocalVideoActivityPubUrl(replayVideo) - await video.save() + await replayVideo.save() - liveSession.replayVideoId = video.id + liveSession.replayVideoId = replayVideo.id await liveSession.save() // If live is blacklisted, also blacklist the replay const blacklist = await VideoBlacklistModel.loadByVideoId(liveVideo.id) if (blacklist) { await VideoBlacklistModel.create({ - videoId: video.id, + videoId: replayVideo.id, unfederated: blacklist.unfederated, reason: blacklist.reason, type: blacklist.type }) } - await assignReplayFilesToVideo({ video, replayDirectory }) + await assignReplayFilesToVideo({ video: replayVideo, replayDirectory }) await remove(replayDirectory) for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { - const image = await generateVideoMiniature({ video, videoFile: video.getMaxQualityFile(), type }) - await video.addAndSaveThumbnail(image) + const image = await generateVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type }) + await replayVideo.addAndSaveThumbnail(image) } - await moveToNextState({ video, isNewVideo: true }) + await moveToNextState({ video: replayVideo, isNewVideo: true }) } async function replaceLiveByReplay (options: { - liveVideo: MVideo + video: MVideo liveSession: MVideoLiveSession live: MVideoLive + permanentLive: boolean replayDirectory: string }) { - const { liveVideo, liveSession, live, replayDirectory } = options + const { video, liveSession, live, permanentLive, replayDirectory } = options - await cleanupTMPLiveFiles(getLiveDirectory(liveVideo)) + const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId) + const videoWithFiles = await VideoModel.loadFull(video.id) + const hlsPlaylist = videoWithFiles.getHLSPlaylist() + + await cleanupTMPLiveFiles(videoWithFiles, hlsPlaylist) await live.destroy() - liveVideo.isLive = false - liveVideo.waitTranscoding = true - liveVideo.state = VideoState.TO_TRANSCODE + videoWithFiles.isLive = false + videoWithFiles.privacy = replaySettings.privacy + videoWithFiles.waitTranscoding = true + videoWithFiles.state = VideoState.TO_TRANSCODE - await liveVideo.save() + await videoWithFiles.save() - liveSession.replayVideoId = liveVideo.id + liveSession.replayVideoId = videoWithFiles.id await liveSession.save() - // Remove old HLS playlist video files - const videoWithFiles = await VideoModel.loadAndPopulateAccountAndServerAndTags(liveVideo.id) - - const hlsPlaylist = videoWithFiles.getHLSPlaylist() await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) // Reset playlist @@ -164,7 +179,7 @@ async function replaceLiveByReplay (options: { await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) - if (live.permanentLive) { // Remove session replay + if (permanentLive) { // Remove session replay await remove(replayDirectory) } else { // We won't stream again in this live, we can delete the base replay directory await remove(getLiveReplayBaseDirectory(videoWithFiles)) @@ -199,51 +214,59 @@ async function assignReplayFilesToVideo (options: { }) { const { video, replayDirectory } = options - let durationDone = false - const concatenatedTsFiles = await readdir(replayDirectory) for (const concatenatedTsFile of concatenatedTsFiles) { + const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) + await video.reload() + const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) const probe = await ffprobePromise(concatenatedTsFilePath) const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) - - const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) - - const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ - video, - concatenatedTsFilePath, - resolution, - isPortraitMode, - isAAC: audioStream?.codec_name === 'aac' - }) - - if (!durationDone) { - video.duration = await getVideoStreamDuration(outputPath) - await video.save() - - durationDone = true + const { resolution } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) + const fps = await getVideoStreamFPS(concatenatedTsFilePath, probe) + + try { + await generateHlsPlaylistResolutionFromTS({ + video, + inputFileMutexReleaser, + concatenatedTsFilePath, + resolution, + fps, + isAAC: audioStream?.codec_name === 'aac' + }) + } catch (err) { + logger.error('Cannot generate HLS playlist resolution from TS files.', { err }) } + + inputFileMutexReleaser() } return video } async function cleanupLiveAndFederate (options: { - live: MVideoLive video: MVideo + permanentLive: boolean + streamingPlaylistId: number }) { - const { live, video } = options + const { permanentLive, video, streamingPlaylistId } = options - const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) + const streamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(streamingPlaylistId) - if (live.permanentLive) { - await cleanupPermanentLive(video, streamingPlaylist) - } else { - await cleanupNormalLive(video, streamingPlaylist) + if (streamingPlaylist) { + if (permanentLive) { + await cleanupAndDestroyPermanentLive(video, streamingPlaylist) + } else { + await cleanupUnsavedNormalLive(video, streamingPlaylist) + } } - const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id) - return federateVideoIfNeeded(fullVideo, false, undefined) + try { + const fullVideo = await VideoModel.loadFull(video.id) + return federateVideoIfNeeded(fullVideo, false, undefined) + } catch (err) { + logger.warn('Cannot federate live after cleanup', { videoId: video.id, err }) + } }