Prefer direct copies for playback, saves and exports within verified device groups
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
/* Paired-room transport; signalling only goes to the server. */
|
||||
(function (root) {
|
||||
'use strict';
|
||||
const waiting = new Map();
|
||||
let profilePeers = [];
|
||||
const rooms = new Map(), transfers = new Map(), pending = new Map();
|
||||
let hooks = { settings: () => ({}), persist() {}, current: () => null, saved() {}, notify() {}, fallback() {} };
|
||||
const enabled = () => hooks.settings().directTransfer !== false && !!root.RTCPeerConnection;
|
||||
@@ -24,7 +26,7 @@
|
||||
if (s.done) return; s.done = true; clearTimeout(s.timer); clearTimeout(s.idle);
|
||||
s.pc?.close(); transfers.delete(s.token);
|
||||
if (s.worker) { s.worker.postMessage({ op: 'abort' }); setTimeout(() => s.worker.terminate(), 1000); }
|
||||
if (error) { hooks.notify(`${error}. Partial copy kept for resume.`); hooks.fallback(s.claim, () => offer(s.key, s.peer, s.claim).catch(e => hooks.notify(e.message))); }
|
||||
if (error) { waiting.get(s.claim.id)?.resolve(false); waiting.delete(s.claim.id); hooks.notify(`${error}. Partial copy kept for resume.`); hooks.fallback(s.claim, () => offer(s.key, s.peer, s.claim).catch(e => hooks.notify(e.message))); }
|
||||
}
|
||||
function connection(s, receiver) {
|
||||
const pc = s.pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
|
||||
@@ -77,13 +79,14 @@
|
||||
const offer = await s.pc.createOffer(); await s.pc.setLocalDescription(offer); signal(key, m.token, { kind: 'offer', sdp: offer.sdp });
|
||||
} else if (result.op === 'done') {
|
||||
send(key, { type: 'direct', action: 'complete', token: m.token });
|
||||
await hooks.saved(result.file); hooks.notify(`Saved ${result.file.title} directly from the other device`); finish(s);
|
||||
await hooks.saved(result.file); waiting.get(result.file.id)?.resolve(true); waiting.delete(result.file.id); publish(); hooks.notify(`Saved ${result.file.title} directly from the other device`); finish(s);
|
||||
} else if (result.op === 'error') finish(s, result.error);
|
||||
};
|
||||
s.worker.postMessage({ op: 'open', file: m.file });
|
||||
}
|
||||
async function handle(key, m) {
|
||||
if (m.type !== 'direct') return;
|
||||
if (m.action === 'request') { if (enabled()) offer(key, m.from, hooks.meta?.(m.id) || { id: m.id, title: m.id }).catch(e => hooks.notify(e.message)); return; }
|
||||
try {
|
||||
if (m.action === 'invite') {
|
||||
if (!enabled() || !root.DirectProtocol.file(m.file)) return send(key, { type: 'direct', action: 'decline', token: m.token });
|
||||
@@ -114,15 +117,37 @@
|
||||
const text = document.createElement('p'); text.textContent = `${m.file.title} · ${(m.file.size / 1024 ** 2).toFixed(1)} MB. Media travels directly between devices. Keep both screens open. Playback starts after the full copy is verified.`;
|
||||
const yes = document.createElement('button'); yes.textContent = 'Accept & save';
|
||||
const no = document.createElement('button'); no.textContent = 'Decline';
|
||||
const decline = () => { send(key, { type: 'direct', action: 'decline', token: m.token }); dialog.remove(); };
|
||||
const decline = () => { waiting.get(m.file.id)?.resolve(false); waiting.delete(m.file.id); send(key, { type: 'direct', action: 'decline', token: m.token }); dialog.remove(); };
|
||||
yes.onclick = () => { dialog.remove(); accept(key, m).catch(e => hooks.notify(e.message)); }; no.onclick = decline; dialog.oncancel = decline;
|
||||
dialog.append(heading, text, yes, no); document.body.append(dialog); dialog.showModal();
|
||||
}
|
||||
async function publish() {
|
||||
if (!root.P2PClient?.isConnected()) return;
|
||||
const files = await root.OPFS.listVideos({ strict: true });
|
||||
root.P2PClient.send({ type: 'direct-inventory', ids: enabled() && hooks.settings().p2pShare !== false ? files.map(f => f.id) : [] });
|
||||
}
|
||||
function source(id) { if (!enabled()) return null; for (const [key, room] of rooms) { const peer = room.peers().find(p => p.files?.includes(id)); if (peer) return { key, peer }; } return null; }
|
||||
function hasSource(id) { return !!source(id); }
|
||||
function obtain(id) {
|
||||
if (!hasSource(id)) return Promise.resolve(null);
|
||||
if (waiting.has(id)) return waiting.get(id).promise;
|
||||
const { key, peer } = source(id);
|
||||
let resolve; const promise = new Promise(r => { resolve = r; });
|
||||
waiting.set(id, { promise, resolve });
|
||||
send(key, { type: 'direct', action: 'request', to: peer.id, id });
|
||||
setTimeout(() => { if (waiting.get(id)?.promise === promise) { waiting.delete(id); resolve(false); hooks.notify('The other device did not accept or is unavailable'); hooks.fallback(hooks.meta?.(id) || { id, title: id }); } }, 60000);
|
||||
return promise;
|
||||
}
|
||||
function configure(h) {
|
||||
hooks = { ...hooks, ...h };
|
||||
if (root.P2PClient) {
|
||||
root.P2PClient.onMessage('hello', () => { attach('profile', { send: root.P2PClient.send, peers: () => profilePeers }); publish(); });
|
||||
root.P2PClient.onMessage('direct-peers', m => { profilePeers = m.peers || []; });
|
||||
root.P2PClient.onMessage('direct', m => handle('profile', m));
|
||||
}
|
||||
root.SettingsSections.register({ id: 'direct-transfer', title: 'Direct device transfer', cluster: 'Library & storage', summary: () => enabled() ? 'On · paired devices only' : 'Off or unsupported', icon: '<path d="M4 6h6v12H4zM14 6h6v12h-6M10 10h4m-4 4h4"/>', render(container) {
|
||||
const label = document.createElement('label'); const toggle = document.createElement('input'); toggle.type = 'checkbox'; toggle.checked = hooks.settings().directTransfer !== false;
|
||||
toggle.onchange = () => { hooks.settings().directTransfer = toggle.checked; hooks.persist(); }; label.append(toggle, ' Direct device transfer (P2P)');
|
||||
toggle.onchange = () => { hooks.settings().directTransfer = toggle.checked; hooks.persist(); publish(); }; label.append(toggle, ' Direct device transfer (P2P)');
|
||||
const help = document.createElement('p'); help.textContent = 'Pair through Remote or join a Watch Party, then send your currently playing saved song. The receiver confirms before saving. Keep both devices open. No TURN relay is used.';
|
||||
container.append(label, help);
|
||||
for (const [key, r] of rooms) for (const peer of r.peers()) {
|
||||
@@ -130,5 +155,5 @@
|
||||
}
|
||||
} });
|
||||
}
|
||||
root.DirectMedia = { configure, attach, handle, offer, enabled };
|
||||
root.DirectMedia = { configure, attach, handle, offer, enabled, obtain, hasSource, publish };
|
||||
}(globalThis));
|
||||
|
||||
Reference in New Issue
Block a user