]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/hls.ts
Fix lint on hls.ts
[github/Chocobozzz/PeerTube.git] / server / lib / hls.ts
CommitLineData
0305db28 1import { close, ensureDir, move, open, outputJSON, read, readFile, remove, stat, writeFile } from 'fs-extra'
daf6e480 2import { flatten, uniq } from 'lodash'
1bb4c9ab 3import PQueue from 'p-queue'
daf6e480 4import { basename, dirname, join } from 'path'
1bb4c9ab 5import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models'
f304a158 6import { sha256 } from '@shared/extra-utils'
a2caee9f 7import { VideoStorage } from '@shared/models'
c729caf6 8import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg'
09209296 9import { logger } from '../helpers/logger'
4c280004
C
10import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
11import { generateRandomString } from '../helpers/utils'
6dd9de95 12import { CONFIG } from '../initializers/config'
7b0c61e7 13import { P2P_MEDIA_LOADER_PEER_VERSION, REQUEST_TIMEOUTS } from '../initializers/constants'
74dc3bca 14import { sequelizeTypescript } from '../initializers/database'
daf6e480
C
15import { VideoFileModel } from '../models/video/video-file'
16import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
a2caee9f 17import { storeHLSFile } from './object-storage'
1bb4c9ab 18import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHlsResolutionPlaylistFilename } from './paths'
0305db28 19import { VideoPathManager } from './video-path-manager'
ae9bbed4
C
20
21async function updateStreamingPlaylistsInfohashesIfNeeded () {
22 const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
23
24 // Use separate SQL queries, because we could have many videos to update
25 for (const playlist of playlistsToUpdate) {
26 await sequelizeTypescript.transaction(async t => {
27 const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t)
28
764b1a14 29 playlist.assignP2PMediaLoaderInfoHashes(playlist.Video, videoFiles)
0e9c48c2 30 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
764b1a14 31
ae9bbed4
C
32 await playlist.save({ transaction: t })
33 })
34 }
35}
09209296 36
1bb4c9ab
C
37async function updatePlaylistAfterFileChange (video: MVideo, playlist: MStreamingPlaylist) {
38 let playlistWithFiles = await updateMasterHLSPlaylist(video, playlist)
39 playlistWithFiles = await updateSha256VODSegments(video, playlist)
09209296 40
1bb4c9ab
C
41 // Refresh playlist, operations can take some time
42 playlistWithFiles = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlist.id)
43 playlistWithFiles.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
44 await playlistWithFiles.save()
83903cb6 45
1bb4c9ab
C
46 video.setHLSPlaylist(playlistWithFiles)
47}
09209296 48
1bb4c9ab 49// ---------------------------------------------------------------------------
09209296 50
1bb4c9ab
C
51// Avoid concurrency issues when updating streaming playlist files
52const playlistFilesQueue = new PQueue({ concurrency: 1 })
09209296 53
1bb4c9ab
C
54function updateMasterHLSPlaylist (video: MVideo, playlistArg: MStreamingPlaylist): Promise<MStreamingPlaylistFilesVideo> {
55 return playlistFilesQueue.add(async () => {
56 const playlist = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id)
d7b1c7b4 57
1bb4c9ab 58 const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
52201311 59
1bb4c9ab
C
60 for (const file of playlist.VideoFiles) {
61 const playlistFilename = getHlsResolutionPlaylistFilename(file.filename)
62
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 ])
09209296 76
1bb4c9ab
C
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)
a2caee9f
C
90 await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n')
91
92 if (playlist.storage === VideoStorage.OBJECT_STORAGE) {
1bb4c9ab
C
93 playlist.playlistUrl = await storeHLSFile(playlist, playlist.playlistFilename)
94 await remove(masterPlaylistPath)
a2caee9f 95 }
1bb4c9ab
C
96
97 return playlist.save()
0305db28 98 })
09209296
C
99}
100
1bb4c9ab
C
101// ---------------------------------------------------------------------------
102
b37d80e3 103function updateSha256VODSegments (video: MVideo, playlistArg: MStreamingPlaylist): Promise<MStreamingPlaylistFilesVideo> {
1bb4c9ab
C
104 return playlistFilesQueue.add(async () => {
105 const json: { [filename: string]: { [range: string]: string } } = {}
4c280004 106
1bb4c9ab 107 const playlist = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id)
4c280004 108
1bb4c9ab
C
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)
09209296 113
1bb4c9ab 114 await VideoPathManager.Instance.makeAvailableVideoFile(fileWithPlaylist, videoPath => {
09209296 115
1bb4c9ab
C
116 return VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(fileWithPlaylist, async resolutionPlaylistPath => {
117 const playlistContent = await readFile(resolutionPlaylistPath)
118 const ranges = getRangesFromPlaylist(playlistContent.toString())
09209296 119
1bb4c9ab
C
120 const fd = await open(videoPath, 'r')
121 for (const range of ranges) {
122 const buf = Buffer.alloc(range.length)
123 await read(fd, buf, 0, range.length, range.offset)
4c280004 124
1bb4c9ab
C
125 rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf)
126 }
127 await close(fd)
128
129 const videoFilename = file.filename
130 json[videoFilename] = rangeHashes
131 })
0305db28 132 })
1bb4c9ab 133 }
09209296 134
1bb4c9ab
C
135 if (playlist.segmentsSha256Filename) {
136 await video.removeStreamingPlaylistFile(playlist, playlist.segmentsSha256Filename)
137 }
138 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
a2caee9f 139
1bb4c9ab
C
140 const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename)
141 await outputJSON(outputPath, json)
142
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 })
09209296
C
150}
151
1bb4c9ab
C
152// ---------------------------------------------------------------------------
153
c6c0fa6c
C
154async function buildSha256Segment (segmentPath: string) {
155 const buf = await readFile(segmentPath)
156 return sha256(buf)
157}
158
18998c45 159function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number, bodyKBLimit: number) {
4c280004 160 let timer
18998c45 161 let remainingBodyKBLimit = bodyKBLimit
09209296 162
4c280004 163 logger.info('Importing HLS playlist %s', playlistUrl)
09209296 164
ba5a8d89 165 return new Promise<void>(async (res, rej) => {
4c280004 166 const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10))
09209296 167
4c280004 168 await ensureDir(tmpDirectory)
09209296
C
169
170 timer = setTimeout(() => {
4c280004 171 deleteTmpDirectory(tmpDirectory)
09209296
C
172
173 return rej(new Error('HLS download timeout.'))
174 }, timeout)
175
4c280004
C
176 try {
177 // Fetch master playlist
178 const subPlaylistUrls = await fetchUniqUrls(playlistUrl)
179
180 const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u))
181 const fileUrls = uniq(flatten(await Promise.all(subRequests)))
182
183 logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls })
184
185 for (const fileUrl of fileUrls) {
186 const destPath = join(tmpDirectory, basename(fileUrl))
187
a5ee023c 188 await doRequestAndSaveToFile(fileUrl, destPath, { bodyKBLimit: remainingBodyKBLimit, timeout: REQUEST_TIMEOUTS.REDUNDANCY })
18998c45
C
189
190 const { size } = await stat(destPath)
191 remainingBodyKBLimit -= (size / 1000)
192
193 logger.debug('Downloaded HLS playlist file %s with %d kB remained limit.', fileUrl, Math.floor(remainingBodyKBLimit))
4c280004
C
194 }
195
196 clearTimeout(timer)
197
198 await move(tmpDirectory, destinationDir, { overwrite: true })
199
200 return res()
201 } catch (err) {
202 deleteTmpDirectory(tmpDirectory)
203
204 return rej(err)
09209296
C
205 }
206 })
4c280004
C
207
208 function deleteTmpDirectory (directory: string) {
209 remove(directory)
210 .catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
211 }
212
213 async function fetchUniqUrls (playlistUrl: string) {
db4b15f2 214 const { body } = await doRequest(playlistUrl)
4c280004
C
215
216 if (!body) return []
217
218 const urls = body.split('\n')
219 .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4'))
220 .map(url => {
221 if (url.startsWith('http://') || url.startsWith('https://')) return url
222
223 return `${dirname(playlistUrl)}/${url}`
224 })
225
226 return uniq(urls)
227 }
09209296
C
228}
229
230// ---------------------------------------------------------------------------
231
232export {
233 updateMasterHLSPlaylist,
c6c0fa6c
C
234 updateSha256VODSegments,
235 buildSha256Segment,
ae9bbed4 236 downloadPlaylistSegments,
1bb4c9ab
C
237 updateStreamingPlaylistsInfohashesIfNeeded,
238 updatePlaylistAfterFileChange
09209296
C
239}
240
241// ---------------------------------------------------------------------------
b5b68755
C
242
243function getRangesFromPlaylist (playlistContent: string) {
244 const ranges: { offset: number, length: number }[] = []
245 const lines = playlistContent.split('\n')
246 const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/
247
248 for (const line of lines) {
249 const captured = regex.exec(line)
250
251 if (captured) {
252 ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) })
253 }
254 }
255
256 return ranges
257}