aboutsummaryrefslogtreecommitdiffstats
path: root/src/media-upload.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/media-upload.js')
-rw-r--r--src/media-upload.js49
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');
+}