X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Flib%2Fjob-queue%2Fhandlers%2Fvideo-live-ending.ts;h=55fd09344135bf4a2b5a7e0a7e8af8472c887898;hb=26e3e98ff0e222a9fb9226938ac6902af77921bd;hp=386ccdc7b6d76e702140c63d25b4992ba1a70aa5;hpb=421ff4618da64f0849353383f690a014024c40da;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 386ccdc7b..55fd09344 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts @@ -1,49 +1,60 @@ -import * as Bull from 'bull' +import { Job } from 'bull' import { pathExists, readdir, remove } from 'fs-extra' import { join } from 'path' -import { ffprobePromise, getAudioStream, getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' -import { VIDEO_LIVE } from '@server/initializers/constants' -import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live' +import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamDuration } from '@server/helpers/ffmpeg' +import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' +import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' +import { cleanupLive, LiveSegmentShaStore } from '@server/lib/live' +import { + generateHLSMasterPlaylistFilename, + generateHlsSha256SegmentsFilename, + getLiveDirectory, + getLiveReplayBaseDirectory +} from '@server/lib/paths' import { generateVideoMiniature } from '@server/lib/thumbnail' -import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' -import { publishAndFederateIfNeeded } from '@server/lib/video' -import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHLSDirectory } from '@server/lib/video-paths' +import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding' +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 { VideoLiveSessionModel } from '@server/models/video/video-live-session' import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' -import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' +import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@server/types/models' import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' import { logger } from '../../../helpers/logger' -async function processVideoLiveEnding (job: Bull.Job) { +async function processVideoLiveEnding (job: Job) { const payload = job.data as VideoLiveEndingPayload + logger.info('Processing video live ending for %s.', payload.videoId, { payload }) + function logError () { logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) } - const video = await VideoModel.load(payload.videoId) + const liveVideo = await VideoModel.load(payload.videoId) const live = await VideoLiveModel.loadByVideoId(payload.videoId) + const liveSession = await VideoLiveSessionModel.load(payload.liveSessionId) - if (!video || !live) { + if (!liveVideo || !live || !liveSession) { logError() return } - const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) - if (!streamingPlaylist) { - logError() - return + LiveSegmentShaStore.Instance.cleanupShaSegments(liveVideo.uuid) + + if (live.saveReplay !== true) { + return cleanupLiveAndFederate({ liveVideo }) } - LiveSegmentShaStore.Instance.cleanupShaSegments(video.uuid) + if (live.permanentLive) { + await saveReplayToExternalVideo({ liveVideo, liveSession, publishedAt: payload.publishedAt, replayDirectory: payload.replayDirectory }) - if (live.saveReplay !== true) { - return cleanupLive(video, streamingPlaylist) + return cleanupLiveAndFederate({ liveVideo }) } - return saveLive(video, live, streamingPlaylist) + return replaceLiveByReplay({ liveVideo, live, liveSession, replayDirectory: payload.replayDirectory }) } // --------------------------------------------------------------------------- @@ -54,29 +65,93 @@ export { // --------------------------------------------------------------------------- -async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MStreamingPlaylist) { - const hlsDirectory = getHLSDirectory(video, false) - const replayDirectory = join(hlsDirectory, VIDEO_LIVE.REPLAY_DIRECTORY) +async function saveReplayToExternalVideo (options: { + liveVideo: MVideo + liveSession: MVideoLiveSession + publishedAt: string + replayDirectory: string +}) { + const { liveVideo, liveSession, publishedAt, replayDirectory } = options + + await cleanupTMPLiveFiles(getLiveDirectory(liveVideo)) + + const video = new VideoModel({ + name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`, + isLive: false, + state: VideoState.TO_TRANSCODE, + duration: 0, + + remote: liveVideo.remote, + category: liveVideo.category, + licence: liveVideo.licence, + language: liveVideo.language, + commentsEnabled: liveVideo.commentsEnabled, + downloadEnabled: liveVideo.downloadEnabled, + waitTranscoding: true, + nsfw: liveVideo.nsfw, + description: liveVideo.description, + support: liveVideo.support, + privacy: liveVideo.privacy, + channelId: liveVideo.channelId + }) as MVideoWithAllFiles + + video.Thumbnails = [] + video.VideoFiles = [] + video.VideoStreamingPlaylists = [] + + video.url = getLocalVideoActivityPubUrl(video) + + await video.save() + + liveSession.replayVideoId = video.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, + unfederated: blacklist.unfederated, + reason: blacklist.reason, + type: blacklist.type + }) + } + + await assignReplayFilesToVideo({ video, 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 rootFiles = await readdir(hlsDirectory) + await moveToNextState({ video, isNewVideo: true }) +} - const playlistFiles = rootFiles.filter(file => { - return file.endsWith('.m3u8') && file !== streamingPlaylist.playlistFilename - }) +async function replaceLiveByReplay (options: { + liveVideo: MVideo + liveSession: MVideoLiveSession + live: MVideoLive + replayDirectory: string +}) { + const { liveVideo, liveSession, live, replayDirectory } = options - await cleanupLiveFiles(hlsDirectory) + await cleanupTMPLiveFiles(getLiveDirectory(liveVideo)) await live.destroy() - video.isLive = false - // Reinit views - video.views = 0 - video.state = VideoState.TO_TRANSCODE + liveVideo.isLive = false + liveVideo.waitTranscoding = true + liveVideo.state = VideoState.TO_TRANSCODE - await video.save() + await liveVideo.save() + + liveSession.replayVideoId = liveVideo.id + await liveSession.save() // Remove old HLS playlist video files - const videoWithFiles = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id) + const videoWithFiles = await VideoModel.loadAndPopulateAccountAndServerAndTags(liveVideo.id) const hlsPlaylist = videoWithFiles.getHLSPlaylist() await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) @@ -87,56 +162,83 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename() await hlsPlaylist.save() + await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) + + await remove(getLiveReplayBaseDirectory(videoWithFiles)) + + // Regenerate the thumbnail & preview? + if (videoWithFiles.getMiniature().automaticallyGenerated === true) { + const miniature = await generateVideoMiniature({ + video: videoWithFiles, + videoFile: videoWithFiles.getMaxQualityFile(), + type: ThumbnailType.MINIATURE + }) + await videoWithFiles.addAndSaveThumbnail(miniature) + } + + if (videoWithFiles.getPreview().automaticallyGenerated === true) { + const preview = await generateVideoMiniature({ + video: videoWithFiles, + videoFile: videoWithFiles.getMaxQualityFile(), + type: ThumbnailType.PREVIEW + }) + await videoWithFiles.addAndSaveThumbnail(preview) + } + + // We consider this is a new video + await moveToNextState({ video: videoWithFiles, isNewVideo: true }) +} + +async function assignReplayFilesToVideo (options: { + video: MVideo + replayDirectory: string +}) { + const { video, replayDirectory } = options + let durationDone = false - for (const playlistFile of playlistFiles) { - const concatenatedTsFile = buildConcatenatedName(playlistFile) + const concatenatedTsFiles = await readdir(replayDirectory) + + for (const concatenatedTsFile of concatenatedTsFiles) { const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) const probe = await ffprobePromise(concatenatedTsFilePath) const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) - const { videoFileResolution, isPortraitMode } = await getVideoFileResolution(concatenatedTsFilePath, probe) + const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) - const outputPath = await generateHlsPlaylistResolutionFromTS({ - video: videoWithFiles, + const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ + video, concatenatedTsFilePath, - resolution: videoFileResolution, + resolution, isPortraitMode, isAAC: audioStream?.codec_name === 'aac' }) if (!durationDone) { - videoWithFiles.duration = await getDurationFromVideoFile(outputPath) - await videoWithFiles.save() + video.duration = await getVideoStreamDuration(outputPath) + await video.save() durationDone = true } } - await remove(replayDirectory) + return video +} - // Regenerate the thumbnail & preview? - if (videoWithFiles.getMiniature().automaticallyGenerated === true) { - await generateVideoMiniature({ - video: videoWithFiles, - videoFile: videoWithFiles.getMaxQualityFile(), - type: ThumbnailType.MINIATURE - }) - } +async function cleanupLiveAndFederate (options: { + liveVideo: MVideo +}) { + const { liveVideo } = options - if (videoWithFiles.getPreview().automaticallyGenerated === true) { - await generateVideoMiniature({ - video: videoWithFiles, - videoFile: videoWithFiles.getMaxQualityFile(), - type: ThumbnailType.PREVIEW - }) - } + const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(liveVideo.id) + await cleanupLive(liveVideo, streamingPlaylist) - await publishAndFederateIfNeeded(videoWithFiles, true) + const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(liveVideo.id) + return federateVideoIfNeeded(fullVideo, false, undefined) } -async function cleanupLiveFiles (hlsDirectory: string) { +async function cleanupTMPLiveFiles (hlsDirectory: string) { if (!await pathExists(hlsDirectory)) return const files = await readdir(hlsDirectory)