/* Progressive preview is only safe for fragmented MP4, never ordinary MP4 chunks. */ (function (root) { 'use strict'; function inspectBytes(bytes) { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const str = (at, n) => String.fromCharCode(...bytes.subarray(at, at + n)); let at = 0, moov = null; while (at + 8 <= bytes.length) { const size = view.getUint32(at); if (size < 8 || at + size > bytes.length) break; if (str(at + 4, 4) === 'moov') { moov = bytes.subarray(at + 8, at + size); break; } at += size; } if (!moov) return null; const tagAt = (tag) => { for (let i = 4; i + 4 <= moov.length; i++) if (String.fromCharCode(...moov.subarray(i, i + 4)) === tag) return i; return -1; }; if (tagAt('mvex') < 0) return null; const avc = tagAt('avcC'), audio = tagAt('mp4a'); if (avc < 0 || avc + 8 > moov.length || moov[avc + 4] !== 1) return null; const codec = Array.from(moov.subarray(avc + 5, avc + 8), b => b.toString(16).padStart(2, '0')).join(''); return `video/mp4; codecs="avc1.${codec}${audio >= 0 ? ', mp4a.40.2' : ''}"`; } async function inspect(file) { return inspectBytes(new Uint8Array(await file.slice(0, 1024 * 1024).arrayBuffer())); } function create(mime, title) { if (!root.MediaSource?.isTypeSupported(mime)) return null; const media = new MediaSource(), url = URL.createObjectURL(media), queue = []; let buffer, queued = 0, ended = false, stopped = false; const dialog = document.createElement('dialog'); dialog.className = 'direct-transfer-dialog'; const heading = document.createElement('h2'); heading.textContent = title; const note = document.createElement('p'); note.textContent = 'Direct preview ยท the full copy is still being saved and verified. Tap Play when ready.'; const video = document.createElement('video'); video.controls = true; video.playsInline = true; video.src = url; video.style.width = '100%'; const close = document.createElement('button'); close.textContent = 'Close preview'; const dispose = () => { stopped = true; queue.length = 0; video.pause(); video.removeAttribute('src'); video.load(); URL.revokeObjectURL(url); dialog.remove(); }; close.onclick = dispose; dialog.oncancel = e => { e.preventDefault(); dispose(); }; dialog.append(heading, note, video, close); document.body.append(dialog); dialog.showModal(); function flush() { if (stopped || !buffer || buffer.updating) return; if (queue.length) { const b = queue.shift(); queued -= b.byteLength; try { buffer.appendBuffer(b); } catch { dispose(); } } else if (ended && media.readyState === 'open') { try { media.endOfStream(); } catch {} } } media.addEventListener('sourceopen', () => { try { buffer = media.addSourceBuffer(mime); buffer.addEventListener('updateend', flush); buffer.addEventListener('error', dispose); flush(); } catch { dispose(); } }, { once: true }); return { push(bytes) { if (stopped) return; queued += bytes.byteLength; if (queued > 8 * 1024 * 1024) { dispose(); return; } queue.push(bytes.slice(0)); flush(); }, end() { ended = true; flush(); }, close: dispose }; } const api = { inspectBytes, inspect, create }; if (typeof module !== 'undefined') module.exports = api; root.DirectStream = api; }(globalThis));