From b5b687550d8ef8beafdf706e45d6556fb5f4c876 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 26 Oct 2020 16:44:23 +0100 Subject: Add ability to save live replay --- server/lib/hls.ts | 32 ++++----- server/lib/job-queue/handlers/video-live-ending.ts | 84 ++++++++++++++++++---- server/lib/job-queue/handlers/video-transcoding.ts | 30 +++++--- server/lib/live-manager.ts | 2 +- server/lib/video-transcoding.ts | 24 ++++--- 5 files changed, 126 insertions(+), 46 deletions(-) (limited to 'server/lib') diff --git a/server/lib/hls.ts b/server/lib/hls.ts index e38a8788c..7aa152638 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts @@ -106,22 +106,6 @@ async function buildSha256Segment (segmentPath: string) { return sha256(buf) } -function getRangesFromPlaylist (playlistContent: string) { - const ranges: { offset: number, length: number }[] = [] - const lines = playlistContent.split('\n') - const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/ - - for (const line of lines) { - const captured = regex.exec(line) - - if (captured) { - ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) }) - } - } - - return ranges -} - function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) { let timer @@ -199,3 +183,19 @@ export { } // --------------------------------------------------------------------------- + +function getRangesFromPlaylist (playlistContent: string) { + const ranges: { offset: number, length: number }[] = [] + const lines = playlistContent.split('\n') + const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/ + + for (const line of lines) { + const captured = regex.exec(line) + + if (captured) { + ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) }) + } + } + + return ranges +} diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 1a58a9f7e..1a9a36129 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts @@ -1,24 +1,89 @@ import * as Bull from 'bull' import { readdir, remove } from 'fs-extra' import { join } from 'path' +import { getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' import { getHLSDirectory } from '@server/lib/video-paths' +import { generateHlsPlaylist } from '@server/lib/video-transcoding' import { VideoModel } from '@server/models/video/video' +import { VideoLiveModel } from '@server/models/video/video-live' import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' -import { VideoLiveEndingPayload } from '@shared/models' +import { MStreamingPlaylist, MVideo } from '@server/types/models' +import { VideoLiveEndingPayload, VideoState } from '@shared/models' import { logger } from '../../../helpers/logger' async function processVideoLiveEnding (job: Bull.Job) { const payload = job.data as VideoLiveEndingPayload - const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) - if (!video) { - logger.warn('Video live %d does not exist anymore. Cannot cleanup.', payload.videoId) + const video = await VideoModel.load(payload.videoId) + const live = await VideoLiveModel.loadByVideoId(payload.videoId) + + const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) + if (!video || !streamingPlaylist || !live) { + logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) return } - const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) + if (live.saveReplay !== true) { + return cleanupLive(video, streamingPlaylist) + } + + return saveLive(video, streamingPlaylist) +} + +// --------------------------------------------------------------------------- + +export { + processVideoLiveEnding +} + +// --------------------------------------------------------------------------- + +async function saveLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { + const videoFiles = await streamingPlaylist.get('VideoFiles') + const hlsDirectory = getHLSDirectory(video, false) + + for (const videoFile of videoFiles) { + const playlistPath = join(hlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(videoFile.resolution)) + + const mp4TmpName = buildMP4TmpName(videoFile.resolution) + await hlsPlaylistToFragmentedMP4(playlistPath, mp4TmpName) + } + + await cleanupLiveFiles(hlsDirectory) + + video.isLive = false + video.state = VideoState.TO_TRANSCODE + await video.save() + + const videoWithFiles = await VideoModel.loadWithFiles(video.id) + + for (const videoFile of videoFiles) { + const videoInputPath = buildMP4TmpName(videoFile.resolution) + const { isPortraitMode } = await getVideoFileResolution(videoInputPath) + + await generateHlsPlaylist({ + video: videoWithFiles, + videoInputPath, + resolution: videoFile.resolution, + copyCodecs: true, + isPortraitMode + }) + } + + video.state = VideoState.PUBLISHED + await video.save() +} + +async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { const hlsDirectory = getHLSDirectory(video, false) + await cleanupLiveFiles(hlsDirectory) + + streamingPlaylist.destroy() + .catch(err => logger.error('Cannot remove live streaming playlist.', { err })) +} + +async function cleanupLiveFiles (hlsDirectory: string) { const files = await readdir(hlsDirectory) for (const filename of files) { @@ -35,13 +100,8 @@ async function processVideoLiveEnding (job: Bull.Job) { .catch(err => logger.error('Cannot remove %s.', p, { err })) } } - - streamingPlaylist.destroy() - .catch(err => logger.error('Cannot remove live streaming playlist.', { err })) } -// --------------------------------------------------------------------------- - -export { - processVideoLiveEnding +function buildMP4TmpName (resolution: number) { + return resolution + 'tmp.mp4' } diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 6659ab716..2aebc29f7 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts @@ -1,21 +1,22 @@ import * as Bull from 'bull' +import { getVideoFilePath } from '@server/lib/video-paths' +import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models' import { MergeAudioTranscodingPayload, NewResolutionTranscodingPayload, OptimizeTranscodingPayload, VideoTranscodingPayload } from '../../../../shared' +import { retryTransactionWrapper } from '../../../helpers/database-utils' +import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' import { logger } from '../../../helpers/logger' +import { CONFIG } from '../../../initializers/config' +import { sequelizeTypescript } from '../../../initializers/database' import { VideoModel } from '../../../models/video/video' -import { JobQueue } from '../job-queue' import { federateVideoIfNeeded } from '../../activitypub/videos' -import { retryTransactionWrapper } from '../../../helpers/database-utils' -import { sequelizeTypescript } from '../../../initializers/database' -import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' -import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding' import { Notifier } from '../../notifier' -import { CONFIG } from '../../../initializers/config' -import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models' +import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding' +import { JobQueue } from '../job-queue' async function processVideoTranscoding (job: Bull.Job) { const payload = job.data as VideoTranscodingPayload @@ -29,7 +30,20 @@ async function processVideoTranscoding (job: Bull.Job) { } if (payload.type === 'hls') { - await generateHlsPlaylist(video, payload.resolution, payload.copyCodecs, payload.isPortraitMode || false) + const videoFileInput = payload.copyCodecs + ? video.getWebTorrentFile(payload.resolution) + : video.getMaxQualityFile() + + const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() + const videoInputPath = getVideoFilePath(videoOrStreamingPlaylist, videoFileInput) + + await generateHlsPlaylist({ + video, + videoInputPath, + resolution: payload.resolution, + copyCodecs: payload.copyCodecs, + isPortraitMode: payload.isPortraitMode || false + }) await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) } else if (payload.type === 'new-resolution') { diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index 3ff2434ff..692c49008 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts @@ -13,7 +13,7 @@ import { VideoModel } from '@server/models/video/video' import { VideoFileModel } from '@server/models/video/video-file' import { VideoLiveModel } from '@server/models/video/video-live' import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' -import { MStreamingPlaylist, MUser, MUserId, MVideoLive, MVideoLiveVideo } from '@server/types/models' +import { MStreamingPlaylist, MUserId, MVideoLive, MVideoLiveVideo } from '@server/types/models' import { VideoState, VideoStreamingPlaylistType } from '@shared/models' import { federateVideoIfNeeded } from './activitypub/videos' import { buildSha256Segment } from './hls' diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index a7b73a30d..c62b3c1ce 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts @@ -147,17 +147,18 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) } -async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, copyCodecs: boolean, isPortraitMode: boolean) { +async function generateHlsPlaylist (options: { + video: MVideoWithFile + videoInputPath: string + resolution: VideoResolution + copyCodecs: boolean + isPortraitMode: boolean +}) { + const { video, videoInputPath, resolution, copyCodecs, isPortraitMode } = options + const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)) - const videoFileInput = copyCodecs - ? video.getWebTorrentFile(resolution) - : video.getMaxQualityFile() - - const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() - const videoInputPath = getVideoFilePath(videoOrStreamingPlaylist, videoFileInput) - const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution) @@ -184,7 +185,7 @@ async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoReso videoId: video.id, playlistUrl, segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive), - p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, video.VideoFiles), + p2pMediaLoaderInfohashes: [], p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, type: VideoStreamingPlaylistType.HLS @@ -211,6 +212,11 @@ async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoReso await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') + videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes( + playlistUrl, videoStreamingPlaylist.VideoFiles + ) + await videoStreamingPlaylist.save() + video.setHLSPlaylist(videoStreamingPlaylist) await updateMasterHLSPlaylist(video) -- cgit v1.2.3