diff options
author | Chocobozzz <me@florianbigard.com> | 2019-02-07 15:08:19 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-02-11 09:13:02 +0100 |
commit | 4c280004ce62bf11ddb091854c28f1e1d54a54d6 (patch) | |
tree | 1899fff4ef18f8663a865997d5d06119b2149319 /server/lib/hls.ts | |
parent | 6ec0b75beb9c8bcd84e178912319913b91830da2 (diff) | |
download | PeerTube-4c280004ce62bf11ddb091854c28f1e1d54a54d6.tar.gz PeerTube-4c280004ce62bf11ddb091854c28f1e1d54a54d6.tar.zst PeerTube-4c280004ce62bf11ddb091854c28f1e1d54a54d6.zip |
Use a single file instead of segments for HLS
Diffstat (limited to 'server/lib/hls.ts')
-rw-r--r-- | server/lib/hls.ts | 136 |
1 files changed, 95 insertions, 41 deletions
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 10db6c3c3..3575981f4 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -1,13 +1,14 @@ | |||
1 | import { VideoModel } from '../models/video/video' | 1 | import { VideoModel } from '../models/video/video' |
2 | import { basename, dirname, join } from 'path' | 2 | import { basename, join, dirname } from 'path' |
3 | import { HLS_PLAYLIST_DIRECTORY, CONFIG } from '../initializers' | 3 | import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers' |
4 | import { outputJSON, pathExists, readdir, readFile, remove, writeFile, move } from 'fs-extra' | 4 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' |
5 | import { getVideoFileSize } from '../helpers/ffmpeg-utils' | 5 | import { getVideoFileSize } from '../helpers/ffmpeg-utils' |
6 | import { sha256 } from '../helpers/core-utils' | 6 | import { sha256 } from '../helpers/core-utils' |
7 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 7 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
8 | import HLSDownloader from 'hlsdownloader' | ||
9 | import { logger } from '../helpers/logger' | 8 | import { logger } from '../helpers/logger' |
10 | import { parse } from 'url' | 9 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' |
10 | import { generateRandomString } from '../helpers/utils' | ||
11 | import { flatten, uniq } from 'lodash' | ||
11 | 12 | ||
12 | async function updateMasterHLSPlaylist (video: VideoModel) { | 13 | async function updateMasterHLSPlaylist (video: VideoModel) { |
13 | const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | 14 | const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) |
@@ -37,66 +38,119 @@ async function updateMasterHLSPlaylist (video: VideoModel) { | |||
37 | } | 38 | } |
38 | 39 | ||
39 | async function updateSha256Segments (video: VideoModel) { | 40 | async function updateSha256Segments (video: VideoModel) { |
40 | const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | 41 | const json: { [filename: string]: { [range: string]: string } } = {} |
41 | const files = await readdir(directory) | 42 | |
42 | const json: { [filename: string]: string} = {} | 43 | const playlistDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) |
44 | |||
45 | // For all the resolutions available for this video | ||
46 | for (const file of video.VideoFiles) { | ||
47 | const rangeHashes: { [range: string]: string } = {} | ||
48 | |||
49 | const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)) | ||
50 | const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
43 | 51 | ||
44 | for (const file of files) { | 52 | // Maybe the playlist is not generated for this resolution yet |
45 | if (file.endsWith('.ts') === false) continue | 53 | if (!await pathExists(playlistPath)) continue |
46 | 54 | ||
47 | const buffer = await readFile(join(directory, file)) | 55 | const playlistContent = await readFile(playlistPath) |
48 | const filename = basename(file) | 56 | const ranges = getRangesFromPlaylist(playlistContent.toString()) |
49 | 57 | ||
50 | json[filename] = sha256(buffer) | 58 | const fd = await open(videoPath, 'r') |
59 | for (const range of ranges) { | ||
60 | const buf = Buffer.alloc(range.length) | ||
61 | await read(fd, buf, 0, range.length, range.offset) | ||
62 | |||
63 | rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf) | ||
64 | } | ||
65 | await close(fd) | ||
66 | |||
67 | const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution) | ||
68 | json[videoFilename] = rangeHashes | ||
51 | } | 69 | } |
52 | 70 | ||
53 | const outputPath = join(directory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | 71 | const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) |
54 | await outputJSON(outputPath, json) | 72 | await outputJSON(outputPath, json) |
55 | } | 73 | } |
56 | 74 | ||
57 | function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) { | 75 | function getRangesFromPlaylist (playlistContent: string) { |
58 | let timer | 76 | const ranges: { offset: number, length: number }[] = [] |
77 | const lines = playlistContent.split('\n') | ||
78 | const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/ | ||
59 | 79 | ||
60 | logger.info('Importing HLS playlist %s', playlistUrl) | 80 | for (const line of lines) { |
81 | const captured = regex.exec(line) | ||
61 | 82 | ||
62 | const params = { | 83 | if (captured) { |
63 | playlistURL: playlistUrl, | 84 | ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) }) |
64 | destination: CONFIG.STORAGE.TMP_DIR | 85 | } |
65 | } | 86 | } |
66 | const downloader = new HLSDownloader(params) | ||
67 | |||
68 | const hlsDestinationDir = join(CONFIG.STORAGE.TMP_DIR, dirname(parse(playlistUrl).pathname)) | ||
69 | 87 | ||
70 | return new Promise<string>(async (res, rej) => { | 88 | return ranges |
71 | downloader.startDownload(err => { | 89 | } |
72 | clearTimeout(timer) | ||
73 | 90 | ||
74 | if (err) { | 91 | function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) { |
75 | deleteTmpDirectory(hlsDestinationDir) | 92 | let timer |
76 | 93 | ||
77 | return rej(err) | 94 | logger.info('Importing HLS playlist %s', playlistUrl) |
78 | } | ||
79 | 95 | ||
80 | move(hlsDestinationDir, destinationDir, { overwrite: true }) | 96 | return new Promise<string>(async (res, rej) => { |
81 | .then(() => res()) | 97 | const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10)) |
82 | .catch(err => { | ||
83 | deleteTmpDirectory(hlsDestinationDir) | ||
84 | 98 | ||
85 | return rej(err) | 99 | await ensureDir(tmpDirectory) |
86 | }) | ||
87 | }) | ||
88 | 100 | ||
89 | timer = setTimeout(() => { | 101 | timer = setTimeout(() => { |
90 | deleteTmpDirectory(hlsDestinationDir) | 102 | deleteTmpDirectory(tmpDirectory) |
91 | 103 | ||
92 | return rej(new Error('HLS download timeout.')) | 104 | return rej(new Error('HLS download timeout.')) |
93 | }, timeout) | 105 | }, timeout) |
94 | 106 | ||
95 | function deleteTmpDirectory (directory: string) { | 107 | try { |
96 | remove(directory) | 108 | // Fetch master playlist |
97 | .catch(err => logger.error('Cannot delete path on HLS download error.', { err })) | 109 | const subPlaylistUrls = await fetchUniqUrls(playlistUrl) |
110 | |||
111 | const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u)) | ||
112 | const fileUrls = uniq(flatten(await Promise.all(subRequests))) | ||
113 | |||
114 | logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls }) | ||
115 | |||
116 | for (const fileUrl of fileUrls) { | ||
117 | const destPath = join(tmpDirectory, basename(fileUrl)) | ||
118 | |||
119 | await doRequestAndSaveToFile({ uri: fileUrl }, destPath) | ||
120 | } | ||
121 | |||
122 | clearTimeout(timer) | ||
123 | |||
124 | await move(tmpDirectory, destinationDir, { overwrite: true }) | ||
125 | |||
126 | return res() | ||
127 | } catch (err) { | ||
128 | deleteTmpDirectory(tmpDirectory) | ||
129 | |||
130 | return rej(err) | ||
98 | } | 131 | } |
99 | }) | 132 | }) |
133 | |||
134 | function deleteTmpDirectory (directory: string) { | ||
135 | remove(directory) | ||
136 | .catch(err => logger.error('Cannot delete path on HLS download error.', { err })) | ||
137 | } | ||
138 | |||
139 | async function fetchUniqUrls (playlistUrl: string) { | ||
140 | const { body } = await doRequest<string>({ uri: playlistUrl }) | ||
141 | |||
142 | if (!body) return [] | ||
143 | |||
144 | const urls = body.split('\n') | ||
145 | .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4')) | ||
146 | .map(url => { | ||
147 | if (url.startsWith('http://') || url.startsWith('https://')) return url | ||
148 | |||
149 | return `${dirname(playlistUrl)}/${url}` | ||
150 | }) | ||
151 | |||
152 | return uniq(urls) | ||
153 | } | ||
100 | } | 154 | } |
101 | 155 | ||
102 | // --------------------------------------------------------------------------- | 156 | // --------------------------------------------------------------------------- |