Files
ytplayer/frontend/direct-stream.js

45 lines
3.3 KiB
JavaScript

/* 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));