aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-10-26 16:44:23 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-11-09 15:33:04 +0100
commitb5b687550d8ef8beafdf706e45d6556fb5f4c876 (patch)
tree232412d463c78af1f7ab5797db5aecf1096d08da /server/lib
parentef680f68351ec10ab73a1131570a6d14ce14c195 (diff)
downloadPeerTube-b5b687550d8ef8beafdf706e45d6556fb5f4c876.tar.gz
PeerTube-b5b687550d8ef8beafdf706e45d6556fb5f4c876.tar.zst
PeerTube-b5b687550d8ef8beafdf706e45d6556fb5f4c876.zip
Add ability to save live replay
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/hls.ts32
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts84
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts30
-rw-r--r--server/lib/live-manager.ts2
-rw-r--r--server/lib/video-transcoding.ts24
5 files changed, 126 insertions, 46 deletions
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) {
106 return sha256(buf) 106 return sha256(buf)
107} 107}
108 108
109function getRangesFromPlaylist (playlistContent: string) {
110 const ranges: { offset: number, length: number }[] = []
111 const lines = playlistContent.split('\n')
112 const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/
113
114 for (const line of lines) {
115 const captured = regex.exec(line)
116
117 if (captured) {
118 ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) })
119 }
120 }
121
122 return ranges
123}
124
125function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) { 109function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) {
126 let timer 110 let timer
127 111
@@ -199,3 +183,19 @@ export {
199} 183}
200 184
201// --------------------------------------------------------------------------- 185// ---------------------------------------------------------------------------
186
187function getRangesFromPlaylist (playlistContent: string) {
188 const ranges: { offset: number, length: number }[] = []
189 const lines = playlistContent.split('\n')
190 const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/
191
192 for (const line of lines) {
193 const captured = regex.exec(line)
194
195 if (captured) {
196 ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) })
197 }
198 }
199
200 return ranges
201}
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 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { readdir, remove } from 'fs-extra' 2import { readdir, remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils'
4import { getHLSDirectory } from '@server/lib/video-paths' 5import { getHLSDirectory } from '@server/lib/video-paths'
6import { generateHlsPlaylist } from '@server/lib/video-transcoding'
5import { VideoModel } from '@server/models/video/video' 7import { VideoModel } from '@server/models/video/video'
8import { VideoLiveModel } from '@server/models/video/video-live'
6import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 9import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
7import { VideoLiveEndingPayload } from '@shared/models' 10import { MStreamingPlaylist, MVideo } from '@server/types/models'
11import { VideoLiveEndingPayload, VideoState } from '@shared/models'
8import { logger } from '../../../helpers/logger' 12import { logger } from '../../../helpers/logger'
9 13
10async function processVideoLiveEnding (job: Bull.Job) { 14async function processVideoLiveEnding (job: Bull.Job) {
11 const payload = job.data as VideoLiveEndingPayload 15 const payload = job.data as VideoLiveEndingPayload
12 16
13 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) 17 const video = await VideoModel.load(payload.videoId)
14 if (!video) { 18 const live = await VideoLiveModel.loadByVideoId(payload.videoId)
15 logger.warn('Video live %d does not exist anymore. Cannot cleanup.', payload.videoId) 19
20 const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
21 if (!video || !streamingPlaylist || !live) {
22 logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId)
16 return 23 return
17 } 24 }
18 25
19 const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) 26 if (live.saveReplay !== true) {
27 return cleanupLive(video, streamingPlaylist)
28 }
29
30 return saveLive(video, streamingPlaylist)
31}
32
33// ---------------------------------------------------------------------------
34
35export {
36 processVideoLiveEnding
37}
38
39// ---------------------------------------------------------------------------
40
41async function saveLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) {
42 const videoFiles = await streamingPlaylist.get('VideoFiles')
43 const hlsDirectory = getHLSDirectory(video, false)
44
45 for (const videoFile of videoFiles) {
46 const playlistPath = join(hlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(videoFile.resolution))
47
48 const mp4TmpName = buildMP4TmpName(videoFile.resolution)
49 await hlsPlaylistToFragmentedMP4(playlistPath, mp4TmpName)
50 }
51
52 await cleanupLiveFiles(hlsDirectory)
53
54 video.isLive = false
55 video.state = VideoState.TO_TRANSCODE
56 await video.save()
57
58 const videoWithFiles = await VideoModel.loadWithFiles(video.id)
59
60 for (const videoFile of videoFiles) {
61 const videoInputPath = buildMP4TmpName(videoFile.resolution)
62 const { isPortraitMode } = await getVideoFileResolution(videoInputPath)
63
64 await generateHlsPlaylist({
65 video: videoWithFiles,
66 videoInputPath,
67 resolution: videoFile.resolution,
68 copyCodecs: true,
69 isPortraitMode
70 })
71 }
72
73 video.state = VideoState.PUBLISHED
74 await video.save()
75}
76
77async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) {
20 const hlsDirectory = getHLSDirectory(video, false) 78 const hlsDirectory = getHLSDirectory(video, false)
21 79
80 await cleanupLiveFiles(hlsDirectory)
81
82 streamingPlaylist.destroy()
83 .catch(err => logger.error('Cannot remove live streaming playlist.', { err }))
84}
85
86async function cleanupLiveFiles (hlsDirectory: string) {
22 const files = await readdir(hlsDirectory) 87 const files = await readdir(hlsDirectory)
23 88
24 for (const filename of files) { 89 for (const filename of files) {
@@ -35,13 +100,8 @@ async function processVideoLiveEnding (job: Bull.Job) {
35 .catch(err => logger.error('Cannot remove %s.', p, { err })) 100 .catch(err => logger.error('Cannot remove %s.', p, { err }))
36 } 101 }
37 } 102 }
38
39 streamingPlaylist.destroy()
40 .catch(err => logger.error('Cannot remove live streaming playlist.', { err }))
41} 103}
42 104
43// --------------------------------------------------------------------------- 105function buildMP4TmpName (resolution: number) {
44 106 return resolution + 'tmp.mp4'
45export {
46 processVideoLiveEnding
47} 107}
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 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { getVideoFilePath } from '@server/lib/video-paths'
3import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models'
2import { 4import {
3 MergeAudioTranscodingPayload, 5 MergeAudioTranscodingPayload,
4 NewResolutionTranscodingPayload, 6 NewResolutionTranscodingPayload,
5 OptimizeTranscodingPayload, 7 OptimizeTranscodingPayload,
6 VideoTranscodingPayload 8 VideoTranscodingPayload
7} from '../../../../shared' 9} from '../../../../shared'
10import { retryTransactionWrapper } from '../../../helpers/database-utils'
11import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
8import { logger } from '../../../helpers/logger' 12import { logger } from '../../../helpers/logger'
13import { CONFIG } from '../../../initializers/config'
14import { sequelizeTypescript } from '../../../initializers/database'
9import { VideoModel } from '../../../models/video/video' 15import { VideoModel } from '../../../models/video/video'
10import { JobQueue } from '../job-queue'
11import { federateVideoIfNeeded } from '../../activitypub/videos' 16import { federateVideoIfNeeded } from '../../activitypub/videos'
12import { retryTransactionWrapper } from '../../../helpers/database-utils'
13import { sequelizeTypescript } from '../../../initializers/database'
14import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
15import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding'
16import { Notifier } from '../../notifier' 17import { Notifier } from '../../notifier'
17import { CONFIG } from '../../../initializers/config' 18import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding'
18import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models' 19import { JobQueue } from '../job-queue'
19 20
20async function processVideoTranscoding (job: Bull.Job) { 21async function processVideoTranscoding (job: Bull.Job) {
21 const payload = job.data as VideoTranscodingPayload 22 const payload = job.data as VideoTranscodingPayload
@@ -29,7 +30,20 @@ async function processVideoTranscoding (job: Bull.Job) {
29 } 30 }
30 31
31 if (payload.type === 'hls') { 32 if (payload.type === 'hls') {
32 await generateHlsPlaylist(video, payload.resolution, payload.copyCodecs, payload.isPortraitMode || false) 33 const videoFileInput = payload.copyCodecs
34 ? video.getWebTorrentFile(payload.resolution)
35 : video.getMaxQualityFile()
36
37 const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist()
38 const videoInputPath = getVideoFilePath(videoOrStreamingPlaylist, videoFileInput)
39
40 await generateHlsPlaylist({
41 video,
42 videoInputPath,
43 resolution: payload.resolution,
44 copyCodecs: payload.copyCodecs,
45 isPortraitMode: payload.isPortraitMode || false
46 })
33 47
34 await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) 48 await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
35 } else if (payload.type === 'new-resolution') { 49 } 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'
13import { VideoFileModel } from '@server/models/video/video-file' 13import { VideoFileModel } from '@server/models/video/video-file'
14import { VideoLiveModel } from '@server/models/video/video-live' 14import { VideoLiveModel } from '@server/models/video/video-live'
15import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 15import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
16import { MStreamingPlaylist, MUser, MUserId, MVideoLive, MVideoLiveVideo } from '@server/types/models' 16import { MStreamingPlaylist, MUserId, MVideoLive, MVideoLiveVideo } from '@server/types/models'
17import { VideoState, VideoStreamingPlaylistType } from '@shared/models' 17import { VideoState, VideoStreamingPlaylistType } from '@shared/models'
18import { federateVideoIfNeeded } from './activitypub/videos' 18import { federateVideoIfNeeded } from './activitypub/videos'
19import { buildSha256Segment } from './hls' 19import { 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
147 return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) 147 return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
148} 148}
149 149
150async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, copyCodecs: boolean, isPortraitMode: boolean) { 150async function generateHlsPlaylist (options: {
151 video: MVideoWithFile
152 videoInputPath: string
153 resolution: VideoResolution
154 copyCodecs: boolean
155 isPortraitMode: boolean
156}) {
157 const { video, videoInputPath, resolution, copyCodecs, isPortraitMode } = options
158
151 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) 159 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
152 await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)) 160 await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
153 161
154 const videoFileInput = copyCodecs
155 ? video.getWebTorrentFile(resolution)
156 : video.getMaxQualityFile()
157
158 const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist()
159 const videoInputPath = getVideoFilePath(videoOrStreamingPlaylist, videoFileInput)
160
161 const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) 162 const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
162 const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution) 163 const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
163 164
@@ -184,7 +185,7 @@ async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoReso
184 videoId: video.id, 185 videoId: video.id,
185 playlistUrl, 186 playlistUrl,
186 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive), 187 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive),
187 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, video.VideoFiles), 188 p2pMediaLoaderInfohashes: [],
188 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, 189 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
189 190
190 type: VideoStreamingPlaylistType.HLS 191 type: VideoStreamingPlaylistType.HLS
@@ -211,6 +212,11 @@ async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoReso
211 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) 212 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
212 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') 213 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
213 214
215 videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(
216 playlistUrl, videoStreamingPlaylist.VideoFiles
217 )
218 await videoStreamingPlaylist.save()
219
214 video.setHLSPlaylist(videoStreamingPlaylist) 220 video.setHLSPlaylist(videoStreamingPlaylist)
215 221
216 await updateMasterHLSPlaylist(video) 222 await updateMasterHLSPlaylist(video)