diff options
Diffstat (limited to 'src/media-upload.js')
| -rw-r--r-- | src/media-upload.js | 49 |
1 files changed, 48 insertions, 1 deletions
diff --git a/src/media-upload.js b/src/media-upload.js index c06771b..d7a99e1 100644 --- a/src/media-upload.js +++ b/src/media-upload.js @@ -1,3 +1,4 @@ +import { mediaServers } from './media-servers.js'; import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex } from '@noble/hashes/utils'; @@ -65,7 +66,7 @@ function putWithProgress(url, { body, headers, signal }, onProgress) { // BUD-02 upload. `signEvent` is deliberately injected: the app can use its // always-available wallet identity, while tests need no secret key. export async function uploadPublicMedia(file, signEvent, { - servers = PUBLIC_MEDIA_SERVERS, + servers = mediaServers(PUBLIC_MEDIA_SERVERS), fetcher = globalThis.fetch, timeoutMs = 5 * 60_000, onProgress = null, // (fraction 0..1) as the bytes go out @@ -115,3 +116,49 @@ export async function uploadPublicMedia(file, signEvent, { if (failures.length && failures.every((x) => x.status === 415)) throw new MediaUploadError('unsupported', details); throw new MediaUploadError('failed', details); } + +// An HLS stream (streamVideo) onto one host: the media files first, then +// each playlist with its files' addresses written in, then the master +// playlist naming those — its address is the post's. One host for the lot, +// and one that serves a playlist as one (ditto and nostr.download do; +// primal's says text/plain, nostr.build won't take it); a host that refuses +// any piece is passed over for the next. `files` maps each name the +// playlists use to its File. +export const STREAM_SERVERS = ['https://blossom.ditto.pub', 'https://nostr.download', 'https://blossom.primal.net']; +export function rewritePlaylist(text, urls) { + const to = (p) => urls.get(p) || p; + return text.split('\n').map((line) => { + const l = line.trim(); + if (l && !l.startsWith('#')) return to(l); + return line.replace(/URI="([^"]+)"/g, (m, p) => `URI="${to(p)}"`); + }).join('\n'); +} +export async function uploadStream({ files, master }, signEvent, { servers = mediaServers(STREAM_SERVERS), fetcher, onProgress = null } = {}) { + const media = [...files.keys()].filter((p) => !/\.m3u8$/.test(p)); + const lists = [...files.keys()].filter((p) => /\.m3u8$/.test(p) && p !== master); + const total = [...files.values()].reduce((n, f) => n + f.size, 0) || 1; + let lastError = null; + for (const server of servers) { + let done = 0; + const put = async (file) => { + const url = await uploadPublicMedia(file, signEvent, { + servers: [server], ...(fetcher ? { fetcher } : {}), + onProgress: onProgress && ((p) => onProgress(Math.min(1, (done + p * file.size) / total))), + }); + done += file.size; + if (onProgress) onProgress(Math.min(1, done / total)); // (the playlists grow a little with the addresses in) + return url; + }; + try { + const urls = new Map(); + for (const p of media) urls.set(p, await put(files.get(p))); + for (const p of lists) { + const text = rewritePlaylist(await files.get(p).text(), urls); + urls.set(p, await put(new File([text], p, { type: 'application/vnd.apple.mpegurl' }))); + } + const text = rewritePlaylist(await files.get(master).text(), urls); + return await put(new File([text], master, { type: 'application/vnd.apple.mpegurl' })); + } catch (error) { lastError = error; } + } + throw lastError || new MediaUploadError('failed'); +} |
