diff options
author | Chocobozzz <me@florianbigard.com> | 2021-07-23 11:20:00 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-07-26 11:29:31 +0200 |
commit | 764b1a14fc494f2cfd7ea590d2f07b01df65c7ad (patch) | |
tree | 198ca5f242c63a205a05fa4cfd6d063277c541fd | |
parent | 83903cb65d531a6b6b91715387493ba8312b264d (diff) | |
download | PeerTube-764b1a14fc494f2cfd7ea590d2f07b01df65c7ad.tar.gz PeerTube-764b1a14fc494f2cfd7ea590d2f07b01df65c7ad.tar.zst PeerTube-764b1a14fc494f2cfd7ea590d2f07b01df65c7ad.zip |
Use random names for VOD HLS playlists
44 files changed, 505 insertions, 278 deletions
diff --git a/scripts/optimize-old-videos.ts b/scripts/optimize-old-videos.ts index 9692d76ba..bde9d1e01 100644 --- a/scripts/optimize-old-videos.ts +++ b/scripts/optimize-old-videos.ts | |||
@@ -19,13 +19,13 @@ run() | |||
19 | process.exit(-1) | 19 | process.exit(-1) |
20 | }) | 20 | }) |
21 | 21 | ||
22 | let currentVideoId = null | 22 | let currentVideoId: string |
23 | let currentFile = null | 23 | let currentFilePath: string |
24 | 24 | ||
25 | process.on('SIGINT', async function () { | 25 | process.on('SIGINT', async function () { |
26 | console.log('Cleaning up temp files') | 26 | console.log('Cleaning up temp files') |
27 | await remove(`${currentFile}_backup`) | 27 | await remove(`${currentFilePath}_backup`) |
28 | await remove(`${dirname(currentFile)}/${currentVideoId}-transcoded.mp4`) | 28 | await remove(`${dirname(currentFilePath)}/${currentVideoId}-transcoded.mp4`) |
29 | process.exit(0) | 29 | process.exit(0) |
30 | }) | 30 | }) |
31 | 31 | ||
@@ -40,12 +40,12 @@ async function run () { | |||
40 | currentVideoId = video.id | 40 | currentVideoId = video.id |
41 | 41 | ||
42 | for (const file of video.VideoFiles) { | 42 | for (const file of video.VideoFiles) { |
43 | currentFile = getVideoFilePath(video, file) | 43 | currentFilePath = getVideoFilePath(video, file) |
44 | 44 | ||
45 | const [ videoBitrate, fps, resolution ] = await Promise.all([ | 45 | const [ videoBitrate, fps, resolution ] = await Promise.all([ |
46 | getVideoFileBitrate(currentFile), | 46 | getVideoFileBitrate(currentFilePath), |
47 | getVideoFileFPS(currentFile), | 47 | getVideoFileFPS(currentFilePath), |
48 | getVideoFileResolution(currentFile) | 48 | getVideoFileResolution(currentFilePath) |
49 | ]) | 49 | ]) |
50 | 50 | ||
51 | const maxBitrate = getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS) | 51 | const maxBitrate = getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS) |
@@ -53,25 +53,27 @@ async function run () { | |||
53 | if (isMaxBitrateExceeded) { | 53 | if (isMaxBitrateExceeded) { |
54 | console.log( | 54 | console.log( |
55 | 'Optimizing video file %s with bitrate %s kbps (max: %s kbps)', | 55 | 'Optimizing video file %s with bitrate %s kbps (max: %s kbps)', |
56 | basename(currentFile), videoBitrate / 1000, maxBitrate / 1000 | 56 | basename(currentFilePath), videoBitrate / 1000, maxBitrate / 1000 |
57 | ) | 57 | ) |
58 | 58 | ||
59 | const backupFile = `${currentFile}_backup` | 59 | const backupFile = `${currentFilePath}_backup` |
60 | await copy(currentFile, backupFile) | 60 | await copy(currentFilePath, backupFile) |
61 | 61 | ||
62 | await optimizeOriginalVideofile(video, file) | 62 | await optimizeOriginalVideofile(video, file) |
63 | // Update file path, the video filename changed | ||
64 | currentFilePath = getVideoFilePath(video, file) | ||
63 | 65 | ||
64 | const originalDuration = await getDurationFromVideoFile(backupFile) | 66 | const originalDuration = await getDurationFromVideoFile(backupFile) |
65 | const newDuration = await getDurationFromVideoFile(currentFile) | 67 | const newDuration = await getDurationFromVideoFile(currentFilePath) |
66 | 68 | ||
67 | if (originalDuration === newDuration) { | 69 | if (originalDuration === newDuration) { |
68 | console.log('Finished optimizing %s', basename(currentFile)) | 70 | console.log('Finished optimizing %s', basename(currentFilePath)) |
69 | await remove(backupFile) | 71 | await remove(backupFile) |
70 | continue | 72 | continue |
71 | } | 73 | } |
72 | 74 | ||
73 | console.log('Failed to optimize %s, restoring original', basename(currentFile)) | 75 | console.log('Failed to optimize %s, restoring original', basename(currentFilePath)) |
74 | await move(backupFile, currentFile, { overwrite: true }) | 76 | await move(backupFile, currentFilePath, { overwrite: true }) |
75 | await createTorrentAndSetInfoHash(video, file) | 77 | await createTorrentAndSetInfoHash(video, file) |
76 | await file.save() | 78 | await file.save() |
77 | } | 79 | } |
diff --git a/scripts/prune-storage.ts b/scripts/prune-storage.ts index 58d24816e..5b029d215 100755 --- a/scripts/prune-storage.ts +++ b/scripts/prune-storage.ts | |||
@@ -2,11 +2,11 @@ import { registerTSPaths } from '../server/helpers/register-ts-paths' | |||
2 | registerTSPaths() | 2 | registerTSPaths() |
3 | 3 | ||
4 | import * as prompt from 'prompt' | 4 | import * as prompt from 'prompt' |
5 | import { join } from 'path' | 5 | import { join, basename } from 'path' |
6 | import { CONFIG } from '../server/initializers/config' | 6 | import { CONFIG } from '../server/initializers/config' |
7 | import { VideoModel } from '../server/models/video/video' | 7 | import { VideoModel } from '../server/models/video/video' |
8 | import { initDatabaseModels } from '../server/initializers/database' | 8 | import { initDatabaseModels } from '../server/initializers/database' |
9 | import { readdir, remove } from 'fs-extra' | 9 | import { readdir, remove, stat } from 'fs-extra' |
10 | import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy' | 10 | import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy' |
11 | import * as Bluebird from 'bluebird' | 11 | import * as Bluebird from 'bluebird' |
12 | import { getUUIDFromFilename } from '../server/helpers/utils' | 12 | import { getUUIDFromFilename } from '../server/helpers/utils' |
@@ -14,6 +14,7 @@ import { ThumbnailModel } from '../server/models/video/thumbnail' | |||
14 | import { ActorImageModel } from '../server/models/actor/actor-image' | 14 | import { ActorImageModel } from '../server/models/actor/actor-image' |
15 | import { uniq, values } from 'lodash' | 15 | import { uniq, values } from 'lodash' |
16 | import { ThumbnailType } from '@shared/models' | 16 | import { ThumbnailType } from '@shared/models' |
17 | import { VideoFileModel } from '@server/models/video/video-file' | ||
17 | 18 | ||
18 | run() | 19 | run() |
19 | .then(() => process.exit(0)) | 20 | .then(() => process.exit(0)) |
@@ -37,8 +38,8 @@ async function run () { | |||
37 | console.log('Detecting files to remove, it could take a while...') | 38 | console.log('Detecting files to remove, it could take a while...') |
38 | 39 | ||
39 | toDelete = toDelete.concat( | 40 | toDelete = toDelete.concat( |
40 | await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesVideoExist(true)), | 41 | await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesWebTorrentFileExist()), |
41 | await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesVideoExist(true)), | 42 | await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesTorrentFileExist()), |
42 | 43 | ||
43 | await pruneDirectory(CONFIG.STORAGE.REDUNDANCY_DIR, doesRedundancyExist), | 44 | await pruneDirectory(CONFIG.STORAGE.REDUNDANCY_DIR, doesRedundancyExist), |
44 | 45 | ||
@@ -78,26 +79,27 @@ async function pruneDirectory (directory: string, existFun: ExistFun) { | |||
78 | 79 | ||
79 | const toDelete: string[] = [] | 80 | const toDelete: string[] = [] |
80 | await Bluebird.map(files, async file => { | 81 | await Bluebird.map(files, async file => { |
81 | if (await existFun(file) !== true) { | 82 | const filePath = join(directory, file) |
82 | toDelete.push(join(directory, file)) | 83 | |
84 | if (await existFun(filePath) !== true) { | ||
85 | toDelete.push(filePath) | ||
83 | } | 86 | } |
84 | }, { concurrency: 20 }) | 87 | }, { concurrency: 20 }) |
85 | 88 | ||
86 | return toDelete | 89 | return toDelete |
87 | } | 90 | } |
88 | 91 | ||
89 | function doesVideoExist (keepOnlyOwned: boolean) { | 92 | function doesWebTorrentFileExist () { |
90 | return async (file: string) => { | 93 | return (filePath: string) => VideoFileModel.doesOwnedWebTorrentVideoFileExist(basename(filePath)) |
91 | const uuid = getUUIDFromFilename(file) | 94 | } |
92 | const video = await VideoModel.load(uuid) | ||
93 | 95 | ||
94 | return video && (keepOnlyOwned === false || video.isOwned()) | 96 | function doesTorrentFileExist () { |
95 | } | 97 | return (filePath: string) => VideoFileModel.doesOwnedTorrentFileExist(basename(filePath)) |
96 | } | 98 | } |
97 | 99 | ||
98 | function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) { | 100 | function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) { |
99 | return async (file: string) => { | 101 | return async (filePath: string) => { |
100 | const thumbnail = await ThumbnailModel.loadByFilename(file, type) | 102 | const thumbnail = await ThumbnailModel.loadByFilename(basename(filePath), type) |
101 | if (!thumbnail) return false | 103 | if (!thumbnail) return false |
102 | 104 | ||
103 | if (keepOnlyOwned) { | 105 | if (keepOnlyOwned) { |
@@ -109,21 +111,20 @@ function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) { | |||
109 | } | 111 | } |
110 | } | 112 | } |
111 | 113 | ||
112 | async function doesActorImageExist (file: string) { | 114 | async function doesActorImageExist (filePath: string) { |
113 | const image = await ActorImageModel.loadByName(file) | 115 | const image = await ActorImageModel.loadByName(basename(filePath)) |
114 | 116 | ||
115 | return !!image | 117 | return !!image |
116 | } | 118 | } |
117 | 119 | ||
118 | async function doesRedundancyExist (file: string) { | 120 | async function doesRedundancyExist (filePath: string) { |
119 | const uuid = getUUIDFromFilename(file) | 121 | const isPlaylist = (await stat(filePath)).isDirectory() |
120 | const video = await VideoModel.loadWithFiles(uuid) | ||
121 | |||
122 | if (!video) return false | ||
123 | |||
124 | const isPlaylist = file.includes('.') === false | ||
125 | 122 | ||
126 | if (isPlaylist) { | 123 | if (isPlaylist) { |
124 | const uuid = getUUIDFromFilename(filePath) | ||
125 | const video = await VideoModel.loadWithFiles(uuid) | ||
126 | if (!video) return false | ||
127 | |||
127 | const p = video.getHLSPlaylist() | 128 | const p = video.getHLSPlaylist() |
128 | if (!p) return false | 129 | if (!p) return false |
129 | 130 | ||
@@ -131,19 +132,10 @@ async function doesRedundancyExist (file: string) { | |||
131 | return !!redundancy | 132 | return !!redundancy |
132 | } | 133 | } |
133 | 134 | ||
134 | const resolution = parseInt(file.split('-')[5], 10) | 135 | const file = await VideoFileModel.loadByFilename(basename(filePath)) |
135 | if (isNaN(resolution)) { | 136 | if (!file) return false |
136 | console.error('Cannot prune %s because we cannot guess guess the resolution.', file) | ||
137 | return true | ||
138 | } | ||
139 | |||
140 | const videoFile = video.getWebTorrentFile(resolution) | ||
141 | if (!videoFile) { | ||
142 | console.error('Cannot find webtorrent file of video %s - %d', video.url, resolution) | ||
143 | return true | ||
144 | } | ||
145 | 137 | ||
146 | const redundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) | 138 | const redundancy = await VideoRedundancyModel.loadLocalByFileId(file.id) |
147 | return !!redundancy | 139 | return !!redundancy |
148 | } | 140 | } |
149 | 141 | ||
diff --git a/scripts/update-host.ts b/scripts/update-host.ts index 592684225..9e8dd41ca 100755 --- a/scripts/update-host.ts +++ b/scripts/update-host.ts | |||
@@ -16,7 +16,6 @@ import { VideoShareModel } from '../server/models/video/video-share' | |||
16 | import { VideoCommentModel } from '../server/models/video/video-comment' | 16 | import { VideoCommentModel } from '../server/models/video/video-comment' |
17 | import { AccountModel } from '../server/models/account/account' | 17 | import { AccountModel } from '../server/models/account/account' |
18 | import { VideoChannelModel } from '../server/models/video/video-channel' | 18 | import { VideoChannelModel } from '../server/models/video/video-channel' |
19 | import { VideoStreamingPlaylistModel } from '../server/models/video/video-streaming-playlist' | ||
20 | import { initDatabaseModels } from '../server/initializers/database' | 19 | import { initDatabaseModels } from '../server/initializers/database' |
21 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 20 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
22 | import { getServerActor } from '@server/models/application/application' | 21 | import { getServerActor } from '@server/models/application/application' |
@@ -128,13 +127,17 @@ async function run () { | |||
128 | for (const file of video.VideoFiles) { | 127 | for (const file of video.VideoFiles) { |
129 | console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid) | 128 | console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid) |
130 | await createTorrentAndSetInfoHash(video, file) | 129 | await createTorrentAndSetInfoHash(video, file) |
130 | |||
131 | await file.save() | ||
131 | } | 132 | } |
132 | 133 | ||
133 | for (const playlist of video.VideoStreamingPlaylists) { | 134 | const playlist = video.getHLSPlaylist() |
134 | playlist.playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | 135 | for (const file of (playlist?.VideoFiles || [])) { |
135 | playlist.segmentsSha256Url = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive) | 136 | console.log('Updating fragmented torrent file %s of video %s.', file.resolution, video.uuid) |
137 | |||
138 | await createTorrentAndSetInfoHash(video, file) | ||
136 | 139 | ||
137 | await playlist.save() | 140 | await file.save() |
138 | } | 141 | } |
139 | } | 142 | } |
140 | } | 143 | } |
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index 7792ae3fc..408f677ff 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts | |||
@@ -209,10 +209,12 @@ async function addVideo (options: { | |||
209 | }) | 209 | }) |
210 | 210 | ||
211 | createTorrentFederate(video, videoFile) | 211 | createTorrentFederate(video, videoFile) |
212 | .then(() => { | ||
213 | if (video.state !== VideoState.TO_TRANSCODE) return | ||
212 | 214 | ||
213 | if (video.state === VideoState.TO_TRANSCODE) { | 215 | return addOptimizeOrMergeAudioJob(videoCreated, videoFile, user) |
214 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user) | 216 | }) |
215 | } | 217 | .catch(err => logger.error('Cannot add optimize/merge audio job for %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) })) |
216 | 218 | ||
217 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) | 219 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) |
218 | 220 | ||
@@ -259,9 +261,9 @@ async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoF | |||
259 | return refreshedFile.save() | 261 | return refreshedFile.save() |
260 | } | 262 | } |
261 | 263 | ||
262 | function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void { | 264 | function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile) { |
263 | // Create the torrent file in async way because it could be long | 265 | // Create the torrent file in async way because it could be long |
264 | createTorrentAndSetInfoHashAsync(video, videoFile) | 266 | return createTorrentAndSetInfoHashAsync(video, videoFile) |
265 | .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) })) | 267 | .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) })) |
266 | .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) | 268 | .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) |
267 | .then(refreshedVideo => { | 269 | .then(refreshedVideo => { |
diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts index cbd7aa401..422774022 100644 --- a/server/helpers/database-utils.ts +++ b/server/helpers/database-utils.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as retry from 'async/retry' | 1 | import * as retry from 'async/retry' |
2 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
3 | import { QueryTypes, Transaction } from 'sequelize' | 3 | import { BindOrReplacements, QueryTypes, Transaction } from 'sequelize' |
4 | import { Model } from 'sequelize-typescript' | 4 | import { Model } from 'sequelize-typescript' |
5 | import { sequelizeTypescript } from '@server/initializers/database' | 5 | import { sequelizeTypescript } from '@server/initializers/database' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
@@ -84,13 +84,15 @@ function resetSequelizeInstance (instance: Model<any>, savedFields: object) { | |||
84 | }) | 84 | }) |
85 | } | 85 | } |
86 | 86 | ||
87 | function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Pick<Model, 'destroy'>> ( | 87 | function filterNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean }> ( |
88 | fromDatabase: T[], | 88 | fromDatabase: T[], |
89 | newModels: T[], | 89 | newModels: T[] |
90 | t: Transaction | ||
91 | ) { | 90 | ) { |
92 | return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f))) | 91 | return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f))) |
93 | .map(f => f.destroy({ transaction: t })) | 92 | } |
93 | |||
94 | function deleteAllModels <T extends Pick<Model, 'destroy'>> (models: T[], transaction: Transaction) { | ||
95 | return Promise.all(models.map(f => f.destroy({ transaction }))) | ||
94 | } | 96 | } |
95 | 97 | ||
96 | // Sequelize always skip the update if we only update updatedAt field | 98 | // Sequelize always skip the update if we only update updatedAt field |
@@ -121,13 +123,28 @@ function afterCommitIfTransaction (t: Transaction, fn: Function) { | |||
121 | 123 | ||
122 | // --------------------------------------------------------------------------- | 124 | // --------------------------------------------------------------------------- |
123 | 125 | ||
126 | function doesExist (query: string, bind?: BindOrReplacements) { | ||
127 | const options = { | ||
128 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
129 | bind, | ||
130 | raw: true | ||
131 | } | ||
132 | |||
133 | return sequelizeTypescript.query(query, options) | ||
134 | .then(results => results.length === 1) | ||
135 | } | ||
136 | |||
137 | // --------------------------------------------------------------------------- | ||
138 | |||
124 | export { | 139 | export { |
125 | resetSequelizeInstance, | 140 | resetSequelizeInstance, |
126 | retryTransactionWrapper, | 141 | retryTransactionWrapper, |
127 | transactionRetryer, | 142 | transactionRetryer, |
128 | updateInstanceWithAnother, | 143 | updateInstanceWithAnother, |
129 | afterCommitIfTransaction, | 144 | afterCommitIfTransaction, |
130 | deleteNonExistingModels, | 145 | filterNonExistingModels, |
146 | deleteAllModels, | ||
131 | setAsUpdated, | 147 | setAsUpdated, |
132 | runInReadCommittedTransaction | 148 | runInReadCommittedTransaction, |
149 | doesExist | ||
133 | } | 150 | } |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 6f5a71b4a..9ad4b7f3b 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -212,14 +212,17 @@ async function transcode (options: TranscodeOptions) { | |||
212 | 212 | ||
213 | async function getLiveTranscodingCommand (options: { | 213 | async function getLiveTranscodingCommand (options: { |
214 | rtmpUrl: string | 214 | rtmpUrl: string |
215 | |||
215 | outPath: string | 216 | outPath: string |
217 | masterPlaylistName: string | ||
218 | |||
216 | resolutions: number[] | 219 | resolutions: number[] |
217 | fps: number | 220 | fps: number |
218 | 221 | ||
219 | availableEncoders: AvailableEncoders | 222 | availableEncoders: AvailableEncoders |
220 | profile: string | 223 | profile: string |
221 | }) { | 224 | }) { |
222 | const { rtmpUrl, outPath, resolutions, fps, availableEncoders, profile } = options | 225 | const { rtmpUrl, outPath, resolutions, fps, availableEncoders, profile, masterPlaylistName } = options |
223 | const input = rtmpUrl | 226 | const input = rtmpUrl |
224 | 227 | ||
225 | const command = getFFmpeg(input, 'live') | 228 | const command = getFFmpeg(input, 'live') |
@@ -301,14 +304,14 @@ async function getLiveTranscodingCommand (options: { | |||
301 | 304 | ||
302 | command.complexFilter(complexFilter) | 305 | command.complexFilter(complexFilter) |
303 | 306 | ||
304 | addDefaultLiveHLSParams(command, outPath) | 307 | addDefaultLiveHLSParams(command, outPath, masterPlaylistName) |
305 | 308 | ||
306 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) | 309 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) |
307 | 310 | ||
308 | return command | 311 | return command |
309 | } | 312 | } |
310 | 313 | ||
311 | function getLiveMuxingCommand (rtmpUrl: string, outPath: string) { | 314 | function getLiveMuxingCommand (rtmpUrl: string, outPath: string, masterPlaylistName: string) { |
312 | const command = getFFmpeg(rtmpUrl, 'live') | 315 | const command = getFFmpeg(rtmpUrl, 'live') |
313 | 316 | ||
314 | command.outputOption('-c:v copy') | 317 | command.outputOption('-c:v copy') |
@@ -316,7 +319,7 @@ function getLiveMuxingCommand (rtmpUrl: string, outPath: string) { | |||
316 | command.outputOption('-map 0:a?') | 319 | command.outputOption('-map 0:a?') |
317 | command.outputOption('-map 0:v?') | 320 | command.outputOption('-map 0:v?') |
318 | 321 | ||
319 | addDefaultLiveHLSParams(command, outPath) | 322 | addDefaultLiveHLSParams(command, outPath, masterPlaylistName) |
320 | 323 | ||
321 | return command | 324 | return command |
322 | } | 325 | } |
@@ -371,12 +374,12 @@ function addDefaultEncoderParams (options: { | |||
371 | } | 374 | } |
372 | } | 375 | } |
373 | 376 | ||
374 | function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string) { | 377 | function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, masterPlaylistName: string) { |
375 | command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS) | 378 | command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS) |
376 | command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) | 379 | command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) |
377 | command.outputOption('-hls_flags delete_segments+independent_segments') | 380 | command.outputOption('-hls_flags delete_segments+independent_segments') |
378 | command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`) | 381 | command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`) |
379 | command.outputOption('-master_pl_name master.m3u8') | 382 | command.outputOption('-master_pl_name ' + masterPlaylistName) |
380 | command.outputOption(`-f hls`) | 383 | command.outputOption(`-f hls`) |
381 | 384 | ||
382 | command.output(join(outPath, '%v.m3u8')) | 385 | command.output(join(outPath, '%v.m3u8')) |
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts index d8220ba9c..ecf63e93e 100644 --- a/server/helpers/webtorrent.ts +++ b/server/helpers/webtorrent.ts | |||
@@ -103,6 +103,11 @@ async function createTorrentAndSetInfoHash ( | |||
103 | 103 | ||
104 | await writeFile(torrentPath, torrent) | 104 | await writeFile(torrentPath, torrent) |
105 | 105 | ||
106 | // Remove old torrent file if it existed | ||
107 | if (videoFile.hasTorrent()) { | ||
108 | await remove(join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename)) | ||
109 | } | ||
110 | |||
106 | const parsedTorrent = parseTorrent(torrent) | 111 | const parsedTorrent = parseTorrent(torrent) |
107 | videoFile.infoHash = parsedTorrent.infoHash | 112 | videoFile.infoHash = parsedTorrent.infoHash |
108 | videoFile.torrentFilename = torrentFilename | 113 | videoFile.torrentFilename = torrentFilename |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index ab59320eb..ee4503b2c 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
24 | 24 | ||
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const LAST_MIGRATION_VERSION = 650 | 27 | const LAST_MIGRATION_VERSION = 655 |
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
diff --git a/server/initializers/migrations/0655-streaming-playlist-filenames.ts b/server/initializers/migrations/0655-streaming-playlist-filenames.ts new file mode 100644 index 000000000..9172a22c4 --- /dev/null +++ b/server/initializers/migrations/0655-streaming-playlist-filenames.ts | |||
@@ -0,0 +1,66 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | for (const column of [ 'playlistUrl', 'segmentsSha256Url' ]) { | ||
11 | const data = { | ||
12 | type: Sequelize.STRING, | ||
13 | allowNull: true, | ||
14 | defaultValue: null | ||
15 | } | ||
16 | |||
17 | await utils.queryInterface.changeColumn('videoStreamingPlaylist', column, data) | ||
18 | } | ||
19 | } | ||
20 | |||
21 | { | ||
22 | await utils.sequelize.query( | ||
23 | `UPDATE "videoStreamingPlaylist" SET "playlistUrl" = NULL, "segmentsSha256Url" = NULL ` + | ||
24 | `WHERE "videoId" IN (SELECT id FROM video WHERE remote IS FALSE)` | ||
25 | ) | ||
26 | } | ||
27 | |||
28 | { | ||
29 | for (const column of [ 'playlistFilename', 'segmentsSha256Filename' ]) { | ||
30 | const data = { | ||
31 | type: Sequelize.STRING, | ||
32 | allowNull: true, | ||
33 | defaultValue: null | ||
34 | } | ||
35 | |||
36 | await utils.queryInterface.addColumn('videoStreamingPlaylist', column, data) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | { | ||
41 | await utils.sequelize.query( | ||
42 | `UPDATE "videoStreamingPlaylist" SET "playlistFilename" = 'master.m3u8', "segmentsSha256Filename" = 'segments-sha256.json'` | ||
43 | ) | ||
44 | } | ||
45 | |||
46 | { | ||
47 | for (const column of [ 'playlistFilename', 'segmentsSha256Filename' ]) { | ||
48 | const data = { | ||
49 | type: Sequelize.STRING, | ||
50 | allowNull: false, | ||
51 | defaultValue: null | ||
52 | } | ||
53 | |||
54 | await utils.queryInterface.changeColumn('videoStreamingPlaylist', column, data) | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | |||
59 | function down (options) { | ||
60 | throw new Error('Not implemented.') | ||
61 | } | ||
62 | |||
63 | export { | ||
64 | up, | ||
65 | down | ||
66 | } | ||
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts index e89c94bcd..f995fe637 100644 --- a/server/lib/activitypub/videos/shared/abstract-builder.ts +++ b/server/lib/activitypub/videos/shared/abstract-builder.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Transaction } from 'sequelize/types' | 1 | import { Transaction } from 'sequelize/types' |
2 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | 2 | import { checkUrlsSameHost } from '@server/helpers/activitypub' |
3 | import { deleteNonExistingModels } from '@server/helpers/database-utils' | 3 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' |
4 | import { logger, LoggerTagsFn } from '@server/helpers/logger' | 4 | import { logger, LoggerTagsFn } from '@server/helpers/logger' |
5 | import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' | 5 | import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' |
6 | import { setVideoTags } from '@server/lib/video' | 6 | import { setVideoTags } from '@server/lib/video' |
@@ -111,8 +111,7 @@ export abstract class APVideoAbstractBuilder { | |||
111 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | 111 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) |
112 | 112 | ||
113 | // Remove video files that do not exist anymore | 113 | // Remove video files that do not exist anymore |
114 | const destroyTasks = deleteNonExistingModels(video.VideoFiles || [], newVideoFiles, t) | 114 | await deleteAllModels(filterNonExistingModels(video.VideoFiles || [], newVideoFiles), t) |
115 | await Promise.all(destroyTasks) | ||
116 | 115 | ||
117 | // Update or add other one | 116 | // Update or add other one |
118 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t)) | 117 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t)) |
@@ -124,13 +123,11 @@ export abstract class APVideoAbstractBuilder { | |||
124 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | 123 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) |
125 | 124 | ||
126 | // Remove video playlists that do not exist anymore | 125 | // Remove video playlists that do not exist anymore |
127 | const destroyTasks = deleteNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists, t) | 126 | await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t) |
128 | await Promise.all(destroyTasks) | ||
129 | 127 | ||
130 | video.VideoStreamingPlaylists = [] | 128 | video.VideoStreamingPlaylists = [] |
131 | 129 | ||
132 | for (const playlistAttributes of streamingPlaylistAttributes) { | 130 | for (const playlistAttributes of streamingPlaylistAttributes) { |
133 | |||
134 | const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t) | 131 | const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t) |
135 | streamingPlaylistModel.Video = video | 132 | streamingPlaylistModel.Video = video |
136 | 133 | ||
@@ -163,8 +160,7 @@ export abstract class APVideoAbstractBuilder { | |||
163 | 160 | ||
164 | const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a)) | 161 | const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a)) |
165 | 162 | ||
166 | const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t) | 163 | await deleteAllModels(filterNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles), t) |
167 | await Promise.all(destroyTasks) | ||
168 | 164 | ||
169 | // Update or add other one | 165 | // Update or add other one |
170 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t)) | 166 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t)) |
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts index 85548428c..1fa16295d 100644 --- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts | |||
@@ -7,10 +7,11 @@ import { logger } from '@server/helpers/logger' | |||
7 | import { getExtFromMimetype } from '@server/helpers/video' | 7 | import { getExtFromMimetype } from '@server/helpers/video' |
8 | import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants' | 8 | import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants' |
9 | import { generateTorrentFileName } from '@server/lib/video-paths' | 9 | import { generateTorrentFileName } from '@server/lib/video-paths' |
10 | import { VideoCaptionModel } from '@server/models/video/video-caption' | ||
10 | import { VideoFileModel } from '@server/models/video/video-file' | 11 | import { VideoFileModel } from '@server/models/video/video-file' |
11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 12 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
12 | import { FilteredModelAttributes } from '@server/types' | 13 | import { FilteredModelAttributes } from '@server/types' |
13 | import { MChannelId, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models' | 14 | import { isStreamingPlaylist, MChannelId, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models' |
14 | import { | 15 | import { |
15 | ActivityHashTagObject, | 16 | ActivityHashTagObject, |
16 | ActivityMagnetUrlObject, | 17 | ActivityMagnetUrlObject, |
@@ -23,7 +24,6 @@ import { | |||
23 | VideoPrivacy, | 24 | VideoPrivacy, |
24 | VideoStreamingPlaylistType | 25 | VideoStreamingPlaylistType |
25 | } from '@shared/models' | 26 | } from '@shared/models' |
26 | import { VideoCaptionModel } from '@server/models/video/video-caption' | ||
27 | 27 | ||
28 | function getThumbnailFromIcons (videoObject: VideoObject) { | 28 | function getThumbnailFromIcons (videoObject: VideoObject) { |
29 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) | 29 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) |
@@ -80,8 +80,8 @@ function getFileAttributesFromUrl ( | |||
80 | 80 | ||
81 | const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType) | 81 | const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType) |
82 | const resolution = fileUrl.height | 82 | const resolution = fileUrl.height |
83 | const videoId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id | 83 | const videoId = isStreamingPlaylist(videoOrPlaylist) ? null : videoOrPlaylist.id |
84 | const videoStreamingPlaylistId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null | 84 | const videoStreamingPlaylistId = isStreamingPlaylist(videoOrPlaylist) ? videoOrPlaylist.id : null |
85 | 85 | ||
86 | const attribute = { | 86 | const attribute = { |
87 | extname, | 87 | extname, |
@@ -130,8 +130,13 @@ function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject: | |||
130 | 130 | ||
131 | const attribute = { | 131 | const attribute = { |
132 | type: VideoStreamingPlaylistType.HLS, | 132 | type: VideoStreamingPlaylistType.HLS, |
133 | |||
134 | playlistFilename: basename(playlistUrlObject.href), | ||
133 | playlistUrl: playlistUrlObject.href, | 135 | playlistUrl: playlistUrlObject.href, |
136 | |||
137 | segmentsSha256Filename: basename(segmentsSha256UrlObject.href), | ||
134 | segmentsSha256Url: segmentsSha256UrlObject.href, | 138 | segmentsSha256Url: segmentsSha256UrlObject.href, |
139 | |||
135 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files), | 140 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files), |
136 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | 141 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, |
137 | videoId: video.id, | 142 | videoId: video.id, |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 212bd095b..32b02bc26 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' | 1 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' |
2 | import { flatten, uniq } from 'lodash' | 2 | import { flatten, uniq } from 'lodash' |
3 | import { basename, dirname, join } from 'path' | 3 | import { basename, dirname, join } from 'path' |
4 | import { MVideoWithFile } from '@server/types/models' | 4 | import { MStreamingPlaylistFilesVideo, MVideoWithFile } from '@server/types/models' |
5 | import { sha256 } from '../helpers/core-utils' | 5 | import { sha256 } from '../helpers/core-utils' |
6 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils' | 6 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils' |
7 | import { logger } from '../helpers/logger' | 7 | import { logger } from '../helpers/logger' |
@@ -12,7 +12,7 @@ import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from | |||
12 | import { sequelizeTypescript } from '../initializers/database' | 12 | import { sequelizeTypescript } from '../initializers/database' |
13 | import { VideoFileModel } from '../models/video/video-file' | 13 | import { VideoFileModel } from '../models/video/video-file' |
14 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 14 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
15 | import { getVideoFilePath } from './video-paths' | 15 | import { getHlsResolutionPlaylistFilename, getVideoFilePath } from './video-paths' |
16 | 16 | ||
17 | async function updateStreamingPlaylistsInfohashesIfNeeded () { | 17 | async function updateStreamingPlaylistsInfohashesIfNeeded () { |
18 | const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() | 18 | const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() |
@@ -22,27 +22,29 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () { | |||
22 | await sequelizeTypescript.transaction(async t => { | 22 | await sequelizeTypescript.transaction(async t => { |
23 | const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t) | 23 | const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t) |
24 | 24 | ||
25 | playlist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles) | 25 | playlist.assignP2PMediaLoaderInfoHashes(playlist.Video, videoFiles) |
26 | playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION | 26 | playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION |
27 | |||
27 | await playlist.save({ transaction: t }) | 28 | await playlist.save({ transaction: t }) |
28 | }) | 29 | }) |
29 | } | 30 | } |
30 | } | 31 | } |
31 | 32 | ||
32 | async function updateMasterHLSPlaylist (video: MVideoWithFile) { | 33 | async function updateMasterHLSPlaylist (video: MVideoWithFile, playlist: MStreamingPlaylistFilesVideo) { |
33 | const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 34 | const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) |
35 | |||
34 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] | 36 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] |
35 | const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
36 | const streamingPlaylist = video.getHLSPlaylist() | ||
37 | 37 | ||
38 | for (const file of streamingPlaylist.VideoFiles) { | 38 | const masterPlaylistPath = join(directory, playlist.playlistFilename) |
39 | const playlistFilename = VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution) | 39 | |
40 | for (const file of playlist.VideoFiles) { | ||
41 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) | ||
40 | 42 | ||
41 | // If we did not generated a playlist for this resolution, skip | 43 | // If we did not generated a playlist for this resolution, skip |
42 | const filePlaylistPath = join(directory, playlistFilename) | 44 | const filePlaylistPath = join(directory, playlistFilename) |
43 | if (await pathExists(filePlaylistPath) === false) continue | 45 | if (await pathExists(filePlaylistPath) === false) continue |
44 | 46 | ||
45 | const videoFilePath = getVideoFilePath(streamingPlaylist, file) | 47 | const videoFilePath = getVideoFilePath(playlist, file) |
46 | 48 | ||
47 | const size = await getVideoStreamSize(videoFilePath) | 49 | const size = await getVideoStreamSize(videoFilePath) |
48 | 50 | ||
@@ -66,23 +68,22 @@ async function updateMasterHLSPlaylist (video: MVideoWithFile) { | |||
66 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') | 68 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') |
67 | } | 69 | } |
68 | 70 | ||
69 | async function updateSha256VODSegments (video: MVideoWithFile) { | 71 | async function updateSha256VODSegments (video: MVideoWithFile, playlist: MStreamingPlaylistFilesVideo) { |
70 | const json: { [filename: string]: { [range: string]: string } } = {} | 72 | const json: { [filename: string]: { [range: string]: string } } = {} |
71 | 73 | ||
72 | const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 74 | const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) |
73 | const hlsPlaylist = video.getHLSPlaylist() | ||
74 | 75 | ||
75 | // For all the resolutions available for this video | 76 | // For all the resolutions available for this video |
76 | for (const file of hlsPlaylist.VideoFiles) { | 77 | for (const file of playlist.VideoFiles) { |
77 | const rangeHashes: { [range: string]: string } = {} | 78 | const rangeHashes: { [range: string]: string } = {} |
78 | 79 | ||
79 | const videoPath = getVideoFilePath(hlsPlaylist, file) | 80 | const videoPath = getVideoFilePath(playlist, file) |
80 | const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | 81 | const resolutionPlaylistPath = join(playlistDirectory, getHlsResolutionPlaylistFilename(file.filename)) |
81 | 82 | ||
82 | // Maybe the playlist is not generated for this resolution yet | 83 | // Maybe the playlist is not generated for this resolution yet |
83 | if (!await pathExists(playlistPath)) continue | 84 | if (!await pathExists(resolutionPlaylistPath)) continue |
84 | 85 | ||
85 | const playlistContent = await readFile(playlistPath) | 86 | const playlistContent = await readFile(resolutionPlaylistPath) |
86 | const ranges = getRangesFromPlaylist(playlistContent.toString()) | 87 | const ranges = getRangesFromPlaylist(playlistContent.toString()) |
87 | 88 | ||
88 | const fd = await open(videoPath, 'r') | 89 | const fd = await open(videoPath, 'r') |
@@ -98,7 +99,7 @@ async function updateSha256VODSegments (video: MVideoWithFile) { | |||
98 | json[videoFilename] = rangeHashes | 99 | json[videoFilename] = rangeHashes |
99 | } | 100 | } |
100 | 101 | ||
101 | const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | 102 | const outputPath = join(playlistDirectory, playlist.segmentsSha256Filename) |
102 | await outputJSON(outputPath, json) | 103 | await outputJSON(outputPath, json) |
103 | } | 104 | } |
104 | 105 | ||
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 1783f206a..4d199f247 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -61,8 +61,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | |||
61 | 61 | ||
62 | if (currentVideoFile) { | 62 | if (currentVideoFile) { |
63 | // Remove old file and old torrent | 63 | // Remove old file and old torrent |
64 | await video.removeFile(currentVideoFile) | 64 | await video.removeFileAndTorrent(currentVideoFile) |
65 | await currentVideoFile.removeTorrent() | ||
66 | // Remove the old video file from the array | 65 | // Remove the old video file from the array |
67 | video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) | 66 | video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) |
68 | 67 | ||
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 9eba41bf8..386ccdc7b 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -7,12 +7,12 @@ import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server | |||
7 | import { generateVideoMiniature } from '@server/lib/thumbnail' | 7 | import { generateVideoMiniature } from '@server/lib/thumbnail' |
8 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' | 8 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' |
9 | import { publishAndFederateIfNeeded } from '@server/lib/video' | 9 | import { publishAndFederateIfNeeded } from '@server/lib/video' |
10 | import { getHLSDirectory } from '@server/lib/video-paths' | 10 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHLSDirectory } from '@server/lib/video-paths' |
11 | import { VideoModel } from '@server/models/video/video' | 11 | import { VideoModel } from '@server/models/video/video' |
12 | import { VideoFileModel } from '@server/models/video/video-file' | 12 | import { VideoFileModel } from '@server/models/video/video-file' |
13 | import { VideoLiveModel } from '@server/models/video/video-live' | 13 | import { VideoLiveModel } from '@server/models/video/video-live' |
14 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 14 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
15 | import { MVideo, MVideoLive } from '@server/types/models' | 15 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' |
16 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | 16 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
17 | import { logger } from '../../../helpers/logger' | 17 | import { logger } from '../../../helpers/logger' |
18 | 18 | ||
@@ -43,7 +43,7 @@ async function processVideoLiveEnding (job: Bull.Job) { | |||
43 | return cleanupLive(video, streamingPlaylist) | 43 | return cleanupLive(video, streamingPlaylist) |
44 | } | 44 | } |
45 | 45 | ||
46 | return saveLive(video, live) | 46 | return saveLive(video, live, streamingPlaylist) |
47 | } | 47 | } |
48 | 48 | ||
49 | // --------------------------------------------------------------------------- | 49 | // --------------------------------------------------------------------------- |
@@ -54,14 +54,14 @@ export { | |||
54 | 54 | ||
55 | // --------------------------------------------------------------------------- | 55 | // --------------------------------------------------------------------------- |
56 | 56 | ||
57 | async function saveLive (video: MVideo, live: MVideoLive) { | 57 | async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MStreamingPlaylist) { |
58 | const hlsDirectory = getHLSDirectory(video, false) | 58 | const hlsDirectory = getHLSDirectory(video, false) |
59 | const replayDirectory = join(hlsDirectory, VIDEO_LIVE.REPLAY_DIRECTORY) | 59 | const replayDirectory = join(hlsDirectory, VIDEO_LIVE.REPLAY_DIRECTORY) |
60 | 60 | ||
61 | const rootFiles = await readdir(hlsDirectory) | 61 | const rootFiles = await readdir(hlsDirectory) |
62 | 62 | ||
63 | const playlistFiles = rootFiles.filter(file => { | 63 | const playlistFiles = rootFiles.filter(file => { |
64 | return file.endsWith('.m3u8') && file !== 'master.m3u8' | 64 | return file.endsWith('.m3u8') && file !== streamingPlaylist.playlistFilename |
65 | }) | 65 | }) |
66 | 66 | ||
67 | await cleanupLiveFiles(hlsDirectory) | 67 | await cleanupLiveFiles(hlsDirectory) |
@@ -80,7 +80,12 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
80 | 80 | ||
81 | const hlsPlaylist = videoWithFiles.getHLSPlaylist() | 81 | const hlsPlaylist = videoWithFiles.getHLSPlaylist() |
82 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) | 82 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) |
83 | |||
84 | // Reset playlist | ||
83 | hlsPlaylist.VideoFiles = [] | 85 | hlsPlaylist.VideoFiles = [] |
86 | hlsPlaylist.playlistFilename = generateHLSMasterPlaylistFilename() | ||
87 | hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename() | ||
88 | await hlsPlaylist.save() | ||
84 | 89 | ||
85 | let durationDone = false | 90 | let durationDone = false |
86 | 91 | ||
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index f5ba6f435..36d9594af 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -125,8 +125,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay | |||
125 | if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { | 125 | if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { |
126 | // Remove webtorrent files if not enabled | 126 | // Remove webtorrent files if not enabled |
127 | for (const file of video.VideoFiles) { | 127 | for (const file of video.VideoFiles) { |
128 | await video.removeFile(file) | 128 | await video.removeFileAndTorrent(file) |
129 | await file.removeTorrent() | ||
130 | await file.destroy() | 129 | await file.destroy() |
131 | } | 130 | } |
132 | 131 | ||
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index da764e009..f106d69fb 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts | |||
@@ -4,16 +4,17 @@ import { isTestInstance } from '@server/helpers/core-utils' | |||
4 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | 4 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' |
5 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 5 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
6 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | 6 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' |
7 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants' | 7 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME } from '@server/initializers/constants' |
8 | import { UserModel } from '@server/models/user/user' | 8 | import { UserModel } from '@server/models/user/user' |
9 | import { VideoModel } from '@server/models/video/video' | 9 | import { VideoModel } from '@server/models/video/video' |
10 | import { VideoLiveModel } from '@server/models/video/video-live' | 10 | import { VideoLiveModel } from '@server/models/video/video-live' |
11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
12 | import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models' | 12 | import { MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models' |
13 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' | 13 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' |
14 | import { federateVideoIfNeeded } from '../activitypub/videos' | 14 | import { federateVideoIfNeeded } from '../activitypub/videos' |
15 | import { JobQueue } from '../job-queue' | 15 | import { JobQueue } from '../job-queue' |
16 | import { PeerTubeSocket } from '../peertube-socket' | 16 | import { PeerTubeSocket } from '../peertube-socket' |
17 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../video-paths' | ||
17 | import { LiveQuotaStore } from './live-quota-store' | 18 | import { LiveQuotaStore } from './live-quota-store' |
18 | import { LiveSegmentShaStore } from './live-segment-sha-store' | 19 | import { LiveSegmentShaStore } from './live-segment-sha-store' |
19 | import { cleanupLive } from './live-utils' | 20 | import { cleanupLive } from './live-utils' |
@@ -392,19 +393,18 @@ class LiveManager { | |||
392 | return resolutionsEnabled.concat([ originResolution ]) | 393 | return resolutionsEnabled.concat([ originResolution ]) |
393 | } | 394 | } |
394 | 395 | ||
395 | private async createLivePlaylist (video: MVideo, allResolutions: number[]) { | 396 | private async createLivePlaylist (video: MVideo, allResolutions: number[]): Promise<MStreamingPlaylistVideo> { |
396 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | 397 | const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video) |
397 | const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({ | ||
398 | videoId: video.id, | ||
399 | playlistUrl, | ||
400 | segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive), | ||
401 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, allResolutions), | ||
402 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | ||
403 | 398 | ||
404 | type: VideoStreamingPlaylistType.HLS | 399 | playlist.playlistFilename = generateHLSMasterPlaylistFilename(true) |
405 | }, { returning: true }) as [ MStreamingPlaylist, boolean ] | 400 | playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(true) |
406 | 401 | ||
407 | return Object.assign(videoStreamingPlaylist, { Video: video }) | 402 | playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION |
403 | playlist.type = VideoStreamingPlaylistType.HLS | ||
404 | |||
405 | playlist.assignP2PMediaLoaderInfoHashes(video, allResolutions) | ||
406 | |||
407 | return playlist.save() | ||
408 | } | 408 | } |
409 | 409 | ||
410 | static get Instance () { | 410 | static get Instance () { |
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts index 26467f060..709d6c615 100644 --- a/server/lib/live/shared/muxing-session.ts +++ b/server/lib/live/shared/muxing-session.ts | |||
@@ -112,13 +112,16 @@ class MuxingSession extends EventEmitter { | |||
112 | this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED | 112 | this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED |
113 | ? await getLiveTranscodingCommand({ | 113 | ? await getLiveTranscodingCommand({ |
114 | rtmpUrl: this.rtmpUrl, | 114 | rtmpUrl: this.rtmpUrl, |
115 | |||
115 | outPath, | 116 | outPath, |
117 | masterPlaylistName: this.streamingPlaylist.playlistFilename, | ||
118 | |||
116 | resolutions: this.allResolutions, | 119 | resolutions: this.allResolutions, |
117 | fps: this.fps, | 120 | fps: this.fps, |
118 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 121 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
119 | profile: CONFIG.LIVE.TRANSCODING.PROFILE | 122 | profile: CONFIG.LIVE.TRANSCODING.PROFILE |
120 | }) | 123 | }) |
121 | : getLiveMuxingCommand(this.rtmpUrl, outPath) | 124 | : getLiveMuxingCommand(this.rtmpUrl, outPath, this.streamingPlaylist.playlistFilename) |
122 | 125 | ||
123 | logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags) | 126 | logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags) |
124 | 127 | ||
@@ -182,7 +185,7 @@ class MuxingSession extends EventEmitter { | |||
182 | } | 185 | } |
183 | 186 | ||
184 | private watchMasterFile (outPath: string) { | 187 | private watchMasterFile (outPath: string) { |
185 | this.masterWatcher = chokidar.watch(outPath + '/master.m3u8') | 188 | this.masterWatcher = chokidar.watch(outPath + '/' + this.streamingPlaylist.playlistFilename) |
186 | 189 | ||
187 | this.masterWatcher.on('add', async () => { | 190 | this.masterWatcher.on('add', async () => { |
188 | this.emit('master-playlist-created', { videoId: this.videoId }) | 191 | this.emit('master-playlist-created', { videoId: this.videoId }) |
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index b5a5eb697..103ab1fab 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -267,7 +267,8 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
267 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy) | 267 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy) |
268 | 268 | ||
269 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) | 269 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) |
270 | await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) | 270 | const masterPlaylistUrl = playlist.getMasterPlaylistUrl(video) |
271 | await downloadPlaylistSegments(masterPlaylistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) | ||
271 | 272 | ||
272 | const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ | 273 | const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ |
273 | expiresOn, | 274 | expiresOn, |
@@ -282,7 +283,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
282 | 283 | ||
283 | await sendCreateCacheFile(serverActor, video, createdModel) | 284 | await sendCreateCacheFile(serverActor, video, createdModel) |
284 | 285 | ||
285 | logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url) | 286 | logger.info('Duplicated playlist %s -> %s.', masterPlaylistUrl, createdModel.url) |
286 | } | 287 | } |
287 | 288 | ||
288 | private async extendsExpirationOf (redundancy: MVideoRedundancyVideo, expiresAfterMs: number) { | 289 | private async extendsExpirationOf (redundancy: MVideoRedundancyVideo, expiresAfterMs: number) { |
@@ -330,7 +331,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
330 | private buildEntryLogId (object: MVideoRedundancyFileVideo | MVideoRedundancyStreamingPlaylistVideo) { | 331 | private buildEntryLogId (object: MVideoRedundancyFileVideo | MVideoRedundancyStreamingPlaylistVideo) { |
331 | if (isMVideoRedundancyFileVideo(object)) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` | 332 | if (isMVideoRedundancyFileVideo(object)) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` |
332 | 333 | ||
333 | return `${object.VideoStreamingPlaylist.playlistUrl}` | 334 | return `${object.VideoStreamingPlaylist.getMasterPlaylistUrl(object.VideoStreamingPlaylist.Video)}` |
334 | } | 335 | } |
335 | 336 | ||
336 | private getTotalFileSizes (files: MVideoFile[], playlists: MStreamingPlaylistFiles[]) { | 337 | private getTotalFileSizes (files: MVideoFile[], playlists: MStreamingPlaylistFiles[]) { |
diff --git a/server/lib/transcoding/video-transcoding.ts b/server/lib/transcoding/video-transcoding.ts index d70f7f474..d2a556360 100644 --- a/server/lib/transcoding/video-transcoding.ts +++ b/server/lib/transcoding/video-transcoding.ts | |||
@@ -10,11 +10,18 @@ import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers | |||
10 | import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' | 10 | import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' |
11 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
12 | import { CONFIG } from '../../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
13 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../../initializers/constants' | 13 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' |
14 | import { VideoFileModel } from '../../models/video/video-file' | 14 | import { VideoFileModel } from '../../models/video/video-file' |
15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | 15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' |
16 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' | 16 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' |
17 | import { generateHLSVideoFilename, generateWebTorrentVideoFilename, getVideoFilePath } from '../video-paths' | 17 | import { |
18 | generateHLSMasterPlaylistFilename, | ||
19 | generateHlsSha256SegmentsFilename, | ||
20 | generateHLSVideoFilename, | ||
21 | generateWebTorrentVideoFilename, | ||
22 | getHlsResolutionPlaylistFilename, | ||
23 | getVideoFilePath | ||
24 | } from '../video-paths' | ||
18 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' | 25 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' |
19 | 26 | ||
20 | /** | 27 | /** |
@@ -272,14 +279,14 @@ async function generateHlsPlaylistCommon (options: { | |||
272 | await ensureDir(videoTranscodedBasePath) | 279 | await ensureDir(videoTranscodedBasePath) |
273 | 280 | ||
274 | const videoFilename = generateHLSVideoFilename(resolution) | 281 | const videoFilename = generateHLSVideoFilename(resolution) |
275 | const playlistFilename = VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution) | 282 | const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename) |
276 | const playlistFileTranscodePath = join(videoTranscodedBasePath, playlistFilename) | 283 | const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename) |
277 | 284 | ||
278 | const transcodeOptions = { | 285 | const transcodeOptions = { |
279 | type, | 286 | type, |
280 | 287 | ||
281 | inputPath, | 288 | inputPath, |
282 | outputPath: playlistFileTranscodePath, | 289 | outputPath: resolutionPlaylistFileTranscodePath, |
283 | 290 | ||
284 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 291 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
285 | profile: CONFIG.TRANSCODING.PROFILE, | 292 | profile: CONFIG.TRANSCODING.PROFILE, |
@@ -299,19 +306,23 @@ async function generateHlsPlaylistCommon (options: { | |||
299 | 306 | ||
300 | await transcode(transcodeOptions) | 307 | await transcode(transcodeOptions) |
301 | 308 | ||
302 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | ||
303 | |||
304 | // Create or update the playlist | 309 | // Create or update the playlist |
305 | const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({ | 310 | const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video) |
306 | videoId: video.id, | 311 | |
307 | playlistUrl, | 312 | if (!playlist.playlistFilename) { |
308 | segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive), | 313 | playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive) |
309 | p2pMediaLoaderInfohashes: [], | 314 | } |
310 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | 315 | |
316 | if (!playlist.segmentsSha256Filename) { | ||
317 | playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive) | ||
318 | } | ||
319 | |||
320 | playlist.p2pMediaLoaderInfohashes = [] | ||
321 | playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION | ||
311 | 322 | ||
312 | type: VideoStreamingPlaylistType.HLS | 323 | playlist.type = VideoStreamingPlaylistType.HLS |
313 | }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ] | 324 | |
314 | videoStreamingPlaylist.Video = video | 325 | await playlist.save() |
315 | 326 | ||
316 | // Build the new playlist file | 327 | // Build the new playlist file |
317 | const extname = extnameUtil(videoFilename) | 328 | const extname = extnameUtil(videoFilename) |
@@ -321,18 +332,18 @@ async function generateHlsPlaylistCommon (options: { | |||
321 | size: 0, | 332 | size: 0, |
322 | filename: videoFilename, | 333 | filename: videoFilename, |
323 | fps: -1, | 334 | fps: -1, |
324 | videoStreamingPlaylistId: videoStreamingPlaylist.id | 335 | videoStreamingPlaylistId: playlist.id |
325 | }) | 336 | }) |
326 | 337 | ||
327 | const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile) | 338 | const videoFilePath = getVideoFilePath(playlist, newVideoFile) |
328 | 339 | ||
329 | // Move files from tmp transcoded directory to the appropriate place | 340 | // Move files from tmp transcoded directory to the appropriate place |
330 | const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 341 | const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) |
331 | await ensureDir(baseHlsDirectory) | 342 | await ensureDir(baseHlsDirectory) |
332 | 343 | ||
333 | // Move playlist file | 344 | // Move playlist file |
334 | const playlistPath = join(baseHlsDirectory, playlistFilename) | 345 | const resolutionPlaylistPath = join(baseHlsDirectory, resolutionPlaylistFilename) |
335 | await move(playlistFileTranscodePath, playlistPath, { overwrite: true }) | 346 | await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) |
336 | // Move video file | 347 | // Move video file |
337 | await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) | 348 | await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) |
338 | 349 | ||
@@ -342,20 +353,20 @@ async function generateHlsPlaylistCommon (options: { | |||
342 | newVideoFile.fps = await getVideoFileFPS(videoFilePath) | 353 | newVideoFile.fps = await getVideoFileFPS(videoFilePath) |
343 | newVideoFile.metadata = await getMetadataFromFile(videoFilePath) | 354 | newVideoFile.metadata = await getMetadataFromFile(videoFilePath) |
344 | 355 | ||
345 | await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile) | 356 | await createTorrentAndSetInfoHash(playlist, newVideoFile) |
346 | 357 | ||
347 | await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) | 358 | await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) |
348 | videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') | ||
349 | 359 | ||
350 | videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes( | 360 | const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo |
351 | playlistUrl, videoStreamingPlaylist.VideoFiles | 361 | playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles') |
352 | ) | 362 | playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles) |
353 | await videoStreamingPlaylist.save() | 363 | |
364 | await playlist.save() | ||
354 | 365 | ||
355 | video.setHLSPlaylist(videoStreamingPlaylist) | 366 | video.setHLSPlaylist(playlist) |
356 | 367 | ||
357 | await updateMasterHLSPlaylist(video) | 368 | await updateMasterHLSPlaylist(video, playlistWithFiles) |
358 | await updateSha256VODSegments(video) | 369 | await updateSha256VODSegments(video, playlistWithFiles) |
359 | 370 | ||
360 | return playlistPath | 371 | return resolutionPlaylistPath |
361 | } | 372 | } |
diff --git a/server/lib/video-paths.ts b/server/lib/video-paths.ts index b7068190c..1e4382108 100644 --- a/server/lib/video-paths.ts +++ b/server/lib/video-paths.ts | |||
@@ -4,19 +4,16 @@ import { CONFIG } from '@server/initializers/config' | |||
4 | import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants' | 4 | import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants' |
5 | import { isStreamingPlaylist, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' | 5 | import { isStreamingPlaylist, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' |
6 | import { buildUUID } from '@server/helpers/uuid' | 6 | import { buildUUID } from '@server/helpers/uuid' |
7 | import { removeFragmentedMP4Ext } from '@shared/core-utils' | ||
7 | 8 | ||
8 | // ################## Video file name ################## | 9 | // ################## Video file name ################## |
9 | 10 | ||
10 | function generateWebTorrentVideoFilename (resolution: number, extname: string) { | 11 | function generateWebTorrentVideoFilename (resolution: number, extname: string) { |
11 | const uuid = buildUUID() | 12 | return buildUUID() + '-' + resolution + extname |
12 | |||
13 | return uuid + '-' + resolution + extname | ||
14 | } | 13 | } |
15 | 14 | ||
16 | function generateHLSVideoFilename (resolution: number) { | 15 | function generateHLSVideoFilename (resolution: number) { |
17 | const uuid = buildUUID() | 16 | return `${buildUUID()}-${resolution}-fragmented.mp4` |
18 | |||
19 | return `${uuid}-${resolution}-fragmented.mp4` | ||
20 | } | 17 | } |
21 | 18 | ||
22 | function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) { | 19 | function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) { |
@@ -54,6 +51,23 @@ function getHLSDirectory (video: MVideoUUID, isRedundancy = false) { | |||
54 | return join(baseDir, video.uuid) | 51 | return join(baseDir, video.uuid) |
55 | } | 52 | } |
56 | 53 | ||
54 | function getHlsResolutionPlaylistFilename (videoFilename: string) { | ||
55 | // Video file name already contain resolution | ||
56 | return removeFragmentedMP4Ext(videoFilename) + '.m3u8' | ||
57 | } | ||
58 | |||
59 | function generateHLSMasterPlaylistFilename (isLive = false) { | ||
60 | if (isLive) return 'master.m3u8' | ||
61 | |||
62 | return buildUUID() + '-master.m3u8' | ||
63 | } | ||
64 | |||
65 | function generateHlsSha256SegmentsFilename (isLive = false) { | ||
66 | if (isLive) return 'segments-sha256.json' | ||
67 | |||
68 | return buildUUID() + '-segments-sha256.json' | ||
69 | } | ||
70 | |||
57 | // ################## Torrents ################## | 71 | // ################## Torrents ################## |
58 | 72 | ||
59 | function generateTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, resolution: number) { | 73 | function generateTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, resolution: number) { |
@@ -91,6 +105,9 @@ export { | |||
91 | getTorrentFilePath, | 105 | getTorrentFilePath, |
92 | 106 | ||
93 | getHLSDirectory, | 107 | getHLSDirectory, |
108 | generateHLSMasterPlaylistFilename, | ||
109 | generateHlsSha256SegmentsFilename, | ||
110 | getHlsResolutionPlaylistFilename, | ||
94 | 111 | ||
95 | getLocalVideoFileMetadataUrl, | 112 | getLocalVideoFileMetadataUrl, |
96 | 113 | ||
diff --git a/server/lib/video.ts b/server/lib/video.ts index daf998704..61fee4949 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -5,7 +5,7 @@ import { sequelizeTypescript } from '@server/initializers/database' | |||
5 | import { TagModel } from '@server/models/video/tag' | 5 | import { TagModel } from '@server/models/video/tag' |
6 | import { VideoModel } from '@server/models/video/video' | 6 | import { VideoModel } from '@server/models/video/video' |
7 | import { FilteredModelAttributes } from '@server/types' | 7 | import { FilteredModelAttributes } from '@server/types' |
8 | import { MThumbnail, MUserId, MVideo, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' | 8 | import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' |
9 | import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models' | 9 | import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models' |
10 | import { federateVideoIfNeeded } from './activitypub/videos' | 10 | import { federateVideoIfNeeded } from './activitypub/videos' |
11 | import { JobQueue } from './job-queue/job-queue' | 11 | import { JobQueue } from './job-queue/job-queue' |
@@ -105,7 +105,7 @@ async function publishAndFederateIfNeeded (video: MVideoUUID, wasLive = false) { | |||
105 | } | 105 | } |
106 | } | 106 | } |
107 | 107 | ||
108 | async function addOptimizeOrMergeAudioJob (video: MVideo, videoFile: MVideoFile, user: MUserId) { | 108 | async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId) { |
109 | let dataInput: VideoTranscodingPayload | 109 | let dataInput: VideoTranscodingPayload |
110 | 110 | ||
111 | if (videoFile.isAudio()) { | 111 | if (videoFile.isAudio()) { |
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 83c00a22d..3080e02a6 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -19,8 +19,8 @@ import { | |||
19 | UpdatedAt | 19 | UpdatedAt |
20 | } from 'sequelize-typescript' | 20 | } from 'sequelize-typescript' |
21 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | 21 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' |
22 | import { doesExist } from '@server/helpers/database-utils' | ||
22 | import { getServerActor } from '@server/models/application/application' | 23 | import { getServerActor } from '@server/models/application/application' |
23 | import { VideoModel } from '@server/models/video/video' | ||
24 | import { | 24 | import { |
25 | MActorFollowActorsDefault, | 25 | MActorFollowActorsDefault, |
26 | MActorFollowActorsDefaultSubscription, | 26 | MActorFollowActorsDefaultSubscription, |
@@ -166,14 +166,8 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
166 | 166 | ||
167 | static isFollowedBy (actorId: number, followerActorId: number) { | 167 | static isFollowedBy (actorId: number, followerActorId: number) { |
168 | const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' | 168 | const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' |
169 | const options = { | ||
170 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
171 | bind: { actorId, followerActorId }, | ||
172 | raw: true | ||
173 | } | ||
174 | 169 | ||
175 | return VideoModel.sequelize.query(query, options) | 170 | return doesExist(query, { actorId, followerActorId }) |
176 | .then(results => results.length === 1) | ||
177 | } | 171 | } |
178 | 172 | ||
179 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { | 173 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index ccda023e0..d645be248 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -160,8 +160,8 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu | |||
160 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 160 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
161 | logger.info('Removing duplicated video file %s.', logIdentifier) | 161 | logger.info('Removing duplicated video file %s.', logIdentifier) |
162 | 162 | ||
163 | videoFile.Video.removeFile(videoFile, true) | 163 | videoFile.Video.removeFileAndTorrent(videoFile, true) |
164 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 164 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
165 | } | 165 | } |
166 | 166 | ||
167 | if (instance.videoStreamingPlaylistId) { | 167 | if (instance.videoStreamingPlaylistId) { |
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index 6b1e59063..3310b3b46 100644 --- a/server/models/video/formatter/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts | |||
@@ -182,8 +182,8 @@ function streamingPlaylistsModelToFormattedJSON ( | |||
182 | return { | 182 | return { |
183 | id: playlist.id, | 183 | id: playlist.id, |
184 | type: playlist.type, | 184 | type: playlist.type, |
185 | playlistUrl: playlist.playlistUrl, | 185 | playlistUrl: playlist.getMasterPlaylistUrl(video), |
186 | segmentsSha256Url: playlist.segmentsSha256Url, | 186 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), |
187 | redundancies, | 187 | redundancies, |
188 | files | 188 | files |
189 | } | 189 | } |
@@ -331,7 +331,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
331 | type: 'Link', | 331 | type: 'Link', |
332 | name: 'sha256', | 332 | name: 'sha256', |
333 | mediaType: 'application/json' as 'application/json', | 333 | mediaType: 'application/json' as 'application/json', |
334 | href: playlist.segmentsSha256Url | 334 | href: playlist.getSha256SegmentsUrl(video) |
335 | }) | 335 | }) |
336 | 336 | ||
337 | addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || []) | 337 | addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || []) |
@@ -339,7 +339,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
339 | url.push({ | 339 | url.push({ |
340 | type: 'Link', | 340 | type: 'Link', |
341 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | 341 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', |
342 | href: playlist.playlistUrl, | 342 | href: playlist.getMasterPlaylistUrl(video), |
343 | tag | 343 | tag |
344 | }) | 344 | }) |
345 | } | 345 | } |
diff --git a/server/models/video/sql/shared/video-tables.ts b/server/models/video/sql/shared/video-tables.ts index abdd22188..742d19099 100644 --- a/server/models/video/sql/shared/video-tables.ts +++ b/server/models/video/sql/shared/video-tables.ts | |||
@@ -92,12 +92,13 @@ export class VideoTables { | |||
92 | } | 92 | } |
93 | 93 | ||
94 | getStreamingPlaylistAttributes () { | 94 | getStreamingPlaylistAttributes () { |
95 | let playlistKeys = [ 'id', 'playlistUrl', 'type' ] | 95 | let playlistKeys = [ 'id', 'playlistUrl', 'playlistFilename', 'type' ] |
96 | 96 | ||
97 | if (this.mode === 'get') { | 97 | if (this.mode === 'get') { |
98 | playlistKeys = playlistKeys.concat([ | 98 | playlistKeys = playlistKeys.concat([ |
99 | 'p2pMediaLoaderInfohashes', | 99 | 'p2pMediaLoaderInfohashes', |
100 | 'p2pMediaLoaderPeerVersion', | 100 | 'p2pMediaLoaderPeerVersion', |
101 | 'segmentsSha256Filename', | ||
101 | 'segmentsSha256Url', | 102 | 'segmentsSha256Url', |
102 | 'videoId', | 103 | 'videoId', |
103 | 'createdAt', | 104 | 'createdAt', |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 22cf63804..797a85a4e 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import * as memoizee from 'memoizee' | 2 | import * as memoizee from 'memoizee' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' | 4 | import { FindOptions, Op, Transaction } from 'sequelize' |
5 | import { | 5 | import { |
6 | AllowNull, | 6 | AllowNull, |
7 | BelongsTo, | 7 | BelongsTo, |
@@ -21,6 +21,7 @@ import { | |||
21 | import { Where } from 'sequelize/types/lib/utils' | 21 | import { Where } from 'sequelize/types/lib/utils' |
22 | import validator from 'validator' | 22 | import validator from 'validator' |
23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | 23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' |
24 | import { doesExist } from '@server/helpers/database-utils' | ||
24 | import { logger } from '@server/helpers/logger' | 25 | import { logger } from '@server/helpers/logger' |
25 | import { extractVideo } from '@server/helpers/video' | 26 | import { extractVideo } from '@server/helpers/video' |
26 | import { getTorrentFilePath } from '@server/lib/video-paths' | 27 | import { getTorrentFilePath } from '@server/lib/video-paths' |
@@ -250,14 +251,8 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
250 | 251 | ||
251 | static doesInfohashExist (infoHash: string) { | 252 | static doesInfohashExist (infoHash: string) { |
252 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' | 253 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' |
253 | const options = { | ||
254 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
255 | bind: { infoHash }, | ||
256 | raw: true | ||
257 | } | ||
258 | 254 | ||
259 | return VideoModel.sequelize.query(query, options) | 255 | return doesExist(query, { infoHash }) |
260 | .then(results => results.length === 1) | ||
261 | } | 256 | } |
262 | 257 | ||
263 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { | 258 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { |
@@ -266,6 +261,33 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
266 | return !!videoFile | 261 | return !!videoFile |
267 | } | 262 | } |
268 | 263 | ||
264 | static async doesOwnedTorrentFileExist (filename: string) { | ||
265 | const query = 'SELECT 1 FROM "videoFile" ' + | ||
266 | 'LEFT JOIN "video" "webtorrent" ON "webtorrent"."id" = "videoFile"."videoId" AND "webtorrent"."remote" IS FALSE ' + | ||
267 | 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' + | ||
268 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + | ||
269 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' | ||
270 | |||
271 | return doesExist(query, { filename }) | ||
272 | } | ||
273 | |||
274 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { | ||
275 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + | ||
276 | 'WHERE "filename" = $filename LIMIT 1' | ||
277 | |||
278 | return doesExist(query, { filename }) | ||
279 | } | ||
280 | |||
281 | static loadByFilename (filename: string) { | ||
282 | const query = { | ||
283 | where: { | ||
284 | filename | ||
285 | } | ||
286 | } | ||
287 | |||
288 | return VideoFileModel.findOne(query) | ||
289 | } | ||
290 | |||
269 | static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { | 291 | static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { |
270 | const query = { | 292 | const query = { |
271 | where: { | 293 | where: { |
@@ -443,10 +465,9 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
443 | } | 465 | } |
444 | 466 | ||
445 | getFileDownloadUrl (video: MVideoWithHost) { | 467 | getFileDownloadUrl (video: MVideoWithHost) { |
446 | const basePath = this.isHLS() | 468 | const path = this.isHLS() |
447 | ? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS | 469 | ? join(STATIC_DOWNLOAD_PATHS.HLS_VIDEOS, `${video.uuid}-${this.resolution}-fragmented${this.extname}`) |
448 | : STATIC_DOWNLOAD_PATHS.VIDEOS | 470 | : join(STATIC_DOWNLOAD_PATHS.VIDEOS, `${video.uuid}-${this.resolution}${this.extname}`) |
449 | const path = join(basePath, this.filename) | ||
450 | 471 | ||
451 | if (video.isOwned()) return WEBSERVER.URL + path | 472 | if (video.isOwned()) return WEBSERVER.URL + path |
452 | 473 | ||
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index d627e8c9d..b15d20cf9 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -1,19 +1,27 @@ | |||
1 | import * as memoizee from 'memoizee' | 1 | import * as memoizee from 'memoizee' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { Op, QueryTypes } from 'sequelize' | 3 | import { Op } from 'sequelize' |
4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
5 | import { doesExist } from '@server/helpers/database-utils' | ||
5 | import { VideoFileModel } from '@server/models/video/video-file' | 6 | import { VideoFileModel } from '@server/models/video/video-file' |
6 | import { MStreamingPlaylist } from '@server/types/models' | 7 | import { MStreamingPlaylist, MVideo } from '@server/types/models' |
8 | import { AttributesOnly } from '@shared/core-utils' | ||
7 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 9 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
8 | import { sha1 } from '../../helpers/core-utils' | 10 | import { sha1 } from '../../helpers/core-utils' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { isArrayOf } from '../../helpers/custom-validators/misc' | 12 | import { isArrayOf } from '../../helpers/custom-validators/misc' |
11 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 13 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
12 | import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants' | 14 | import { |
15 | CONSTRAINTS_FIELDS, | ||
16 | MEMOIZE_LENGTH, | ||
17 | MEMOIZE_TTL, | ||
18 | P2P_MEDIA_LOADER_PEER_VERSION, | ||
19 | STATIC_PATHS, | ||
20 | WEBSERVER | ||
21 | } from '../../initializers/constants' | ||
13 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 22 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
14 | import { throwIfNotValid } from '../utils' | 23 | import { throwIfNotValid } from '../utils' |
15 | import { VideoModel } from './video' | 24 | import { VideoModel } from './video' |
16 | import { AttributesOnly } from '@shared/core-utils' | ||
17 | 25 | ||
18 | @Table({ | 26 | @Table({ |
19 | tableName: 'videoStreamingPlaylist', | 27 | tableName: 'videoStreamingPlaylist', |
@@ -43,7 +51,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
43 | type: VideoStreamingPlaylistType | 51 | type: VideoStreamingPlaylistType |
44 | 52 | ||
45 | @AllowNull(false) | 53 | @AllowNull(false) |
46 | @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url')) | 54 | @Column |
55 | playlistFilename: string | ||
56 | |||
57 | @AllowNull(true) | ||
58 | @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url', true)) | ||
47 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) | 59 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) |
48 | playlistUrl: string | 60 | playlistUrl: string |
49 | 61 | ||
@@ -57,7 +69,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
57 | p2pMediaLoaderPeerVersion: number | 69 | p2pMediaLoaderPeerVersion: number |
58 | 70 | ||
59 | @AllowNull(false) | 71 | @AllowNull(false) |
60 | @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url')) | 72 | @Column |
73 | segmentsSha256Filename: string | ||
74 | |||
75 | @AllowNull(true) | ||
76 | @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url', true)) | ||
61 | @Column | 77 | @Column |
62 | segmentsSha256Url: string | 78 | segmentsSha256Url: string |
63 | 79 | ||
@@ -98,14 +114,8 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
98 | 114 | ||
99 | static doesInfohashExist (infoHash: string) { | 115 | static doesInfohashExist (infoHash: string) { |
100 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' | 116 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' |
101 | const options = { | ||
102 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
103 | bind: { infoHash }, | ||
104 | raw: true | ||
105 | } | ||
106 | 117 | ||
107 | return VideoModel.sequelize.query<object>(query, options) | 118 | return doesExist(query, { infoHash }) |
108 | .then(results => results.length === 1) | ||
109 | } | 119 | } |
110 | 120 | ||
111 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { | 121 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { |
@@ -125,7 +135,13 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
125 | p2pMediaLoaderPeerVersion: { | 135 | p2pMediaLoaderPeerVersion: { |
126 | [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION | 136 | [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION |
127 | } | 137 | } |
128 | } | 138 | }, |
139 | include: [ | ||
140 | { | ||
141 | model: VideoModel.unscoped(), | ||
142 | required: true | ||
143 | } | ||
144 | ] | ||
129 | } | 145 | } |
130 | 146 | ||
131 | return VideoStreamingPlaylistModel.findAll(query) | 147 | return VideoStreamingPlaylistModel.findAll(query) |
@@ -144,7 +160,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
144 | return VideoStreamingPlaylistModel.findByPk(id, options) | 160 | return VideoStreamingPlaylistModel.findByPk(id, options) |
145 | } | 161 | } |
146 | 162 | ||
147 | static loadHLSPlaylistByVideo (videoId: number) { | 163 | static loadHLSPlaylistByVideo (videoId: number): Promise<MStreamingPlaylist> { |
148 | const options = { | 164 | const options = { |
149 | where: { | 165 | where: { |
150 | type: VideoStreamingPlaylistType.HLS, | 166 | type: VideoStreamingPlaylistType.HLS, |
@@ -155,30 +171,29 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
155 | return VideoStreamingPlaylistModel.findOne(options) | 171 | return VideoStreamingPlaylistModel.findOne(options) |
156 | } | 172 | } |
157 | 173 | ||
158 | static getHlsPlaylistFilename (resolution: number) { | 174 | static async loadOrGenerate (video: MVideo) { |
159 | return resolution + '.m3u8' | 175 | let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) |
160 | } | 176 | if (!playlist) playlist = new VideoStreamingPlaylistModel() |
161 | 177 | ||
162 | static getMasterHlsPlaylistFilename () { | 178 | return Object.assign(playlist, { videoId: video.id, Video: video }) |
163 | return 'master.m3u8' | ||
164 | } | 179 | } |
165 | 180 | ||
166 | static getHlsSha256SegmentsFilename () { | 181 | assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { |
167 | return 'segments-sha256.json' | 182 | const masterPlaylistUrl = this.getMasterPlaylistUrl(video) |
168 | } | ||
169 | 183 | ||
170 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { | 184 | this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files) |
171 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
172 | } | 185 | } |
173 | 186 | ||
174 | static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) { | 187 | getMasterPlaylistUrl (video: MVideo) { |
175 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | 188 | if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid) |
189 | |||
190 | return this.playlistUrl | ||
176 | } | 191 | } |
177 | 192 | ||
178 | static getHlsSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) { | 193 | getSha256SegmentsUrl (video: MVideo) { |
179 | if (isLive) return join('/live', 'segments-sha256', videoUUID) | 194 | if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive) |
180 | 195 | ||
181 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | 196 | return this.segmentsSha256Url |
182 | } | 197 | } |
183 | 198 | ||
184 | getStringType () { | 199 | getStringType () { |
@@ -195,4 +210,14 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
195 | return this.type === other.type && | 210 | return this.type === other.type && |
196 | this.videoId === other.videoId | 211 | this.videoId === other.videoId |
197 | } | 212 | } |
213 | |||
214 | private getMasterPlaylistStaticPath (videoUUID: string) { | ||
215 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename) | ||
216 | } | ||
217 | |||
218 | private getSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) { | ||
219 | if (isLive) return join('/live', 'segments-sha256', videoUUID) | ||
220 | |||
221 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename) | ||
222 | } | ||
198 | } | 223 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 1e5648a36..0f0f894e4 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -762,8 +762,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
762 | 762 | ||
763 | // Remove physical files and torrents | 763 | // Remove physical files and torrents |
764 | instance.VideoFiles.forEach(file => { | 764 | instance.VideoFiles.forEach(file => { |
765 | tasks.push(instance.removeFile(file)) | 765 | tasks.push(instance.removeFileAndTorrent(file)) |
766 | tasks.push(file.removeTorrent()) | ||
767 | }) | 766 | }) |
768 | 767 | ||
769 | // Remove playlists file | 768 | // Remove playlists file |
@@ -1670,10 +1669,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1670 | .concat(toAdd) | 1669 | .concat(toAdd) |
1671 | } | 1670 | } |
1672 | 1671 | ||
1673 | removeFile (videoFile: MVideoFile, isRedundancy = false) { | 1672 | removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) { |
1674 | const filePath = getVideoFilePath(this, videoFile, isRedundancy) | 1673 | const filePath = getVideoFilePath(this, videoFile, isRedundancy) |
1675 | return remove(filePath) | 1674 | |
1676 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1675 | const promises: Promise<any>[] = [ remove(filePath) ] |
1676 | if (!isRedundancy) promises.push(videoFile.removeTorrent()) | ||
1677 | |||
1678 | return Promise.all(promises) | ||
1677 | } | 1679 | } |
1678 | 1680 | ||
1679 | async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { | 1681 | async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { |
diff --git a/server/tests/api/live/live-constraints.ts b/server/tests/api/live/live-constraints.ts index 20346113d..4acde3cc5 100644 --- a/server/tests/api/live/live-constraints.ts +++ b/server/tests/api/live/live-constraints.ts | |||
@@ -4,7 +4,7 @@ import 'mocha' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { VideoPrivacy } from '@shared/models' | 5 | import { VideoPrivacy } from '@shared/models' |
6 | import { | 6 | import { |
7 | checkLiveCleanup, | 7 | checkLiveCleanupAfterSave, |
8 | cleanupTests, | 8 | cleanupTests, |
9 | ConfigCommand, | 9 | ConfigCommand, |
10 | createMultipleServers, | 10 | createMultipleServers, |
@@ -43,7 +43,7 @@ describe('Test live constraints', function () { | |||
43 | expect(video.duration).to.be.greaterThan(0) | 43 | expect(video.duration).to.be.greaterThan(0) |
44 | } | 44 | } |
45 | 45 | ||
46 | await checkLiveCleanup(servers[0], videoId, resolutions) | 46 | await checkLiveCleanupAfterSave(servers[0], videoId, resolutions) |
47 | } | 47 | } |
48 | 48 | ||
49 | async function waitUntilLivePublishedOnAllServers (videoId: string) { | 49 | async function waitUntilLivePublishedOnAllServers (videoId: string) { |
diff --git a/server/tests/api/live/live-save-replay.ts b/server/tests/api/live/live-save-replay.ts index bd15396ec..8f1fb78a5 100644 --- a/server/tests/api/live/live-save-replay.ts +++ b/server/tests/api/live/live-save-replay.ts | |||
@@ -4,7 +4,7 @@ import 'mocha' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { FfmpegCommand } from 'fluent-ffmpeg' | 5 | import { FfmpegCommand } from 'fluent-ffmpeg' |
6 | import { | 6 | import { |
7 | checkLiveCleanup, | 7 | checkLiveCleanupAfterSave, |
8 | cleanupTests, | 8 | cleanupTests, |
9 | ConfigCommand, | 9 | ConfigCommand, |
10 | createMultipleServers, | 10 | createMultipleServers, |
@@ -150,7 +150,7 @@ describe('Save replay setting', function () { | |||
150 | await checkVideoState(liveVideoUUID, VideoState.LIVE_ENDED) | 150 | await checkVideoState(liveVideoUUID, VideoState.LIVE_ENDED) |
151 | 151 | ||
152 | // No resolutions saved since we did not save replay | 152 | // No resolutions saved since we did not save replay |
153 | await checkLiveCleanup(servers[0], liveVideoUUID, []) | 153 | await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, []) |
154 | }) | 154 | }) |
155 | 155 | ||
156 | it('Should correctly terminate the stream on blacklist and delete the live', async function () { | 156 | it('Should correctly terminate the stream on blacklist and delete the live', async function () { |
@@ -179,7 +179,7 @@ describe('Save replay setting', function () { | |||
179 | 179 | ||
180 | await wait(5000) | 180 | await wait(5000) |
181 | await waitJobs(servers) | 181 | await waitJobs(servers) |
182 | await checkLiveCleanup(servers[0], liveVideoUUID, []) | 182 | await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, []) |
183 | }) | 183 | }) |
184 | 184 | ||
185 | it('Should correctly terminate the stream on delete and delete the video', async function () { | 185 | it('Should correctly terminate the stream on delete and delete the video', async function () { |
@@ -203,7 +203,7 @@ describe('Save replay setting', function () { | |||
203 | await waitJobs(servers) | 203 | await waitJobs(servers) |
204 | 204 | ||
205 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | 205 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) |
206 | await checkLiveCleanup(servers[0], liveVideoUUID, []) | 206 | await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, []) |
207 | }) | 207 | }) |
208 | }) | 208 | }) |
209 | 209 | ||
@@ -259,7 +259,7 @@ describe('Save replay setting', function () { | |||
259 | }) | 259 | }) |
260 | 260 | ||
261 | it('Should have cleaned up the live files', async function () { | 261 | it('Should have cleaned up the live files', async function () { |
262 | await checkLiveCleanup(servers[0], liveVideoUUID, [ 720 ]) | 262 | await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, [ 720 ]) |
263 | }) | 263 | }) |
264 | 264 | ||
265 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | 265 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { |
@@ -287,7 +287,7 @@ describe('Save replay setting', function () { | |||
287 | 287 | ||
288 | await wait(5000) | 288 | await wait(5000) |
289 | await waitJobs(servers) | 289 | await waitJobs(servers) |
290 | await checkLiveCleanup(servers[0], liveVideoUUID, [ 720 ]) | 290 | await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, [ 720 ]) |
291 | }) | 291 | }) |
292 | 292 | ||
293 | it('Should correctly terminate the stream on delete and delete the video', async function () { | 293 | it('Should correctly terminate the stream on delete and delete the video', async function () { |
@@ -310,7 +310,7 @@ describe('Save replay setting', function () { | |||
310 | await waitJobs(servers) | 310 | await waitJobs(servers) |
311 | 311 | ||
312 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | 312 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) |
313 | await checkLiveCleanup(servers[0], liveVideoUUID, []) | 313 | await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, []) |
314 | }) | 314 | }) |
315 | }) | 315 | }) |
316 | 316 | ||
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 4676a840a..d555cff19 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -2,10 +2,10 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { join } from 'path' | 5 | import { basename, join } from 'path' |
6 | import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' | 6 | import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' |
7 | import { | 7 | import { |
8 | checkLiveCleanup, | 8 | checkLiveCleanupAfterSave, |
9 | checkLiveSegmentHash, | 9 | checkLiveSegmentHash, |
10 | checkResolutionsInMasterPlaylist, | 10 | checkResolutionsInMasterPlaylist, |
11 | cleanupTests, | 11 | cleanupTests, |
@@ -506,6 +506,10 @@ describe('Test live', function () { | |||
506 | await makeRawRequest(hlsPlaylist.playlistUrl, HttpStatusCode.OK_200) | 506 | await makeRawRequest(hlsPlaylist.playlistUrl, HttpStatusCode.OK_200) |
507 | await makeRawRequest(hlsPlaylist.segmentsSha256Url, HttpStatusCode.OK_200) | 507 | await makeRawRequest(hlsPlaylist.segmentsSha256Url, HttpStatusCode.OK_200) |
508 | 508 | ||
509 | // We should have generated random filenames | ||
510 | expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8') | ||
511 | expect(basename(hlsPlaylist.segmentsSha256Url)).to.not.equal('segments-sha256.json') | ||
512 | |||
509 | expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length) | 513 | expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length) |
510 | 514 | ||
511 | for (const resolution of resolutions) { | 515 | for (const resolution of resolutions) { |
@@ -520,7 +524,9 @@ describe('Test live', function () { | |||
520 | expect(file.fps).to.be.approximately(30, 2) | 524 | expect(file.fps).to.be.approximately(30, 2) |
521 | } | 525 | } |
522 | 526 | ||
523 | const filename = `${video.uuid}-${resolution}-fragmented.mp4` | 527 | const filename = basename(file.fileUrl) |
528 | expect(filename).to.not.contain(video.uuid) | ||
529 | |||
524 | const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, filename)) | 530 | const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, filename)) |
525 | 531 | ||
526 | const probe = await ffprobePromise(segmentPath) | 532 | const probe = await ffprobePromise(segmentPath) |
@@ -537,7 +543,7 @@ describe('Test live', function () { | |||
537 | it('Should correctly have cleaned up the live files', async function () { | 543 | it('Should correctly have cleaned up the live files', async function () { |
538 | this.timeout(30000) | 544 | this.timeout(30000) |
539 | 545 | ||
540 | await checkLiveCleanup(servers[0], liveVideoId, [ 240, 360, 720 ]) | 546 | await checkLiveCleanupAfterSave(servers[0], liveVideoId, [ 240, 360, 720 ]) |
541 | }) | 547 | }) |
542 | }) | 548 | }) |
543 | 549 | ||
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts index 16372b039..d0ca82b07 100644 --- a/server/tests/api/users/users-multiple-servers.ts +++ b/server/tests/api/users/users-multiple-servers.ts | |||
@@ -58,10 +58,10 @@ describe('Test users with multiple servers', function () { | |||
58 | const { uuid } = await servers[0].videos.upload({ token: userAccessToken }) | 58 | const { uuid } = await servers[0].videos.upload({ token: userAccessToken }) |
59 | videoUUID = uuid | 59 | videoUUID = uuid |
60 | 60 | ||
61 | await waitJobs(servers) | ||
62 | |||
61 | await saveVideoInServers(servers, videoUUID) | 63 | await saveVideoInServers(servers, videoUUID) |
62 | } | 64 | } |
63 | |||
64 | await waitJobs(servers) | ||
65 | }) | 65 | }) |
66 | 66 | ||
67 | it('Should be able to update my display name', async function () { | 67 | it('Should be able to update my display name', async function () { |
diff --git a/server/tests/api/videos/resumable-upload.ts b/server/tests/api/videos/resumable-upload.ts index c94d92cf2..857859fd3 100644 --- a/server/tests/api/videos/resumable-upload.ts +++ b/server/tests/api/videos/resumable-upload.ts | |||
@@ -170,8 +170,13 @@ describe('Test resumable upload', function () { | |||
170 | 170 | ||
171 | const size = 1000 | 171 | const size = 1000 |
172 | 172 | ||
173 | // Content length check seems to have changed in v16 | ||
174 | const expectedStatus = process.version.startsWith('v16') | ||
175 | ? HttpStatusCode.CONFLICT_409 | ||
176 | : HttpStatusCode.BAD_REQUEST_400 | ||
177 | |||
173 | const contentRangeBuilder = (start: number) => `bytes ${start}-${start + size - 1}/${size}` | 178 | const contentRangeBuilder = (start: number) => `bytes ${start}-${start + size - 1}/${size}` |
174 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409, contentRangeBuilder, contentLength: size }) | 179 | await sendChunks({ pathUploadId: uploadId, expectedStatus, contentRangeBuilder, contentLength: size }) |
175 | await checkFileSize(uploadId, 0) | 180 | await checkFileSize(uploadId, 0) |
176 | }) | 181 | }) |
177 | }) | 182 | }) |
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts index 921d7ce64..961f0e617 100644 --- a/server/tests/api/videos/video-hls.ts +++ b/server/tests/api/videos/video-hls.ts | |||
@@ -2,7 +2,8 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { join } from 'path' | 5 | import { basename, join } from 'path' |
6 | import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils' | ||
6 | import { | 7 | import { |
7 | checkDirectoryIsEmpty, | 8 | checkDirectoryIsEmpty, |
8 | checkResolutionsInMasterPlaylist, | 9 | checkResolutionsInMasterPlaylist, |
@@ -19,8 +20,6 @@ import { | |||
19 | } from '@shared/extra-utils' | 20 | } from '@shared/extra-utils' |
20 | import { HttpStatusCode, VideoStreamingPlaylistType } from '@shared/models' | 21 | import { HttpStatusCode, VideoStreamingPlaylistType } from '@shared/models' |
21 | import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' | 22 | import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' |
22 | import { uuidRegex } from '@shared/core-utils' | ||
23 | import { basename } from 'path/posix' | ||
24 | 23 | ||
25 | const expect = chai.expect | 24 | const expect = chai.expect |
26 | 25 | ||
@@ -78,11 +77,13 @@ async function checkHlsPlaylist (servers: PeerTubeServer[], videoUUID: string, h | |||
78 | // Check resolution playlists | 77 | // Check resolution playlists |
79 | { | 78 | { |
80 | for (const resolution of resolutions) { | 79 | for (const resolution of resolutions) { |
80 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
81 | const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8' | ||
82 | |||
81 | const subPlaylist = await server.streamingPlaylists.get({ | 83 | const subPlaylist = await server.streamingPlaylists.get({ |
82 | url: `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8` | 84 | url: `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${playlistName}` |
83 | }) | 85 | }) |
84 | 86 | ||
85 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
86 | expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`)) | 87 | expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`)) |
87 | expect(subPlaylist).to.contain(basename(file.fileUrl)) | 88 | expect(subPlaylist).to.contain(basename(file.fileUrl)) |
88 | } | 89 | } |
diff --git a/server/tests/cli/optimize-old-videos.ts b/server/tests/cli/optimize-old-videos.ts index 685b3b7b8..579b2e7d8 100644 --- a/server/tests/cli/optimize-old-videos.ts +++ b/server/tests/cli/optimize-old-videos.ts | |||
@@ -2,7 +2,6 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { join } from 'path' | ||
6 | import { | 5 | import { |
7 | cleanupTests, | 6 | cleanupTests, |
8 | createMultipleServers, | 7 | createMultipleServers, |
@@ -86,7 +85,7 @@ describe('Test optimize old videos', function () { | |||
86 | 85 | ||
87 | expect(file.size).to.be.below(8000000) | 86 | expect(file.size).to.be.below(8000000) |
88 | 87 | ||
89 | const path = servers[0].servers.buildDirectory(join('videos', video.uuid + '-' + file.resolution.id + '.mp4')) | 88 | const path = servers[0].servers.buildWebTorrentFilePath(file.fileUrl) |
90 | const bitrate = await getVideoFileBitrate(path) | 89 | const bitrate = await getVideoFileBitrate(path) |
91 | const fps = await getVideoFileFPS(path) | 90 | const fps = await getVideoFileFPS(path) |
92 | const resolution = await getVideoFileResolution(path) | 91 | const resolution = await getVideoFileResolution(path) |
diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts index 954a87833..2d4c02da7 100644 --- a/server/tests/cli/prune-storage.ts +++ b/server/tests/cli/prune-storage.ts | |||
@@ -36,7 +36,7 @@ async function assertNotExists (server: PeerTubeServer, directory: string, subst | |||
36 | } | 36 | } |
37 | } | 37 | } |
38 | 38 | ||
39 | async function assertCountAreOkay (servers: PeerTubeServer[]) { | 39 | async function assertCountAreOkay (servers: PeerTubeServer[], videoServer2UUID: string) { |
40 | for (const server of servers) { | 40 | for (const server of servers) { |
41 | const videosCount = await countFiles(server, 'videos') | 41 | const videosCount = await countFiles(server, 'videos') |
42 | expect(videosCount).to.equal(8) | 42 | expect(videosCount).to.equal(8) |
@@ -53,12 +53,21 @@ async function assertCountAreOkay (servers: PeerTubeServer[]) { | |||
53 | const avatarsCount = await countFiles(server, 'avatars') | 53 | const avatarsCount = await countFiles(server, 'avatars') |
54 | expect(avatarsCount).to.equal(2) | 54 | expect(avatarsCount).to.equal(2) |
55 | } | 55 | } |
56 | |||
57 | // When we'll prune HLS directories too | ||
58 | // const hlsRootCount = await countFiles(servers[1], 'streaming-playlists/hls/') | ||
59 | // expect(hlsRootCount).to.equal(2) | ||
60 | |||
61 | // const hlsCount = await countFiles(servers[1], 'streaming-playlists/hls/' + videoServer2UUID) | ||
62 | // expect(hlsCount).to.equal(10) | ||
56 | } | 63 | } |
57 | 64 | ||
58 | describe('Test prune storage scripts', function () { | 65 | describe('Test prune storage scripts', function () { |
59 | let servers: PeerTubeServer[] | 66 | let servers: PeerTubeServer[] |
60 | const badNames: { [directory: string]: string[] } = {} | 67 | const badNames: { [directory: string]: string[] } = {} |
61 | 68 | ||
69 | let videoServer2UUID: string | ||
70 | |||
62 | before(async function () { | 71 | before(async function () { |
63 | this.timeout(120000) | 72 | this.timeout(120000) |
64 | 73 | ||
@@ -68,7 +77,9 @@ describe('Test prune storage scripts', function () { | |||
68 | 77 | ||
69 | for (const server of servers) { | 78 | for (const server of servers) { |
70 | await server.videos.upload({ attributes: { name: 'video 1' } }) | 79 | await server.videos.upload({ attributes: { name: 'video 1' } }) |
71 | await server.videos.upload({ attributes: { name: 'video 2' } }) | 80 | |
81 | const { uuid } = await server.videos.upload({ attributes: { name: 'video 2' } }) | ||
82 | if (server.serverNumber === 2) videoServer2UUID = uuid | ||
72 | 83 | ||
73 | await server.users.updateMyAvatar({ fixture: 'avatar.png' }) | 84 | await server.users.updateMyAvatar({ fixture: 'avatar.png' }) |
74 | 85 | ||
@@ -112,7 +123,7 @@ describe('Test prune storage scripts', function () { | |||
112 | }) | 123 | }) |
113 | 124 | ||
114 | it('Should have the files on the disk', async function () { | 125 | it('Should have the files on the disk', async function () { |
115 | await assertCountAreOkay(servers) | 126 | await assertCountAreOkay(servers, videoServer2UUID) |
116 | }) | 127 | }) |
117 | 128 | ||
118 | it('Should create some dirty files', async function () { | 129 | it('Should create some dirty files', async function () { |
@@ -176,6 +187,28 @@ describe('Test prune storage scripts', function () { | |||
176 | 187 | ||
177 | badNames['avatars'] = [ n1, n2 ] | 188 | badNames['avatars'] = [ n1, n2 ] |
178 | } | 189 | } |
190 | |||
191 | // When we'll prune HLS directories too | ||
192 | // { | ||
193 | // const directory = join('streaming-playlists', 'hls') | ||
194 | // const base = servers[1].servers.buildDirectory(directory) | ||
195 | |||
196 | // const n1 = buildUUID() | ||
197 | // await createFile(join(base, n1)) | ||
198 | // badNames[directory] = [ n1 ] | ||
199 | // } | ||
200 | |||
201 | // { | ||
202 | // const directory = join('streaming-playlists', 'hls', videoServer2UUID) | ||
203 | // const base = servers[1].servers.buildDirectory(directory) | ||
204 | // const n1 = buildUUID() + '-240-fragmented-.mp4' | ||
205 | // const n2 = buildUUID() + '-master.m3u8' | ||
206 | |||
207 | // await createFile(join(base, n1)) | ||
208 | // await createFile(join(base, n2)) | ||
209 | |||
210 | // badNames[directory] = [ n1, n2 ] | ||
211 | // } | ||
179 | } | 212 | } |
180 | }) | 213 | }) |
181 | 214 | ||
@@ -187,7 +220,7 @@ describe('Test prune storage scripts', function () { | |||
187 | }) | 220 | }) |
188 | 221 | ||
189 | it('Should have removed files', async function () { | 222 | it('Should have removed files', async function () { |
190 | await assertCountAreOkay(servers) | 223 | await assertCountAreOkay(servers, videoServer2UUID) |
191 | 224 | ||
192 | for (const directory of Object.keys(badNames)) { | 225 | for (const directory of Object.keys(badNames)) { |
193 | for (const name of badNames[directory]) { | 226 | for (const name of badNames[directory]) { |
diff --git a/server/tests/cli/update-host.ts b/server/tests/cli/update-host.ts index fcbcb55ba..43fbaec30 100644 --- a/server/tests/cli/update-host.ts +++ b/server/tests/cli/update-host.ts | |||
@@ -108,21 +108,22 @@ describe('Test update host scripts', function () { | |||
108 | 108 | ||
109 | for (const video of data) { | 109 | for (const video of data) { |
110 | const videoDetails = await server.videos.get({ id: video.id }) | 110 | const videoDetails = await server.videos.get({ id: video.id }) |
111 | const files = videoDetails.files.concat(videoDetails.streamingPlaylists[0].files) | ||
111 | 112 | ||
112 | expect(videoDetails.files).to.have.lengthOf(4) | 113 | expect(files).to.have.lengthOf(8) |
113 | 114 | ||
114 | for (const file of videoDetails.files) { | 115 | for (const file of files) { |
115 | expect(file.magnetUri).to.contain('localhost%3A9002%2Ftracker%2Fsocket') | 116 | expect(file.magnetUri).to.contain('localhost%3A9002%2Ftracker%2Fsocket') |
116 | expect(file.magnetUri).to.contain('localhost%3A9002%2Fstatic%2Fwebseed%2F') | 117 | expect(file.magnetUri).to.contain('localhost%3A9002%2Fstatic%2F') |
117 | 118 | ||
118 | const torrent = await parseTorrentVideo(server, videoDetails.uuid, file.resolution.id) | 119 | const torrent = await parseTorrentVideo(server, file) |
119 | const announceWS = torrent.announce.find(a => a === 'ws://localhost:9002/tracker/socket') | 120 | const announceWS = torrent.announce.find(a => a === 'ws://localhost:9002/tracker/socket') |
120 | expect(announceWS).to.not.be.undefined | 121 | expect(announceWS).to.not.be.undefined |
121 | 122 | ||
122 | const announceHttp = torrent.announce.find(a => a === 'http://localhost:9002/tracker/announce') | 123 | const announceHttp = torrent.announce.find(a => a === 'http://localhost:9002/tracker/announce') |
123 | expect(announceHttp).to.not.be.undefined | 124 | expect(announceHttp).to.not.be.undefined |
124 | 125 | ||
125 | expect(torrent.urlList[0]).to.contain('http://localhost:9002/static/webseed') | 126 | expect(torrent.urlList[0]).to.contain('http://localhost:9002/static/') |
126 | } | 127 | } |
127 | } | 128 | } |
128 | }) | 129 | }) |
diff --git a/server/tests/plugins/plugin-transcoding.ts b/server/tests/plugins/plugin-transcoding.ts index c14c34c7e..93637e3ce 100644 --- a/server/tests/plugins/plugin-transcoding.ts +++ b/server/tests/plugins/plugin-transcoding.ts | |||
@@ -2,7 +2,6 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { expect } from 'chai' | 4 | import { expect } from 'chai' |
5 | import { join } from 'path' | ||
6 | import { getAudioStream, getVideoFileFPS, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' | 5 | import { getAudioStream, getVideoFileFPS, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' |
7 | import { | 6 | import { |
8 | cleanupTests, | 7 | cleanupTests, |
@@ -247,7 +246,9 @@ describe('Test transcoding plugins', function () { | |||
247 | const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid | 246 | const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid |
248 | await waitJobs([ server ]) | 247 | await waitJobs([ server ]) |
249 | 248 | ||
250 | const path = server.servers.buildDirectory(join('videos', videoUUID + '-240.mp4')) | 249 | const video = await server.videos.get({ id: videoUUID }) |
250 | |||
251 | const path = server.servers.buildWebTorrentFilePath(video.files[0].fileUrl) | ||
251 | const audioProbe = await getAudioStream(path) | 252 | const audioProbe = await getAudioStream(path) |
252 | expect(audioProbe.audioStream.codec_name).to.equal('opus') | 253 | expect(audioProbe.audioStream.codec_name).to.equal('opus') |
253 | 254 | ||
diff --git a/server/types/models/video/video-streaming-playlist.ts b/server/types/models/video/video-streaming-playlist.ts index 8b3ef51fc..1e4dccb8e 100644 --- a/server/types/models/video/video-streaming-playlist.ts +++ b/server/types/models/video/video-streaming-playlist.ts | |||
@@ -39,5 +39,5 @@ export type MStreamingPlaylistRedundanciesOpt = | |||
39 | PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> | 39 | PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> |
40 | 40 | ||
41 | export function isStreamingPlaylist (value: MVideo | MStreamingPlaylistVideo): value is MStreamingPlaylistVideo { | 41 | export function isStreamingPlaylist (value: MVideo | MStreamingPlaylistVideo): value is MStreamingPlaylistVideo { |
42 | return !!(value as MStreamingPlaylist).playlistUrl | 42 | return !!(value as MStreamingPlaylist).videoId |
43 | } | 43 | } |
diff --git a/shared/core-utils/miscs/regexp.ts b/shared/core-utils/miscs/regexp.ts index 862b8e00f..59eb87eb6 100644 --- a/shared/core-utils/miscs/regexp.ts +++ b/shared/core-utils/miscs/regexp.ts | |||
@@ -1 +1,5 @@ | |||
1 | export const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | 1 | export const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' |
2 | |||
3 | export function removeFragmentedMP4Ext (path: string) { | ||
4 | return path.replace(/-fragmented.mp4$/i, '') | ||
5 | } | ||
diff --git a/shared/extra-utils/miscs/webtorrent.ts b/shared/extra-utils/miscs/webtorrent.ts index 815ea3d56..a1097effe 100644 --- a/shared/extra-utils/miscs/webtorrent.ts +++ b/shared/extra-utils/miscs/webtorrent.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { readFile } from 'fs-extra' | 1 | import { readFile } from 'fs-extra' |
2 | import * as parseTorrent from 'parse-torrent' | 2 | import * as parseTorrent from 'parse-torrent' |
3 | import { join } from 'path' | 3 | import { basename, join } from 'path' |
4 | import * as WebTorrent from 'webtorrent' | 4 | import * as WebTorrent from 'webtorrent' |
5 | import { VideoFile } from '@shared/models' | ||
5 | import { PeerTubeServer } from '../server' | 6 | import { PeerTubeServer } from '../server' |
6 | 7 | ||
7 | let webtorrent: WebTorrent.Instance | 8 | let webtorrent: WebTorrent.Instance |
@@ -15,8 +16,8 @@ function webtorrentAdd (torrent: string, refreshWebTorrent = false) { | |||
15 | return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res)) | 16 | return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res)) |
16 | } | 17 | } |
17 | 18 | ||
18 | async function parseTorrentVideo (server: PeerTubeServer, videoUUID: string, resolution: number) { | 19 | async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) { |
19 | const torrentName = videoUUID + '-' + resolution + '.torrent' | 20 | const torrentName = basename(file.torrentUrl) |
20 | const torrentPath = server.servers.buildDirectory(join('torrents', torrentName)) | 21 | const torrentPath = server.servers.buildDirectory(join('torrents', torrentName)) |
21 | 22 | ||
22 | const data = await readFile(torrentPath) | 23 | const data = await readFile(torrentPath) |
diff --git a/shared/extra-utils/server/servers-command.ts b/shared/extra-utils/server/servers-command.ts index 441c728c1..40a11e8d7 100644 --- a/shared/extra-utils/server/servers-command.ts +++ b/shared/extra-utils/server/servers-command.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { exec } from 'child_process' | 1 | import { exec } from 'child_process' |
2 | import { copy, ensureDir, readFile, remove } from 'fs-extra' | 2 | import { copy, ensureDir, readFile, remove } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { basename, join } from 'path' |
4 | import { basename } from 'path/posix' | ||
5 | import { root } from '@server/helpers/core-utils' | 4 | import { root } from '@server/helpers/core-utils' |
6 | import { HttpStatusCode } from '@shared/models' | 5 | import { HttpStatusCode } from '@shared/models' |
7 | import { getFileSize, isGithubCI, wait } from '../miscs' | 6 | import { getFileSize, isGithubCI, wait } from '../miscs' |
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts index 502964b1a..94f5f5b59 100644 --- a/shared/extra-utils/videos/live.ts +++ b/shared/extra-utils/videos/live.ts | |||
@@ -76,7 +76,7 @@ async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], vi | |||
76 | } | 76 | } |
77 | } | 77 | } |
78 | 78 | ||
79 | async function checkLiveCleanup (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) { | 79 | async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) { |
80 | const basePath = server.servers.buildDirectory('streaming-playlists') | 80 | const basePath = server.servers.buildDirectory('streaming-playlists') |
81 | const hlsPath = join(basePath, 'hls', videoUUID) | 81 | const hlsPath = join(basePath, 'hls', videoUUID) |
82 | 82 | ||
@@ -93,12 +93,18 @@ async function checkLiveCleanup (server: PeerTubeServer, videoUUID: string, reso | |||
93 | expect(files).to.have.lengthOf(resolutions.length * 2 + 2) | 93 | expect(files).to.have.lengthOf(resolutions.length * 2 + 2) |
94 | 94 | ||
95 | for (const resolution of resolutions) { | 95 | for (const resolution of resolutions) { |
96 | expect(files).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) | 96 | const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`)) |
97 | expect(files).to.contain(`${resolution}.m3u8`) | 97 | expect(fragmentedFile).to.exist |
98 | |||
99 | const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`)) | ||
100 | expect(playlistFile).to.exist | ||
98 | } | 101 | } |
99 | 102 | ||
100 | expect(files).to.contain('master.m3u8') | 103 | const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8')) |
101 | expect(files).to.contain('segments-sha256.json') | 104 | expect(masterPlaylistFile).to.exist |
105 | |||
106 | const shaFile = files.find(f => f.endsWith('-segments-sha256.json')) | ||
107 | expect(shaFile).to.exist | ||
102 | } | 108 | } |
103 | 109 | ||
104 | export { | 110 | export { |
@@ -107,5 +113,5 @@ export { | |||
107 | testFfmpegStreamError, | 113 | testFfmpegStreamError, |
108 | stopFfmpeg, | 114 | stopFfmpeg, |
109 | waitUntilLivePublishedOnAllServers, | 115 | waitUntilLivePublishedOnAllServers, |
110 | checkLiveCleanup | 116 | checkLiveCleanupAfterSave |
111 | } | 117 | } |
diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts index db40c27be..a224b8f5f 100644 --- a/shared/extra-utils/videos/streaming-playlists.ts +++ b/shared/extra-utils/videos/streaming-playlists.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { expect } from 'chai' | 1 | import { expect } from 'chai' |
2 | import { basename } from 'path' | 2 | import { basename } from 'path' |
3 | import { sha256 } from '@server/helpers/core-utils' | 3 | import { sha256 } from '@server/helpers/core-utils' |
4 | import { removeFragmentedMP4Ext } from '@shared/core-utils' | ||
4 | import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models' | 5 | import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models' |
5 | import { PeerTubeServer } from '../server' | 6 | import { PeerTubeServer } from '../server' |
6 | 7 | ||
@@ -15,11 +16,11 @@ async function checkSegmentHash (options: { | |||
15 | const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options | 16 | const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options |
16 | const command = server.streamingPlaylists | 17 | const command = server.streamingPlaylists |
17 | 18 | ||
18 | const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8` }) | ||
19 | |||
20 | const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) | 19 | const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) |
21 | const videoName = basename(file.fileUrl) | 20 | const videoName = basename(file.fileUrl) |
22 | 21 | ||
22 | const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${removeFragmentedMP4Ext(videoName)}.m3u8` }) | ||
23 | |||
23 | const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) | 24 | const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) |
24 | 25 | ||
25 | const length = parseInt(matches[1], 10) | 26 | const length = parseInt(matches[1], 10) |