aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/hls.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/hls.ts')
-rw-r--r--server/lib/hls.ts147
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 @@
1import { close, ensureDir, move, open, outputJSON, read, readFile, remove, stat, writeFile } from 'fs-extra' 1import { close, ensureDir, move, open, outputJSON, read, readFile, remove, stat, writeFile } from 'fs-extra'
2import { flatten, uniq } from 'lodash' 2import { flatten, uniq } from 'lodash'
3import PQueue from 'p-queue'
3import { basename, dirname, join } from 'path' 4import { basename, dirname, join } from 'path'
4import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models' 5import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models'
5import { sha256 } from '@shared/extra-utils' 6import { sha256 } from '@shared/extra-utils'
6import { VideoStorage } from '@shared/models' 7import { VideoStorage } from '@shared/models'
7import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg' 8import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg'
@@ -14,7 +15,7 @@ import { sequelizeTypescript } from '../initializers/database'
14import { VideoFileModel } from '../models/video/video-file' 15import { VideoFileModel } from '../models/video/video-file'
15import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 16import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
16import { storeHLSFile } from './object-storage' 17import { storeHLSFile } from './object-storage'
17import { getHlsResolutionPlaylistFilename } from './paths' 18import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHlsResolutionPlaylistFilename } from './paths'
18import { VideoPathManager } from './video-path-manager' 19import { VideoPathManager } from './video-path-manager'
19 20
20async function updateStreamingPlaylistsInfohashesIfNeeded () { 21async function updateStreamingPlaylistsInfohashesIfNeeded () {
@@ -33,80 +34,123 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () {
33 } 34 }
34} 35}
35 36
36async function updateMasterHLSPlaylist (video: MVideo, playlist: MStreamingPlaylistFilesVideo) { 37async 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 52const playlistFilesQueue = new PQueue({ concurrency: 1 })
50 53
51 const codecs = await Promise.all([ 54function 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
72async function updateSha256VODSegments (video: MVideoUUID, playlist: MStreamingPlaylistFilesVideo) { 101// ---------------------------------------------------------------------------
73 const json: { [filename: string]: { [range: string]: string } } = {} 102
103async 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
110async function buildSha256Segment (segmentPath: string) { 154async 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// ---------------------------------------------------------------------------