42 lines
2.9 KiB
JavaScript
42 lines
2.9 KiB
JavaScript
/* Shared bounded control protocol: media bytes belong exclusively on DataChannel. */
|
|
(function (root) {
|
|
'use strict';
|
|
const CHUNK = 65536;
|
|
const text = (v, n) => typeof v === 'string' && v.length <= n && !/[\u0000-\u001f]/.test(v);
|
|
const id = (v) => text(v, 128) && /^[A-Za-z0-9_-]+$/.test(v);
|
|
function file(v) {
|
|
if (!v || !id(v.id) || !/^[a-f0-9]{64}$/.test(v.cid) || !Number.isSafeInteger(v.size) || v.size <= 0 || v.size > 4 * 1024 ** 3) return null;
|
|
if (!text(v.title, 300) || !text(v.channel || '', 200)) return null;
|
|
const streamMime = typeof v.streamMime === 'string' && /^video\/mp4; codecs="avc1\.[a-f0-9]{6}(, mp4a\.40\.2)?"$/.test(v.streamMime) ? v.streamMime : '';
|
|
return { streamMime, extension: /^(mp4|m4a|webm|ogg|opus|mkv|mp3|wav|aac)$/.test(v.extension) ? v.extension : 'mp4', id: v.id, cid: v.cid, size: v.size, title: v.title, channel: v.channel || '', duration: Number.isFinite(v.duration) && v.duration >= 0 ? v.duration : 0 };
|
|
}
|
|
function parse(value) {
|
|
if (typeof value === 'string') { if (value.length > 65536) return null; try { value = JSON.parse(value); } catch { return null; } }
|
|
if (!value || typeof value !== 'object' || Array.isArray(value) || value.type !== 'direct') return null;
|
|
const m = { type: 'direct', action: value.action };
|
|
if (value.action === 'request') return id(value.to) && id(value.id) ? { ...m, to: value.to, id: value.id } : null;
|
|
if (value.action === 'invite') {
|
|
const f = file(value.file); if (!id(value.to) || !f) return null;
|
|
return { ...m, to: value.to, file: f };
|
|
}
|
|
if (!['accept', 'decline', 'signal', 'complete'].includes(value.action) || !id(value.token)) return null;
|
|
m.token = value.token;
|
|
if (value.action === 'signal') {
|
|
const d = value.data;
|
|
if (!d || typeof d !== 'object') return null;
|
|
if (['offer', 'answer'].includes(d.kind) && typeof d.sdp === 'string' && d.sdp.length <= 60000) m.data = { kind: d.kind, sdp: d.sdp };
|
|
else if (d.kind === 'ice' && d.candidate && text(d.candidate.candidate, 2048)) {
|
|
const c = d.candidate;
|
|
if (!(c.sdpMid == null || text(c.sdpMid, 64)) || !(c.sdpMLineIndex == null || Number.isInteger(c.sdpMLineIndex) && c.sdpMLineIndex >= 0 && c.sdpMLineIndex < 100)) return null;
|
|
m.data = { kind: 'ice', candidate: { candidate: c.candidate, sdpMid: c.sdpMid ?? null, sdpMLineIndex: c.sdpMLineIndex ?? null } };
|
|
} else return null;
|
|
}
|
|
return m;
|
|
}
|
|
function offset(v, size) { if (!Number.isSafeInteger(v) || v < 0 || v > size) throw new Error('Invalid resume offset'); return v; }
|
|
function nextChunk(pos, size) { offset(pos, size); return { start: pos, end: Math.min(size, pos + CHUNK) }; }
|
|
const api = { CHUNK, file, parse, offset, nextChunk };
|
|
if (typeof module !== 'undefined') module.exports = api;
|
|
root.DirectProtocol = api;
|
|
}(typeof globalThis !== 'undefined' ? globalThis : this));
|