// mock.js — MockProvider. Built FIRST, on purpose. // // Proves the UI is fully source-agnostic with ZERO network: fake channels, a // rolling "scheduled-live" EPG timeline (so now/next + the guide populate), and // a couple of real CORS-clean public TEST streams so playback can be exercised // standalone (no relay needed). One channel points at a dead URL to exercise the // "the orb clouds over" no-signal path. import { SourceProvider } from './source-provider.js'; /** Tiny inline-SVG logo so channels render with no network. */ function logo(initials, accent = '#ff2b2b') { const svg = `` + `` + `` + `${initials}`; return 'data:image/svg+xml,' + encodeURIComponent(svg); } // CORS-clean public test streams (HLS). Stable, broadcaster-hosted demos. const TEST_STREAMS = { bipbop: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8', mux: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', bbb: 'https://test-streams.mux.dev/test_001/stream.m3u8', dead: 'https://stream.invalid.example/offline.m3u8', }; const TITLE_POOL = { 'Arcane Cinema': ['The Reanimator', 'Hex of the Hollow', 'Salt & Sigil', 'Midnight Grimoire', 'The Pondering Hour', 'Witchlight', 'The Veil Lifts', 'Candle & Bone'], 'Hack the Planet': ['Packets at Dawn', 'Root of All Evil', 'The Blue Box', 'Zero Cool', 'Buffer Overflow', 'The Last Daemon', 'Kernel Panic', 'Shell Games'], 'Old Signals': ['Test Pattern Theatre', 'Static & Snow', 'The Late Late Broadcast', 'Color Bars', 'Sign-Off Sermon', 'Dead Air'], 'The Almanac': ['What Fate Foretells', 'Stars & Portents', 'The Long Now', 'Tomorrow, Maybe'], }; function buildSchedule(channelId, group, now) { /** @type {import('./source-provider.js').Program[]} */ const programs = []; const titles = TITLE_POOL[group] || TITLE_POOL['Old Signals']; // Deterministic-ish per channel so reloads look stable. let seed = [...channelId].reduce((a, c) => a + c.charCodeAt(0), 0); const rand = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; }; // Start two hours back, fill ~10h. let t = now - 2 * 3600 * 1000; t = t - (t % (30 * 60 * 1000)); // align to :00/:30 const end = now + 8 * 3600 * 1000; let i = Math.floor(rand() * titles.length); while (t < end) { const slots = 1 + Math.floor(rand() * 3); // 30/60/90 min const dur = slots * 30 * 60 * 1000; programs.push({ channelId, title: titles[i % titles.length], desc: 'A scheduled broadcast. Tuning in drops you mid-program, live — just like real TV.', start: t, stop: t + dur, }); t += dur; i++; } return programs; } const CHANNELS = [ { n: 2, name: 'Arcane Cinema', group: 'Arcane Cinema', stream: 'mux', init: 'AC' }, { n: 3, name: 'Hack the Planet',group: 'Hack the Planet', stream: 'bbb', init: 'HP' }, { n: 4, name: 'Bip-Bop Test', group: 'Old Signals', stream: 'bipbop', init: 'BB' }, { n: 5, name: 'The Late Show', group: 'Old Signals', stream: 'mux', init: 'LS' }, { n: 6, name: 'Dead Air', group: 'Old Signals', stream: 'dead', init: 'XX' }, { n: 7, name: 'The Almanac', group: 'The Almanac', stream: 'bbb', init: 'AL' }, ]; export class MockProvider extends SourceProvider { get id() { return 'mock'; } get name() { return 'The Mock Realm'; } get supportsGuides() { return true; } async listChannels() { return CHANNELS.map((c) => ({ id: `mock:${c.n}`, source: 'mock', name: c.name, number: c.n, logo: logo(c.init), group: c.group, country: 'XX', language: 'eng', kind: 'live', streamUrls: [TEST_STREAMS[c.stream]], probeUrls: [TEST_STREAMS[c.stream]], guideId: `mock:${c.n}`, })); } async getStreamHandle(channelId) { const c = CHANNELS.find((x) => `mock:${x.n}` === channelId); if (!c) return null; // Mock returns direct URLs (test streams are CORS-clean) so the UI runs standalone. return { url: TEST_STREAMS[c.stream], protocol: 'hls' }; } async getPrograms(channelId, window) { const c = CHANNELS.find((x) => `mock:${x.n}` === channelId); if (!c) return []; const all = buildSchedule(channelId, c.group, Date.now()); return all.filter((p) => p.stop > window.from && p.start < window.to); } async healthCheck(channelId) { const c = CHANNELS.find((x) => `mock:${x.n}` === channelId); if (c && c.stream === 'dead') return { ok: false, error: 'mock offline channel' }; return { ok: true }; } }