diff options
author | Chocobozzz <me@florianbigard.com> | 2022-07-29 14:50:41 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-08-01 14:55:10 +0200 |
commit | 1bb4c9ab2e8b3b3022351b33a82a5e527fa5d4d7 (patch) | |
tree | a6554ee0a3ccc2ae402665b2ecf57bb38fd0ed72 /server/lib/hls.ts | |
parent | 12d84abeca4917d2f1e3f308010bfcd56d37cb7c (diff) | |
download | PeerTube-1bb4c9ab2e8b3b3022351b33a82a5e527fa5d4d7.tar.gz PeerTube-1bb4c9ab2e8b3b3022351b33a82a5e527fa5d4d7.tar.zst PeerTube-1bb4c9ab2e8b3b3022351b33a82a5e527fa5d4d7.zip |
Add ability to delete a specific video file
Diffstat (limited to 'server/lib/hls.ts')
-rw-r--r-- | server/lib/hls.ts | 147 |
1 files changed, 96 insertions, 51 deletions
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 43043315b..20754219f 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { close, ensureDir, move, open, outputJSON, read, readFile, remove, stat, writeFile } from 'fs-extra' | 1 | import { close, ensureDir, move, open, outputJSON, read, readFile, remove, stat, writeFile } from 'fs-extra' |
2 | import { flatten, uniq } from 'lodash' | 2 | import { flatten, uniq } from 'lodash' |
3 | import PQueue from 'p-queue' | ||
3 | import { basename, dirname, join } from 'path' | 4 | import { basename, dirname, join } from 'path' |
4 | import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models' | 5 | import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models' |
5 | import { sha256 } from '@shared/extra-utils' | 6 | import { sha256 } from '@shared/extra-utils' |
6 | import { VideoStorage } from '@shared/models' | 7 | import { VideoStorage } from '@shared/models' |
7 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg' | 8 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg' |
@@ -14,7 +15,7 @@ import { sequelizeTypescript } from '../initializers/database' | |||
14 | import { VideoFileModel } from '../models/video/video-file' | 15 | import { VideoFileModel } from '../models/video/video-file' |
15 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 16 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
16 | import { storeHLSFile } from './object-storage' | 17 | import { storeHLSFile } from './object-storage' |
17 | import { getHlsResolutionPlaylistFilename } from './paths' | 18 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHlsResolutionPlaylistFilename } from './paths' |
18 | import { VideoPathManager } from './video-path-manager' | 19 | import { VideoPathManager } from './video-path-manager' |
19 | 20 | ||
20 | async function updateStreamingPlaylistsInfohashesIfNeeded () { | 21 | async function updateStreamingPlaylistsInfohashesIfNeeded () { |
@@ -33,80 +34,123 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () { | |||
33 | } | 34 | } |
34 | } | 35 | } |
35 | 36 | ||
36 | async function updateMasterHLSPlaylist (video: MVideo, playlist: MStreamingPlaylistFilesVideo) { | 37 | async function updatePlaylistAfterFileChange (video: MVideo, playlist: MStreamingPlaylist) { |
37 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] | 38 | let playlistWithFiles = await updateMasterHLSPlaylist(video, playlist) |
39 | playlistWithFiles = await updateSha256VODSegments(video, playlist) | ||
38 | 40 | ||
39 | for (const file of playlist.VideoFiles) { | 41 | // Refresh playlist, operations can take some time |
40 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) | 42 | playlistWithFiles = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlist.id) |
43 | playlistWithFiles.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles) | ||
44 | await playlistWithFiles.save() | ||
41 | 45 | ||
42 | await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => { | 46 | video.setHLSPlaylist(playlistWithFiles) |
43 | const size = await getVideoStreamDimensionsInfo(videoFilePath) | 47 | } |
44 | 48 | ||
45 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) | 49 | // --------------------------------------------------------------------------- |
46 | const resolution = `RESOLUTION=${size?.width || 0}x${size?.height || 0}` | ||
47 | 50 | ||
48 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` | 51 | // Avoid concurrency issues when updating streaming playlist files |
49 | if (file.fps) line += ',FRAME-RATE=' + file.fps | 52 | const playlistFilesQueue = new PQueue({ concurrency: 1 }) |
50 | 53 | ||
51 | const codecs = await Promise.all([ | 54 | function updateMasterHLSPlaylist (video: MVideo, playlistArg: MStreamingPlaylist): Promise<MStreamingPlaylistFilesVideo> { |
52 | getVideoStreamCodec(videoFilePath), | 55 | return playlistFilesQueue.add(async () => { |
53 | getAudioStreamCodec(videoFilePath) | 56 | const playlist = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id) |
54 | ]) | ||
55 | 57 | ||
56 | line += `,CODECS="${codecs.filter(c => !!c).join(',')}"` | 58 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] |
57 | 59 | ||
58 | masterPlaylists.push(line) | 60 | for (const file of playlist.VideoFiles) { |
59 | masterPlaylists.push(playlistFilename) | 61 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) |
60 | }) | 62 | |
61 | } | 63 | await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => { |
64 | const size = await getVideoStreamDimensionsInfo(videoFilePath) | ||
65 | |||
66 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) | ||
67 | const resolution = `RESOLUTION=${size?.width || 0}x${size?.height || 0}` | ||
68 | |||
69 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` | ||
70 | if (file.fps) line += ',FRAME-RATE=' + file.fps | ||
71 | |||
72 | const codecs = await Promise.all([ | ||
73 | getVideoStreamCodec(videoFilePath), | ||
74 | getAudioStreamCodec(videoFilePath) | ||
75 | ]) | ||
62 | 76 | ||
63 | await VideoPathManager.Instance.makeAvailablePlaylistFile(playlist, playlist.playlistFilename, async masterPlaylistPath => { | 77 | line += `,CODECS="${codecs.filter(c => !!c).join(',')}"` |
78 | |||
79 | masterPlaylists.push(line) | ||
80 | masterPlaylists.push(playlistFilename) | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | if (playlist.playlistFilename) { | ||
85 | await video.removeStreamingPlaylistFile(playlist, playlist.playlistFilename) | ||
86 | } | ||
87 | playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive) | ||
88 | |||
89 | const masterPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.playlistFilename) | ||
64 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') | 90 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') |
65 | 91 | ||
66 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { | 92 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { |
67 | await storeHLSFile(playlist, playlist.playlistFilename, masterPlaylistPath) | 93 | playlist.playlistUrl = await storeHLSFile(playlist, playlist.playlistFilename) |
94 | await remove(masterPlaylistPath) | ||
68 | } | 95 | } |
96 | |||
97 | return playlist.save() | ||
69 | }) | 98 | }) |
70 | } | 99 | } |
71 | 100 | ||
72 | async function updateSha256VODSegments (video: MVideoUUID, playlist: MStreamingPlaylistFilesVideo) { | 101 | // --------------------------------------------------------------------------- |
73 | const json: { [filename: string]: { [range: string]: string } } = {} | 102 | |
103 | async function updateSha256VODSegments (video: MVideo, playlistArg: MStreamingPlaylist): Promise<MStreamingPlaylistFilesVideo> { | ||
104 | return playlistFilesQueue.add(async () => { | ||
105 | const json: { [filename: string]: { [range: string]: string } } = {} | ||
74 | 106 | ||
75 | // For all the resolutions available for this video | 107 | const playlist = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id) |
76 | for (const file of playlist.VideoFiles) { | ||
77 | const rangeHashes: { [range: string]: string } = {} | ||
78 | const fileWithPlaylist = file.withVideoOrPlaylist(playlist) | ||
79 | 108 | ||
80 | await VideoPathManager.Instance.makeAvailableVideoFile(fileWithPlaylist, videoPath => { | 109 | // For all the resolutions available for this video |
110 | for (const file of playlist.VideoFiles) { | ||
111 | const rangeHashes: { [range: string]: string } = {} | ||
112 | const fileWithPlaylist = file.withVideoOrPlaylist(playlist) | ||
81 | 113 | ||
82 | return VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(fileWithPlaylist, async resolutionPlaylistPath => { | 114 | await VideoPathManager.Instance.makeAvailableVideoFile(fileWithPlaylist, videoPath => { |
83 | const playlistContent = await readFile(resolutionPlaylistPath) | ||
84 | const ranges = getRangesFromPlaylist(playlistContent.toString()) | ||
85 | 115 | ||
86 | const fd = await open(videoPath, 'r') | 116 | return VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(fileWithPlaylist, async resolutionPlaylistPath => { |
87 | for (const range of ranges) { | 117 | const playlistContent = await readFile(resolutionPlaylistPath) |
88 | const buf = Buffer.alloc(range.length) | 118 | const ranges = getRangesFromPlaylist(playlistContent.toString()) |
89 | await read(fd, buf, 0, range.length, range.offset) | ||
90 | 119 | ||
91 | rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf) | 120 | const fd = await open(videoPath, 'r') |
92 | } | 121 | for (const range of ranges) { |
93 | await close(fd) | 122 | const buf = Buffer.alloc(range.length) |
123 | await read(fd, buf, 0, range.length, range.offset) | ||
94 | 124 | ||
95 | const videoFilename = file.filename | 125 | rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf) |
96 | json[videoFilename] = rangeHashes | 126 | } |
127 | await close(fd) | ||
128 | |||
129 | const videoFilename = file.filename | ||
130 | json[videoFilename] = rangeHashes | ||
131 | }) | ||
97 | }) | 132 | }) |
98 | }) | 133 | } |
99 | } | ||
100 | 134 | ||
101 | const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename) | 135 | if (playlist.segmentsSha256Filename) { |
102 | await outputJSON(outputPath, json) | 136 | await video.removeStreamingPlaylistFile(playlist, playlist.segmentsSha256Filename) |
137 | } | ||
138 | playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive) | ||
103 | 139 | ||
104 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { | 140 | const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename) |
105 | await storeHLSFile(playlist, playlist.segmentsSha256Filename) | 141 | await outputJSON(outputPath, json) |
106 | await remove(outputPath) | 142 | |
107 | } | 143 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { |
144 | playlist.segmentsSha256Url = await storeHLSFile(playlist, playlist.segmentsSha256Filename) | ||
145 | await remove(outputPath) | ||
146 | } | ||
147 | |||
148 | return playlist.save() | ||
149 | }) | ||
108 | } | 150 | } |
109 | 151 | ||
152 | // --------------------------------------------------------------------------- | ||
153 | |||
110 | async function buildSha256Segment (segmentPath: string) { | 154 | async function buildSha256Segment (segmentPath: string) { |
111 | const buf = await readFile(segmentPath) | 155 | const buf = await readFile(segmentPath) |
112 | return sha256(buf) | 156 | return sha256(buf) |
@@ -190,7 +234,8 @@ export { | |||
190 | updateSha256VODSegments, | 234 | updateSha256VODSegments, |
191 | buildSha256Segment, | 235 | buildSha256Segment, |
192 | downloadPlaylistSegments, | 236 | downloadPlaylistSegments, |
193 | updateStreamingPlaylistsInfohashesIfNeeded | 237 | updateStreamingPlaylistsInfohashesIfNeeded, |
238 | updatePlaylistAfterFileChange | ||
194 | } | 239 | } |
195 | 240 | ||
196 | // --------------------------------------------------------------------------- | 241 | // --------------------------------------------------------------------------- |