diff options
| author | Adam Soltys <[email protected]> | 2026-10-18 05:54:23 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-18 05:54:23 -0700 |
| commit | b9297719d294779a2ea14c77a67975308bace65c (patch) | |
| tree | d005658191f0ce4dec1a97212d6f4525900b60c0 /src/media-upload.js | |
| parent | 31661e28b1a010fa67b3a2527c43a129ab2a41cc (diff) | |
| download | coinosv3-b9297719d294779a2ea14c77a67975308bace65c.tar.gz coinosv3-b9297719d294779a2ea14c77a67975308bace65c.zip | |
HLS for long videos: vmedia serves a long clip as a 360p/720p stream made as it's watched (/vmedia/hls.m3u8), the feed and pager play long clips and posted .m3u8 streams through the browser's own HLS, and a long video posted from the app goes up as an HLS stream made on the device (master playlist on Blossom).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
Diffstat (limited to 'src/media-upload.js')
| -rw-r--r-- | src/media-upload.js | 46 |
1 files changed, 46 insertions, 0 deletions
diff --git a/src/media-upload.js b/src/media-upload.js index c06771b..e45d2fe 100644 --- a/src/media-upload.js +++ b/src/media-upload.js @@ -115,3 +115,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 = 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'); +} |
