aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-02-01 14:19:44 +0100
committerChocobozzz <me@florianbigard.com>2022-02-01 14:19:44 +0100
commita2caee9f5162232234de2e8aae6957cc7f38c853 (patch)
treef6ca87b03ed80ca4b6625a7b2b31706cd4f831ad
parent0f11ec8dd32b50897c18588db948e96cf0fc2c70 (diff)
downloadPeerTube-a2caee9f5162232234de2e8aae6957cc7f38c853.tar.gz
PeerTube-a2caee9f5162232234de2e8aae6957cc7f38c853.tar.zst
PeerTube-a2caee9f5162232234de2e8aae6957cc7f38c853.zip
Fix HLS re transcoding with object storage enabled
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.html2
-rw-r--r--server/lib/hls.ts15
-rw-r--r--server/lib/job-queue/handlers/move-to-object-storage.ts17
-rw-r--r--server/lib/object-storage/videos.ts6
-rw-r--r--server/tests/api/videos/video-create-transcoding.ts85
-rw-r--r--server/tests/cli/create-transcoding-job.ts13
6 files changed, 119 insertions, 19 deletions
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html
index 121bc502c..7fc796751 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.html
+++ b/client/src/app/+admin/overview/videos/video-list.component.html
@@ -82,7 +82,7 @@
82 82
83 <td> 83 <td>
84 <span *ngIf="isHLS(video)" class="badge badge-blue">HLS</span> 84 <span *ngIf="isHLS(video)" class="badge badge-blue">HLS</span>
85 <span *ngIf="isWebTorrent(video)" class="badge badge-blue">WebTorrent</span> 85 <span *ngIf="isWebTorrent(video)" class="badge badge-blue">WebTorrent ({{ video.files.length }})</span>
86 <span *ngIf="video.isLive" class="badge badge-blue">Live</span> 86 <span *ngIf="video.isLive" class="badge badge-blue">Live</span>
87 87
88 <span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span> 88 <span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span>
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
index 1574ff27b..985f50587 100644
--- a/server/lib/hls.ts
+++ b/server/lib/hls.ts
@@ -3,6 +3,7 @@ import { flatten, uniq } from 'lodash'
3import { basename, dirname, join } from 'path' 3import { basename, dirname, join } from 'path'
4import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models' 4import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models'
5import { sha256 } from '@shared/extra-utils' 5import { sha256 } from '@shared/extra-utils'
6import { VideoStorage } from '@shared/models'
6import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils' 7import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils'
7import { logger } from '../helpers/logger' 8import { logger } from '../helpers/logger'
8import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' 9import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
@@ -12,6 +13,7 @@ import { P2P_MEDIA_LOADER_PEER_VERSION, REQUEST_TIMEOUTS } from '../initializers
12import { sequelizeTypescript } from '../initializers/database' 13import { sequelizeTypescript } from '../initializers/database'
13import { VideoFileModel } from '../models/video/video-file' 14import { VideoFileModel } from '../models/video/video-file'
14import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 15import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
16import { storeHLSFile } from './object-storage'
15import { getHlsResolutionPlaylistFilename } from './paths' 17import { getHlsResolutionPlaylistFilename } from './paths'
16import { VideoPathManager } from './video-path-manager' 18import { VideoPathManager } from './video-path-manager'
17 19
@@ -58,8 +60,12 @@ async function updateMasterHLSPlaylist (video: MVideo, playlist: MStreamingPlayl
58 }) 60 })
59 } 61 }
60 62
61 await VideoPathManager.Instance.makeAvailablePlaylistFile(playlist, playlist.playlistFilename, masterPlaylistPath => { 63 await VideoPathManager.Instance.makeAvailablePlaylistFile(playlist, playlist.playlistFilename, async masterPlaylistPath => {
62 return writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') 64 await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n')
65
66 if (playlist.storage === VideoStorage.OBJECT_STORAGE) {
67 await storeHLSFile(playlist, playlist.playlistFilename, masterPlaylistPath)
68 }
63 }) 69 })
64} 70}
65 71
@@ -94,6 +100,11 @@ async function updateSha256VODSegments (video: MVideoUUID, playlist: MStreamingP
94 100
95 const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename) 101 const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename)
96 await outputJSON(outputPath, json) 102 await outputJSON(outputPath, json)
103
104 if (playlist.storage === VideoStorage.OBJECT_STORAGE) {
105 await storeHLSFile(playlist, playlist.segmentsSha256Filename)
106 await remove(outputPath)
107 }
97} 108}
98 109
99async function buildSha256Segment (segmentPath: string) { 110async function buildSha256Segment (segmentPath: string) {
diff --git a/server/lib/job-queue/handlers/move-to-object-storage.ts b/server/lib/job-queue/handlers/move-to-object-storage.ts
index 9e39322a8..69b441176 100644
--- a/server/lib/job-queue/handlers/move-to-object-storage.ts
+++ b/server/lib/job-queue/handlers/move-to-object-storage.ts
@@ -1,7 +1,7 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { remove } from 'fs-extra' 2import { remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { logger } from '@server/helpers/logger' 4import { logger, loggerTagsFactory } from '@server/helpers/logger'
5import { updateTorrentMetadata } from '@server/helpers/webtorrent' 5import { updateTorrentMetadata } from '@server/helpers/webtorrent'
6import { CONFIG } from '@server/initializers/config' 6import { CONFIG } from '@server/initializers/config'
7import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants' 7import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants'
@@ -13,6 +13,8 @@ import { VideoJobInfoModel } from '@server/models/video/video-job-info'
13import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoWithAllFiles } from '@server/types/models' 13import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoWithAllFiles } from '@server/types/models'
14import { MoveObjectStoragePayload, VideoStorage } from '@shared/models' 14import { MoveObjectStoragePayload, VideoStorage } from '@shared/models'
15 15
16const lTagsBase = loggerTagsFactory('move-object-storage')
17
16export async function processMoveToObjectStorage (job: Job) { 18export async function processMoveToObjectStorage (job: Job) {
17 const payload = job.data as MoveObjectStoragePayload 19 const payload = job.data as MoveObjectStoragePayload
18 logger.info('Moving video %s in job %d.', payload.videoUUID, job.id) 20 logger.info('Moving video %s in job %d.', payload.videoUUID, job.id)
@@ -20,26 +22,33 @@ export async function processMoveToObjectStorage (job: Job) {
20 const video = await VideoModel.loadWithFiles(payload.videoUUID) 22 const video = await VideoModel.loadWithFiles(payload.videoUUID)
21 // No video, maybe deleted? 23 // No video, maybe deleted?
22 if (!video) { 24 if (!video) {
23 logger.info('Can\'t process job %d, video does not exist.', job.id) 25 logger.info('Can\'t process job %d, video does not exist.', job.id, lTagsBase(payload.videoUUID))
24 return undefined 26 return undefined
25 } 27 }
26 28
29 const lTags = lTagsBase(video.uuid, video.url)
30
27 try { 31 try {
28 if (video.VideoFiles) { 32 if (video.VideoFiles) {
33 logger.debug('Moving %d webtorrent files for video %s.', video.VideoFiles.length, video.uuid, lTags)
34
29 await moveWebTorrentFiles(video) 35 await moveWebTorrentFiles(video)
30 } 36 }
31 37
32 if (video.VideoStreamingPlaylists) { 38 if (video.VideoStreamingPlaylists) {
39 logger.debug('Moving HLS playlist of %s.', video.uuid)
40
33 await moveHLSFiles(video) 41 await moveHLSFiles(video)
34 } 42 }
35 43
36 const pendingMove = await VideoJobInfoModel.decrease(video.uuid, 'pendingMove') 44 const pendingMove = await VideoJobInfoModel.decrease(video.uuid, 'pendingMove')
37 if (pendingMove === 0) { 45 if (pendingMove === 0) {
38 logger.info('Running cleanup after moving files to object storage (video %s in job %d)', video.uuid, job.id) 46 logger.info('Running cleanup after moving files to object storage (video %s in job %d)', video.uuid, job.id, lTags)
47
39 await doAfterLastJob(video, payload.isNewVideo) 48 await doAfterLastJob(video, payload.isNewVideo)
40 } 49 }
41 } catch (err) { 50 } catch (err) {
42 logger.error('Cannot move video %s to object storage.', video.url, { err }) 51 logger.error('Cannot move video %s to object storage.', video.url, { err, ...lTags })
43 52
44 await moveToFailedMoveToObjectStorageState(video) 53 await moveToFailedMoveToObjectStorageState(video)
45 await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingMove') 54 await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingMove')
diff --git a/server/lib/object-storage/videos.ts b/server/lib/object-storage/videos.ts
index 8988f3e2a..066b48ab0 100644
--- a/server/lib/object-storage/videos.ts
+++ b/server/lib/object-storage/videos.ts
@@ -6,11 +6,9 @@ import { getHLSDirectory } from '../paths'
6import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys' 6import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys'
7import { lTags, makeAvailable, removeObject, removePrefix, storeObject } from './shared' 7import { lTags, makeAvailable, removeObject, removePrefix, storeObject } from './shared'
8 8
9function storeHLSFile (playlist: MStreamingPlaylistVideo, filename: string) { 9function storeHLSFile (playlist: MStreamingPlaylistVideo, filename: string, path?: string) {
10 const baseHlsDirectory = getHLSDirectory(playlist.Video)
11
12 return storeObject({ 10 return storeObject({
13 inputPath: join(baseHlsDirectory, filename), 11 inputPath: path ?? join(getHLSDirectory(playlist.Video), filename),
14 objectStorageKey: generateHLSObjectStorageKey(playlist, filename), 12 objectStorageKey: generateHLSObjectStorageKey(playlist, filename),
15 bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS 13 bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS
16 }) 14 })
diff --git a/server/tests/api/videos/video-create-transcoding.ts b/server/tests/api/videos/video-create-transcoding.ts
index dcdbd9c6e..445866a16 100644
--- a/server/tests/api/videos/video-create-transcoding.ts
+++ b/server/tests/api/videos/video-create-transcoding.ts
@@ -2,11 +2,12 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { expectStartWith } from '@server/tests/shared' 5import { checkResolutionsInMasterPlaylist, expectStartWith } from '@server/tests/shared'
6import { areObjectStorageTestsDisabled } from '@shared/core-utils' 6import { areObjectStorageTestsDisabled } from '@shared/core-utils'
7import { HttpStatusCode, VideoDetails } from '@shared/models' 7import { HttpStatusCode, VideoDetails } from '@shared/models'
8import { 8import {
9 cleanupTests, 9 cleanupTests,
10 ConfigCommand,
10 createMultipleServers, 11 createMultipleServers,
11 doubleFollow, 12 doubleFollow,
12 expectNoFailedTranscodingJob, 13 expectNoFailedTranscodingJob,
@@ -25,14 +26,19 @@ async function checkFilesInObjectStorage (video: VideoDetails) {
25 await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) 26 await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200)
26 } 27 }
27 28
28 const streamingPlaylistFiles = video.streamingPlaylists.length === 0 29 if (video.streamingPlaylists.length === 0) return
29 ? []
30 : video.streamingPlaylists[0].files
31 30
32 for (const file of streamingPlaylistFiles) { 31 const hlsPlaylist = video.streamingPlaylists[0]
32 for (const file of hlsPlaylist.files) {
33 expectStartWith(file.fileUrl, ObjectStorageCommand.getPlaylistBaseUrl()) 33 expectStartWith(file.fileUrl, ObjectStorageCommand.getPlaylistBaseUrl())
34 await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) 34 await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200)
35 } 35 }
36
37 expectStartWith(hlsPlaylist.playlistUrl, ObjectStorageCommand.getPlaylistBaseUrl())
38 await makeRawRequest(hlsPlaylist.playlistUrl, HttpStatusCode.OK_200)
39
40 expectStartWith(hlsPlaylist.segmentsSha256Url, ObjectStorageCommand.getPlaylistBaseUrl())
41 await makeRawRequest(hlsPlaylist.segmentsSha256Url, HttpStatusCode.OK_200)
36} 42}
37 43
38function runTests (objectStorage: boolean) { 44function runTests (objectStorage: boolean) {
@@ -150,6 +156,75 @@ function runTests (objectStorage: boolean) {
150 } 156 }
151 }) 157 })
152 158
159 it('Should correctly update HLS playlist on resolution change', async function () {
160 await servers[0].config.updateExistingSubConfig({
161 newConfig: {
162 transcoding: {
163 enabled: true,
164 resolutions: ConfigCommand.getCustomConfigResolutions(false),
165
166 webtorrent: {
167 enabled: true
168 },
169 hls: {
170 enabled: true
171 }
172 }
173 }
174 })
175
176 const { uuid } = await servers[0].videos.quickUpload({ name: 'quick' })
177
178 await waitJobs(servers)
179
180 for (const server of servers) {
181 const videoDetails = await server.videos.get({ id: uuid })
182
183 expect(videoDetails.files).to.have.lengthOf(1)
184 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
185 expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(1)
186
187 if (objectStorage) await checkFilesInObjectStorage(videoDetails)
188 }
189
190 await servers[0].config.updateExistingSubConfig({
191 newConfig: {
192 transcoding: {
193 enabled: true,
194 resolutions: ConfigCommand.getCustomConfigResolutions(true),
195
196 webtorrent: {
197 enabled: true
198 },
199 hls: {
200 enabled: true
201 }
202 }
203 }
204 })
205
206 await servers[0].videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' })
207 await waitJobs(servers)
208
209 for (const server of servers) {
210 const videoDetails = await server.videos.get({ id: uuid })
211
212 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
213 expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
214
215 if (objectStorage) {
216 await checkFilesInObjectStorage(videoDetails)
217
218 const hlsPlaylist = videoDetails.streamingPlaylists[0]
219 const resolutions = hlsPlaylist.files.map(f => f.resolution.id)
220 await checkResolutionsInMasterPlaylist({ server: servers[0], playlistUrl: hlsPlaylist.playlistUrl, resolutions })
221
222 const shaBody = await servers[0].streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
223 expect(Object.keys(shaBody)).to.have.lengthOf(5)
224 }
225 }
226 })
227
153 it('Should not have updated published at attributes', async function () { 228 it('Should not have updated published at attributes', async function () {
154 const video = await servers[0].videos.get({ id: videoUUID }) 229 const video = await servers[0].videos.get({ id: videoUUID })
155 230
diff --git a/server/tests/cli/create-transcoding-job.ts b/server/tests/cli/create-transcoding-job.ts
index c85130fef..b90e9bde9 100644
--- a/server/tests/cli/create-transcoding-job.ts
+++ b/server/tests/cli/create-transcoding-job.ts
@@ -14,7 +14,7 @@ import {
14 setAccessTokensToServers, 14 setAccessTokensToServers,
15 waitJobs 15 waitJobs
16} from '@shared/server-commands' 16} from '@shared/server-commands'
17import { expectStartWith } from '../shared' 17import { checkResolutionsInMasterPlaylist, expectStartWith } from '../shared'
18 18
19const expect = chai.expect 19const expect = chai.expect
20 20
@@ -163,11 +163,18 @@ function runTests (objectStorage: boolean) {
163 163
164 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) 164 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
165 165
166 const files = videoDetails.streamingPlaylists[0].files 166 const hlsPlaylist = videoDetails.streamingPlaylists[0]
167
168 const files = hlsPlaylist.files
167 expect(files).to.have.lengthOf(1) 169 expect(files).to.have.lengthOf(1)
168 expect(files[0].resolution.id).to.equal(480) 170 expect(files[0].resolution.id).to.equal(480)
169 171
170 if (objectStorage) await checkFilesInObjectStorage(files, 'playlist') 172 if (objectStorage) {
173 await checkFilesInObjectStorage(files, 'playlist')
174
175 const resolutions = files.map(f => f.resolution.id)
176 await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
177 }
171 } 178 }
172 }) 179 })
173 180