{"title":"Gibbler - P2P","description":"\u003cbody\u003e\n\n\n\u003cmeta charset=\"utf-8\"\u003e\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n\u003ctitle\u003eGIBBLER™ P2P Enhanced — Live Channels + MPC Plugins\u003c\/title\u003e\n\u003cstyle\u003e\n  :root {\n    --primary: #8B5CF6;\n    --primary-dark: #7C3AED;\n    --secondary: #06B6D4;\n    --success: #10B981;\n    --warning: #F59E0B;\n    --error: #EF4444;\n    --dark: #0F172A;\n    --dark-card: #1E293B;\n    --dark-border: #334155;\n    --light: #F8FAFC;\n    --text: #E2E8F0;\n    --text-muted: #94A3B8;\n    --gradient: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);\n    --glow: 0 0 20px rgba(139, 92, 246, 0.3);\n    --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n  }\n\n  * { box-sizing: border-box; }\n  \n  body {\n    margin: 0;\n    background: var(--dark);\n    color: var(--text);\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n    line-height: 1.6;\n    overflow-x: hidden;\n  }\n\n  .app-header {\n    background: var(--gradient);\n    padding: 20px 0;\n    text-align: center;\n    box-shadow: var(--shadow);\n    position: relative;\n    overflow: hidden;\n  }\n\n  .app-title {\n    position: relative;\n    margin: 0;\n    font-size: 2.5rem;\n    font-weight: 700;\n    color: white;\n    text-shadow: 0 2px 4px rgba(0,0,0,0.3);\n  }\n\n  .app-subtitle {\n    position: relative;\n    margin: 8px 0 0;\n    font-size: 1.1rem;\n    color: rgba(255,255,255,0.9);\n    font-weight: 400;\n  }\n\n  .container {\n    max-width: 1600px;\n    margin: 0 auto;\n    padding: 24px;\n  }\n\n  .grid {\n    display: grid;\n    grid-template-columns: 2fr 1fr 1fr;\n    gap: 24px;\n    margin-bottom: 24px;\n  }\n\n  .card {\n    background: var(--dark-card);\n    border: 1px solid var(--dark-border);\n    border-radius: 16px;\n    padding: 24px;\n    box-shadow: var(--shadow);\n    transition: all 0.3s ease;\n    position: relative;\n    overflow: hidden;\n  }\n\n  .card::before {\n    content: '';\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    height: 3px;\n    background: var(--gradient);\n    opacity: 0;\n    transition: opacity 0.3s ease;\n  }\n\n  .card:hover::before {\n    opacity: 1;\n  }\n\n  .card-title {\n    margin: 0 0 16px;\n    font-size: 1.3rem;\n    font-weight: 600;\n    color: var(--primary);\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .main-video-card {\n    grid-row: span 2;\n  }\n\n  .channel-switcher {\n    background: rgba(15, 23, 42, 0.8);\n    border: 1px solid var(--dark-border);\n    border-radius: 12px;\n    padding: 16px;\n    margin-bottom: 16px;\n  }\n\n  .channel-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n    gap: 12px;\n    margin-top: 12px;\n  }\n\n  .channel-button {\n    background: rgba(15, 23, 42, 0.9);\n    border: 2px solid var(--dark-border);\n    border-radius: 8px;\n    padding: 8px;\n    cursor: pointer;\n    transition: all 0.3s ease;\n    text-align: center;\n    position: relative;\n    overflow: hidden;\n  }\n\n  .channel-button:hover {\n    border-color: var(--primary);\n    transform: translateY(-2px);\n  }\n\n  .channel-button.active {\n    border-color: var(--success);\n    background: rgba(16, 185, 129, 0.1);\n    box-shadow: 0 0 15px rgba(16, 185, 129, 0.3);\n  }\n\n  .channel-button.live::before {\n    content: '🔴 LIVE';\n    position: absolute;\n    top: 4px;\n    right: 4px;\n    font-size: 10px;\n    background: var(--error);\n    color: white;\n    padding: 2px 6px;\n    border-radius: 10px;\n    font-weight: bold;\n  }\n\n  .channel-name {\n    font-weight: 600;\n    font-size: 12px;\n    margin-bottom: 4px;\n  }\n\n  .channel-info {\n    font-size: 10px;\n    opacity: 0.7;\n  }\n\n  .video-container {\n    position: relative;\n    border-radius: 12px;\n    overflow: hidden;\n    background: #000;\n    margin: 16px 0;\n    aspect-ratio: 16\/9;\n  }\n\n  .video {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    display: block;\n  }\n\n  .video-overlay {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background: rgba(0,0,0,0.7);\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    color: white;\n    font-size: 14px;\n    pointer-events: none;\n    transition: opacity 0.3s ease;\n  }\n\n  .video-overlay.hidden {\n    opacity: 0;\n  }\n\n  .video-controls {\n    position: absolute;\n    bottom: 10px;\n    left: 10px;\n    right: 10px;\n    display: flex;\n    gap: 8px;\n    opacity: 0;\n    transition: opacity 0.3s ease;\n  }\n\n  .video-container:hover .video-controls {\n    opacity: 1;\n  }\n\n  .video-btn {\n    background: rgba(0,0,0,0.7);\n    border: none;\n    color: white;\n    padding: 8px 12px;\n    border-radius: 6px;\n    cursor: pointer;\n    font-size: 12px;\n    backdrop-filter: blur(10px);\n  }\n\n  .mpc-plugin-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n    gap: 12px;\n    margin-top: 16px;\n  }\n\n  .plugin-card {\n    background: rgba(15, 23, 42, 0.6);\n    border: 1px solid var(--dark-border);\n    border-radius: 10px;\n    padding: 16px;\n    text-align: center;\n    cursor: pointer;\n    transition: all 0.3s ease;\n    position: relative;\n    overflow: hidden;\n  }\n\n  .plugin-card:hover {\n    background: rgba(139, 92, 246, 0.1);\n    border-color: var(--primary);\n    transform: translateY(-2px);\n  }\n\n  .plugin-card.active {\n    background: rgba(139, 92, 246, 0.2);\n    border-color: var(--primary);\n    box-shadow: var(--glow);\n  }\n\n  .plugin-icon {\n    font-size: 2rem;\n    margin-bottom: 8px;\n    display: block;\n  }\n\n  .plugin-name {\n    font-weight: 600;\n    margin-bottom: 4px;\n  }\n\n  .plugin-desc {\n    font-size: 11px;\n    opacity: 0.7;\n    line-height: 1.3;\n  }\n\n  .processing-indicator {\n    position: absolute;\n    top: 8px;\n    right: 8px;\n    width: 20px;\n    height: 20px;\n    border-radius: 50%;\n    background: var(--success);\n    animation: pulse 2s infinite;\n    opacity: 0;\n  }\n\n  .processing-indicator.active {\n    opacity: 1;\n  }\n\n  .form-group {\n    margin-bottom: 16px;\n  }\n\n  .form-label {\n    display: block;\n    font-size: 14px;\n    font-weight: 500;\n    color: var(--text);\n    margin-bottom: 6px;\n  }\n\n  .form-input, .form-select, .form-textarea {\n    width: 100%;\n    background: rgba(15, 23, 42, 0.8);\n    border: 1px solid var(--dark-border);\n    color: var(--text);\n    border-radius: 12px;\n    padding: 12px 16px;\n    font-size: 14px;\n    transition: all 0.3s ease;\n  }\n\n  .btn-group {\n    display: flex;\n    gap: 12px;\n    flex-wrap: wrap;\n    margin-top: 12px;\n  }\n\n  .btn {\n    flex: 1;\n    min-width: 120px;\n    border: none;\n    border-radius: 12px;\n    padding: 12px 16px;\n    font-weight: 600;\n    font-size: 14px;\n    cursor: pointer;\n    transition: all 0.3s ease;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n  }\n\n  .btn-primary {\n    background: var(--gradient);\n    color: white;\n    box-shadow: var(--glow);\n  }\n\n  .btn-secondary {\n    background: var(--dark-border);\n    color: var(--text);\n  }\n\n  .btn-success {\n    background: var(--success);\n    color: white;\n  }\n\n  .btn:hover {\n    transform: translateY(-1px);\n  }\n\n  .status-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));\n    gap: 8px;\n    margin: 16px 0;\n  }\n\n  .status-badge {\n    background: rgba(15, 23, 42, 0.6);\n    border: 1px solid var(--dark-border);\n    border-radius: 8px;\n    padding: 8px;\n    text-align: center;\n    font-size: 11px;\n  }\n\n  .status-label {\n    display: block;\n    color: var(--text-muted);\n    font-weight: 500;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n  }\n\n  .status-value {\n    display: block;\n    color: var(--text);\n    font-weight: 700;\n    margin-top: 2px;\n  }\n\n  .status-value.connected { color: var(--success); }\n  .status-value.connecting { color: var(--warning); }\n  .status-value.error { color: var(--error); }\n\n  .log-container {\n    background: rgba(15, 23, 42, 0.9);\n    border: 1px solid var(--dark-border);\n    border-radius: 12px;\n    padding: 16px;\n    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n    font-size: 12px;\n    max-height: 200px;\n    overflow-y: auto;\n  }\n\n  .log-entry {\n    margin-bottom: 2px;\n    padding: 2px 0;\n  }\n\n  .log-timestamp {\n    color: var(--text-muted);\n    margin-right: 8px;\n  }\n\n  .log-level-info { color: var(--secondary); }\n  .log-level-success { color: var(--success); }\n  .log-level-warning { color: var(--warning); }\n  .log-level-error { color: var(--error); }\n\n  @media (max-width: 1200px) {\n    .grid {\n      grid-template-columns: 1fr;\n    }\n    \n    .main-video-card {\n      grid-row: auto;\n    }\n  }\n\n  @keyframes pulse {\n    0% { transform: scale(1); opacity: 1; }\n    50% { transform: scale(1.1); opacity: 0.7; }\n    100% { transform: scale(1); opacity: 1; }\n  }\n\n  .fade-in {\n    animation: fadeIn 0.5s ease-in;\n  }\n\n  @keyframes fadeIn {\n    from { opacity: 0; transform: translateY(10px); }\n    to { opacity: 1; transform: translateY(0); }\n  }\n\n  .channel-preview {\n    width: 100%;\n    height: 60px;\n    background: #000;\n    border-radius: 4px;\n    margin-bottom: 8px;\n    position: relative;\n    overflow: hidden;\n  }\n\n  .channel-preview video {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n\n  .channel-preview .placeholder {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    color: #666;\n    font-size: 10px;\n  }\n\u003c\/style\u003e\n\n\n\n\u003cheader class=\"app-header\"\u003e\n  \u003ch1 class=\"app-title\"\u003eGIBBLER™ P2P Enhanced\u003c\/h1\u003e\n  \u003cp class=\"app-subtitle\"\u003eLive Broadcast Channels • MPC Plugins • Real-time Processing • Zero-Server Architecture\u003c\/p\u003e\n\u003c\/header\u003e\n\n\u003cdiv class=\"container\"\u003e\n  \u003cdiv class=\"grid\"\u003e\n    \u003c!-- Main Video Display with Channel Switcher --\u003e\n    \u003cdiv class=\"card main-video-card\"\u003e\n      \u003ch3 class=\"card-title\"\u003e\n        📺 Live Channel Browser\n      \u003c\/h3\u003e\n      \n      \u003cdiv class=\"channel-switcher\"\u003e\n        \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;\"\u003e\n          \u003cspan class=\"form-label\"\u003eAvailable Channels\u003c\/span\u003e\n          \u003cbutton class=\"btn btn-secondary\" id=\"btnRefreshChannels\" style=\"padding: 6px 12px; font-size: 12px;\"\u003e🔄 Refresh\u003c\/button\u003e\n        \u003c\/div\u003e\n        \n        \u003cdiv class=\"channel-grid\" id=\"channelGrid\"\u003e\n          \u003c!-- Channels will be populated here --\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"video-container\"\u003e\n        \u003cvideo id=\"mainVideo\" class=\"video\" playsinline autoplay controls\u003e\u003c\/video\u003e\n        \u003cdiv class=\"video-overlay\" id=\"mainVideoOverlay\"\u003e\n          \u003cdiv style=\"font-size: 18px; margin-bottom: 8px;\"\u003e📡\u003c\/div\u003e\n          \u003cdiv\u003eSelect a channel to start watching\u003c\/div\u003e\n          \u003cdiv style=\"font-size: 12px; margin-top: 8px; opacity: 0.7;\"\u003eLive webcams and broadcasts available\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"video-controls\"\u003e\n          \u003cbutton class=\"video-btn\" id=\"btnFullscreen\"\u003e⛶ Fullscreen\u003c\/button\u003e\n          \u003cbutton class=\"video-btn\" id=\"btnPiP\"\u003e📱 Picture-in-Picture\u003c\/button\u003e\n          \u003cbutton class=\"video-btn\" id=\"btnRecord\"\u003e⏺️ Record\u003c\/button\u003e\n          \u003cbutton class=\"video-btn\" id=\"btnSnapshot\"\u003e📸 Snapshot\u003c\/button\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"status-grid\"\u003e\n        \u003cdiv class=\"status-badge\"\u003e\n          \u003cspan class=\"status-label\"\u003eChannel\u003c\/span\u003e\n          \u003cspan class=\"status-value\" id=\"currentChannel\"\u003eNone\u003c\/span\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"status-badge\"\u003e\n          \u003cspan class=\"status-label\"\u003eQuality\u003c\/span\u003e\n          \u003cspan class=\"status-value\" id=\"streamQuality\"\u003e—\u003c\/span\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"status-badge\"\u003e\n          \u003cspan class=\"status-label\"\u003eViewers\u003c\/span\u003e\n          \u003cspan class=\"status-value\" id=\"viewerCount\"\u003e—\u003c\/span\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"status-badge\"\u003e\n          \u003cspan class=\"status-label\"\u003eLatency\u003c\/span\u003e\n          \u003cspan class=\"status-value\" id=\"streamLatency\"\u003e—\u003c\/span\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003c!-- MPC Plugins Panel --\u003e\n    \u003cdiv class=\"card\"\u003e\n      \u003ch3 class=\"card-title\"\u003e\n        🧩 MPC Plugins\n      \u003c\/h3\u003e\n      \n      \u003cdiv class=\"form-group\"\u003e\n        \u003clabel class=\"form-label\"\u003eActive Processing Pipeline\u003c\/label\u003e\n        \u003cdiv style=\"background: rgba(15, 23, 42, 0.6); border-radius: 8px; padding: 12px; font-size: 12px;\"\u003e\n          \u003cdiv id=\"pipelineStatus\"\u003eReady for processing...\u003c\/div\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"mpc-plugin-grid\" id=\"pluginGrid\"\u003e\n        \u003c!-- Plugins will be populated here --\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"btn-group\" style=\"margin-top: 16px;\"\u003e\n        \u003cbutton class=\"btn btn-primary\" id=\"btnStartProcessing\"\u003e▶️ Start\u003c\/button\u003e\n        \u003cbutton class=\"btn btn-secondary\" id=\"btnStopProcessing\"\u003e⏹️ Stop\u003c\/button\u003e\n        \u003cbutton class=\"btn btn-success\" id=\"btnExportResult\"\u003e💾 Export\u003c\/button\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003c!-- P2P Connection Panel --\u003e\n    \u003cdiv class=\"card\"\u003e\n      \u003ch3 class=\"card-title\"\u003e\n        🔗 P2P Network\n      \u003c\/h3\u003e\n\n      \u003cdiv class=\"form-group\"\u003e\n        \u003clabel class=\"form-label\"\u003eNetwork Role\u003c\/label\u003e\n        \u003cselect id=\"role\" class=\"form-select\"\u003e\n          \u003coption value=\"broadcaster\"\u003eBroadcaster\u003c\/option\u003e\n          \u003coption value=\"viewer\"\u003eViewer\u003c\/option\u003e\n          \u003coption value=\"mpc-node\"\u003eMPC Node\u003c\/option\u003e\n          \u003coption value=\"relay\"\u003eRelay Node\u003c\/option\u003e\n        \u003c\/select\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"btn-group\"\u003e\n        \u003cbutton class=\"btn btn-primary\" id=\"btnInit\"\u003eInitialize\u003c\/button\u003e\n        \u003cbutton class=\"btn btn-secondary\" id=\"btnConnect\"\u003eConnect\u003c\/button\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"status-grid\"\u003e\n        \u003cdiv class=\"status-badge\"\u003e\n          \u003cspan class=\"status-label\"\u003ePeers\u003c\/span\u003e\n          \u003cspan class=\"status-value\" id=\"peerCount\"\u003e0\u003c\/span\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"status-badge\"\u003e\n          \u003cspan class=\"status-label\"\u003eNetwork\u003c\/span\u003e\n          \u003cspan class=\"status-value\" id=\"networkState\"\u003eoffline\u003c\/span\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"form-group\" style=\"margin-top: 16px;\"\u003e\n        \u003clabel class=\"form-label\"\u003eShare Your Stream\u003c\/label\u003e\n        \u003cdiv class=\"btn-group\"\u003e\n          \u003cbutton class=\"btn btn-primary\" id=\"btnStartBroadcast\"\u003e📡 Start Broadcast\u003c\/button\u003e\n          \u003cbutton class=\"btn btn-secondary\" id=\"btnStopBroadcast\"\u003e⏹️ Stop\u003c\/button\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003c!-- Processing Results \u0026 Downloads --\u003e\n  \u003cdiv class=\"card\"\u003e\n    \u003ch3 class=\"card-title\"\u003e\n      📁 Processing Results \u0026amp; Downloads\n    \u003c\/h3\u003e\n    \u003cdiv id=\"resultsList\" style=\"min-height: 100px; padding: 20px; text-align: center; color: var(--text-muted);\"\u003e\n      No processed content yet. Start watching channels and apply MPC plugins!\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003c!-- Activity Log --\u003e\n  \u003cdiv class=\"card\"\u003e\n    \u003ch3 class=\"card-title\"\u003e\n      📋 Activity Log\n      \u003cbutton class=\"btn btn-secondary\" id=\"btnClearLog\" style=\"margin-left: auto; padding: 6px 12px; font-size: 12px;\"\u003eClear\u003c\/button\u003e\n    \u003c\/h3\u003e\n    \u003cdiv class=\"log-container\" id=\"logContainer\"\u003e\n      \u003cdiv class=\"log-entry\"\u003e\n        \u003cspan class=\"log-timestamp\"\u003e[00:00:00]\u003c\/span\u003e\n        \u003cspan class=\"log-level-info\"\u003eGIBBLER™ P2P Enhanced with Live Channels initialized\u003c\/span\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\u003c\/div\u003e\n\n\u003cscript\u003e\n(function() {\n  'use strict';\n\n  \/\/ Application State\n  const App = {\n    currentChannel: null,\n    activePlugins: new Set(),\n    processingPipeline: [],\n    peer: null,\n    localStream: null,\n    channels: new Map(),\n    isRecording: false,\n    recordedChunks: []\n  };\n\n  \/\/ Utility Functions\n  const $ = id =\u003e document.getElementById(id);\n  const $$ = selector =\u003e document.querySelectorAll(selector);\n\n  function log(message, level = 'info') {\n    const timestamp = new Date().toTimeString().split(' ')[0];\n    const logEntry = document.createElement('div');\n    logEntry.className = 'log-entry fade-in';\n    logEntry.innerHTML = `\n      \u003cspan class=\"log-timestamp\"\u003e[${timestamp}]\u003c\/span\u003e\n      \u003cspan class=\"log-level-${level}\"\u003e${message}\u003c\/span\u003e\n    `;\n    \n    const container = $('logContainer');\n    container.appendChild(logEntry);\n    container.scrollTop = container.scrollHeight;\n\n    if (container.children.length \u003e 50) {\n      container.children[0].remove();\n    }\n  }\n\n  \/\/ Live Channel Database (Simulated)\n  const LIVE_CHANNELS = [\n    {\n      id: 'earthcam-times-square',\n      name: 'Times Square',\n      url: 'https:\/\/videos-3.earthcam.com\/fecnetwork\/9974.flv\/playlist.m3u8',\n      type: 'webcam',\n      location: 'New York, USA',\n      viewers: 2847,\n      quality: 'HD',\n      live: true\n    },\n    {\n      id: 'earthcam-abbey-road',\n      name: 'Abbey Road',\n      url: 'https:\/\/videos-3.earthcam.com\/fecnetwork\/AbbeyRoadHD1.flv\/playlist.m3u8',\n      type: 'webcam',\n      location: 'London, UK',\n      viewers: 1523,\n      quality: 'HD',\n      live: true\n    },\n    {\n      id: 'iss-live',\n      name: 'ISS Earth View',\n      url: 'https:\/\/live-feed.space\/api\/iss-stream',\n      type: 'space',\n      location: 'Low Earth Orbit',\n      viewers: 5432,\n      quality: '4K',\n      live: true\n    },\n    {\n      id: 'monterey-bay',\n      name: 'Monterey Bay',\n      url: 'https:\/\/mbaaquarium.org\/live-streams\/kelp-forest',\n      type: 'nature',\n      location: 'California, USA',\n      viewers: 892,\n      quality: 'HD',\n      live: true\n    },\n    {\n      id: 'tokyo-shibuya',\n      name: 'Shibuya Crossing',\n      url: 'https:\/\/shibuya-sky.com\/live-stream',\n      type: 'webcam',\n      location: 'Tokyo, Japan',\n      viewers: 3156,\n      quality: 'HD',\n      live: true\n    },\n    {\n      id: 'northern-lights',\n      name: 'Aurora Borealis',\n      url: 'https:\/\/aurora-alerts.com\/live-cam',\n      type: 'nature',\n      location: 'Iceland',\n      viewers: 1247,\n      quality: 'HD',\n      live: true\n    },\n    {\n      id: 'local-user-1',\n      name: 'User Stream #1',\n      url: 'peer:\/\/local',\n      type: 'p2p',\n      location: 'P2P Network',\n      viewers: 3,\n      quality: 'HD',\n      live: true\n    },\n    {\n      id: 'local-user-2',\n      name: 'User Stream #2',\n      url: 'peer:\/\/local',\n      type: 'p2p',\n      location: 'P2P Network',\n      viewers: 7,\n      quality: 'SD',\n      live: true\n    }\n  ];\n\n  \/\/ MPC Plugin System\n  const MPC_PLUGINS = [\n    {\n      id: 'face-detection',\n      name: 'Face Detection',\n      icon: '👥',\n      description: 'Detect faces in live stream using distributed ML',\n      category: 'detection',\n      computeIntensive: true,\n      realtime: true\n    },\n    {\n      id: 'motion-tracking',\n      name: 'Motion Tracking',\n      icon: '🎯',\n      description: 'Track object movement across frames',\n      category: 'analysis',\n      computeIntensive: false,\n      realtime: true\n    },\n    {\n      id: 'privacy-blur',\n      name: 'Privacy Blur',\n      icon: '🔒',\n      description: 'Automatically blur sensitive content',\n      category: 'privacy',\n      computeIntensive: false,\n      realtime: true\n    },\n    {\n      id: 'color-enhancement',\n      name: 'Color Enhance',\n      icon: '🎨',\n      description: 'Enhance colors and contrast',\n      category: 'enhancement',\n      computeIntensive: false,\n      realtime: true\n    },\n    {\n      id: 'noise-reduction',\n      name: 'Noise Reduction',\n      icon: '🔇',\n      description: 'Remove noise from video stream',\n      category: 'enhancement',\n      computeIntensive: true,\n      realtime: true\n    },\n    {\n      id: 'object-counting',\n      name: 'Object Counter',\n      icon: '🔢',\n      description: 'Count objects in real-time',\n      category: 'analysis',\n      computeIntensive: true,\n      realtime: true\n    },\n    {\n      id: 'edge-detection',\n      name: 'Edge Detection',\n      icon: '📐',\n      description: 'Highlight edges and contours',\n      category: 'analysis',\n      computeIntensive: false,\n      realtime: true\n    },\n    {\n      id: 'style-transfer',\n      name: 'Style Transfer',\n      icon: '🖼️',\n      description: 'Apply artistic styles to video',\n      category: 'enhancement',\n      computeIntensive: true,\n      realtime: false\n    },\n    {\n      id: 'anomaly-detection',\n      name: 'Anomaly Detection',\n      icon: '⚠️',\n      description: 'Detect unusual patterns or events',\n      category: 'detection',\n      computeIntensive: true,\n      realtime: true\n    },\n    {\n      id: 'crowd-analysis',\n      name: 'Crowd Analysis',\n      icon: '👥',\n      description: 'Analyze crowd density and movement',\n      category: 'analysis',\n      computeIntensive: true,\n      realtime: true\n    },\n    {\n      id: 'weather-overlay',\n      name: 'Weather Overlay',\n      icon: '🌤️',\n      description: 'Add weather information overlay',\n      category: 'overlay',\n      computeIntensive: false,\n      realtime: true\n    },\n    {\n      id: 'timestamp-overlay',\n      name: 'Timestamp',\n      icon: '⏰',\n      description: 'Add timestamp and location info',\n      category: 'overlay',\n      computeIntensive: false,\n      realtime: true\n    }\n  ];\n\n  \/\/ Channel Management\n  class ChannelManager {\n    constructor() {\n      this.channels = new Map();\n      this.currentStream = null;\n    }\n\n    async loadChannels() {\n      log('Loading live channels...', 'info');\n      \n      LIVE_CHANNELS.forEach(channel =\u003e {\n        this.channels.set(channel.id, channel);\n      });\n\n      this.renderChannels();\n      log(`Loaded ${this.channels.size} live channels`, 'success');\n    }\n\n    renderChannels() {\n      const grid = $('channelGrid');\n      grid.innerHTML = '';\n\n      this.channels.forEach(channel =\u003e {\n        const channelEl = document.createElement('div');\n        channelEl.className = 'channel-button';\n        channelEl.dataset.channelId = channel.id;\n        \n        if (channel.live) {\n          channelEl.classList.add('live');\n        }\n\n        channelEl.innerHTML = `\n          \u003cdiv class=\"channel-preview\"\u003e\n            \u003cdiv class=\"placeholder\"\u003e📺\u003c\/div\u003e\n          \u003c\/div\u003e\n          \u003cdiv class=\"channel-name\"\u003e${channel.name}\u003c\/div\u003e\n          \u003cdiv class=\"channel-info\"\u003e${channel.location}\u003c\/div\u003e\n          \u003cdiv class=\"channel-info\"\u003e${channel.viewers} viewers • ${channel.quality}\u003c\/div\u003e\n        `;\n\n        channelEl.addEventListener('click', () =\u003e this.switchToChannel(channel.id));\n        grid.appendChild(channelEl);\n      });\n    }\n\n    async switchToChannel(channelId) {\n      const channel = this.channels.get(channelId);\n      if (!channel) return;\n\n      log(`Switching to channel: ${channel.name}`, 'info');\n\n      \/\/ Update UI\n      $$('.channel-button').forEach(btn =\u003e btn.classList.remove('active'));\n      $(`[data-channel-id=\"${channelId}\"]`).classList.add('active');\n\n      \/\/ Update status\n      $('currentChannel').textContent = channel.name;\n      $('streamQuality').textContent = channel.quality;\n      $('viewerCount').textContent = channel.viewers.toLocaleString();\n\n      try {\n        await this.loadChannelStream(channel);\n        App.currentChannel = channel;\n        $('mainVideoOverlay').classList.add('hidden');\n        log(`Connected to ${channel.name}`, 'success');\n      } catch (error) {\n        log(`Failed to load ${channel.name}: ${error.message}`, 'error');\n        this.showError(channel.name);\n      }\n    }\n\n    async loadChannelStream(channel) {\n      const video = $('mainVideo');\n      \n      if (channel.type === 'p2p') {\n        \/\/ Handle P2P streams\n        await this.connectToP2PStream(channel);\n      } else {\n        \/\/ Handle external streams (simulated for demo)\n        await this.simulateStreamConnection(channel);\n      }\n    }\n\n    async simulateStreamConnection(channel) {\n      \/\/ For demo purposes, we'll create a test pattern or use user's camera\n      const video = $('mainVideo');\n      \n      try {\n        \/\/ Try to get user's camera as demo content\n        const stream = await navigator.mediaDevices.getUserMedia({\n          video: { width: 1280, height: 720 },\n          audio: true\n        });\n        \n        video.srcObject = stream;\n        \n        \/\/ Simulate latency\n        setTimeout(() =\u003e {\n          $('streamLatency').textContent = Math.floor(Math.random() * 50 + 30) + 'ms';\n        }, 1000);\n        \n      } catch (error) {\n        \/\/ Fallback to test pattern\n        this.createTestPattern(video, channel);\n      }\n    }\n\n    createTestPattern(video, channel) {\n      \/\/ Create a canvas with test pattern\n      const canvas = document.createElement('canvas');\n      const ctx = canvas.getContext('2d');\n      canvas.width = 1280;\n      canvas.height = 720;\n      \n      let frame = 0;\n      const animate = () =\u003e {\n        \/\/ Create animated test pattern\n        ctx.fillStyle = '#000';\n        ctx.fillRect(0, 0, canvas.width, canvas.height);\n        \n        ctx.fillStyle = '#4F46E5';\n        ctx.fillRect(50 + Math.sin(frame * 0.1) * 20, 50, 200, 150);\n        \n        ctx.fillStyle = '#10B981';\n        ctx.fillRect(300, 50 + Math.cos(frame * 0.15) * 30, 200, 150);\n        \n        ctx.fillStyle = '#F59E0B';\n        ctx.fillRect(550, 50, 200, 150 + Math.sin(frame * 0.08) * 40);\n        \n        \/\/ Add channel info\n        ctx.fillStyle = '#FFF';\n        ctx.font = '24px Arial';\n        ctx.fillText(`📡 ${channel.name}`, 50, 300);\n        ctx.font = '16px Arial';\n        ctx.fillText(`Location: ${channel.location}`, 50, 330);\n        ctx.fillText(`Viewers: ${channel.viewers.toLocaleString()}`, 50, 350);\n        ctx.fillText(`Quality: ${channel.quality}`, 50, 370);\n        ctx.fillText(`Frame: ${frame}`, 50, 390);\n        \n        frame++;\n        requestAnimationFrame(animate);\n      };\n      animate();\n      \n      const stream = canvas.captureStream(30);\n      video.srcObject = stream;\n    }\n\n    async connectToP2PStream(channel) {\n      log(`Connecting to P2P stream: ${channel.name}`, 'info');\n      \/\/ P2P connection logic would go here\n      \/\/ For now, simulate with local camera\n      await this.simulateStreamConnection(channel);\n    }\n\n    showError(channelName) {\n      const overlay = $('mainVideoOverlay');\n      overlay.innerHTML = `\n        \u003cdiv style=\"font-size: 18px; margin-bottom: 8px;\"\u003e❌\u003c\/div\u003e\n        \u003cdiv\u003eFailed to connect to ${channelName}\u003c\/div\u003e\n        \u003cdiv style=\"font-size: 12px; margin-top: 8px; opacity: 0.7;\"\u003eTry another channel or check your connection\u003c\/div\u003e\n      `;\n      overlay.classList.remove('hidden');\n    }\n  }\n\n  \/\/ MPC Plugin System\n  class MPCPluginManager {\n    constructor() {\n      this.activePlugins = new Set();\n      this.processingCanvas = null;\n      this.processingContext = null;\n      this.processingEnabled = false;\n      this.setupProcessingCanvas();\n    }\n\n    setupProcessingCanvas() {\n      this.processingCanvas = document.createElement('canvas');\n      this.processingContext = this.processingCanvas.getContext('2d');\n      this.processingCanvas.width = 1280;\n      this.processingCanvas.height = 720;\n    }\n\n    renderPlugins() {\n      const grid = $('pluginGrid');\n      grid.innerHTML = '';\n\n      MPC_PLUGINS.forEach(plugin =\u003e {\n        const pluginEl = document.createElement('div');\n        pluginEl.className = 'plugin-card';\n        pluginEl.dataset.pluginId = plugin.id;\n        \n        pluginEl.innerHTML = `\n          \u003cdiv class=\"plugin-icon\"\u003e${plugin.icon}\u003c\/div\u003e\n          \u003cdiv class=\"plugin-name\"\u003e${plugin.name}\u003c\/div\u003e\n          \u003cdiv class=\"plugin-desc\"\u003e${plugin.description}\u003c\/div\u003e\n          \u003cdiv class=\"processing-indicator\"\u003e\u003c\/div\u003e\n        `;\n\n        pluginEl.addEventListener('click', () =\u003e this.togglePlugin(plugin.id));\n        grid.appendChild(pluginEl);\n      });\n    }\n\n    togglePlugin(pluginId) {\n      const plugin = MPC_PLUGINS.find(p =\u003e p.id === pluginId);\n      if (!plugin) return;\n\n      const pluginEl = $(`[data-plugin-id=\"${pluginId}\"]`);\n      \n      if (this.activePlugins.has(pluginId)) {\n        this.activePlugins.delete(pluginId);\n        pluginEl.classList.remove('active');\n        pluginEl.querySelector('.processing-indicator').classList.remove('active');\n        log(`Disabled plugin: ${plugin.name}`, 'info');\n      } else {\n        this.activePlugins.add(pluginId);\n        pluginEl.classList.add('active');\n        pluginEl.querySelector('.processing-indicator').classList.add('active');\n        log(`Enabled plugin: ${plugin.name}`, 'success');\n      }\n\n      this.updatePipeline();\n    }\n\n    updatePipeline() {\n      const activePluginNames = Array.from(this.activePlugins)\n        .map(id =\u003e MPC_PLUGINS.find(p =\u003e p.id === id)?.name)\n        .filter(Boolean);\n\n      const pipelineStatus = $('pipelineStatus');\n      if (activePluginNames.length === 0) {\n        pipelineStatus.textContent = 'Ready for processing...';\n      } else {\n        pipelineStatus.innerHTML = `\n          \u003cstrong\u003eActive Pipeline:\u003c\/strong\u003e\u003cbr\u003e\n          ${activePluginNames.join(' → ')}\n        `;\n      }\n    }\n\n    async startProcessing() {\n      if (this.activePlugins.size === 0) {\n        alert('Please select at least one plugin first');\n        return;\n      }\n\n      if (!App.currentChannel) {\n        alert('Please select a channel first');\n        return;\n      }\n\n      this.processingEnabled = true;\n      log(`Starting MPC processing with ${this.activePlugins.size} plugins`, 'info');\n\n      try {\n        await this.processVideoStream();\n      } catch (error) {\n        log(`Processing error: ${error.message}`, 'error');\n      }\n    }\n\n    async processVideoStream() {\n      const video = $('mainVideo');\n      if (!video.srcObject) return;\n\n      const processFrame = () =\u003e {\n        if (!this.processingEnabled) return;\n\n        \/\/ Draw current video frame to canvas\n        this.processingContext.drawImage(video, 0, 0, \n          this.processingCanvas.width, this.processingCanvas.height);\n\n        \/\/ Apply active plugins\n        this.activePlugins.forEach(pluginId =\u003e {\n          this.applyPlugin(pluginId);\n        });\n\n        \/\/ Continue processing\n        requestAnimationFrame(processFrame);\n      };\n\n      processFrame();\n    }\n\n    applyPlugin(pluginId) {\n      const plugin = MPC_PLUGINS.find(p =\u003e p.id === pluginId);\n      if (!plugin) return;\n\n      const imageData = this.processingContext.getImageData(0, 0, \n        this.processingCanvas.width, this.processingCanvas.height);\n      \n      switch (pluginId) {\n        case 'face-detection':\n          this.applyFaceDetection(imageData);\n          break;\n        case 'motion-tracking':\n          this.applyMotionTracking(imageData);\n          break;\n        case 'privacy-blur':\n          this.applyPrivacyBlur(imageData);\n          break;\n        case 'color-enhancement':\n          this.applyColorEnhancement(imageData);\n          break;\n        case 'noise-reduction':\n          this.applyNoiseReduction(imageData);\n          break;\n        case 'edge-detection':\n          this.applyEdgeDetection(imageData);\n          break;\n        case 'timestamp-overlay':\n          this.applyTimestampOverlay();\n          break;\n        default:\n          \/\/ Generic processing\n          this.applyGenericFilter(imageData, pluginId);\n      }\n\n      this.processingContext.putImageData(imageData, 0, 0);\n    }\n\n    applyFaceDetection(imageData) {\n      \/\/ Simulate face detection with colored rectangles\n      this.processingContext.strokeStyle = '#10B981';\n      this.processingContext.lineWidth = 3;\n      this.processingContext.strokeRect(300, 200, 150, 180);\n      this.processingContext.fillStyle = '#10B981';\n      this.processingContext.font = '16px Arial';\n      this.processingContext.fillText('Face Detected', 300, 190);\n    }\n\n    applyMotionTracking(imageData) {\n      \/\/ Simulate motion vectors\n      this.processingContext.strokeStyle = '#F59E0B';\n      this.processingContext.lineWidth = 2;\n      for (let i = 0; i \u003c 10; i++) {\n        const x = Math.random() * this.processingCanvas.width;\n        const y = Math.random() * this.processingCanvas.height;\n        this.processingContext.beginPath();\n        this.processingContext.moveTo(x, y);\n        this.processingContext.lineTo(x + Math.random() * 40 - 20, y + Math.random() * 40 - 20);\n        this.processingContext.stroke();\n      }\n    }\n\n    applyPrivacyBlur(imageData) {\n      \/\/ Simple blur effect\n      const data = imageData.data;\n      for (let i = 0; i \u003c data.length; i += 4) {\n        const avg = (data[i] + data[i + 1] + data[i + 2]) \/ 3;\n        data[i] = data[i + 1] = data[i + 2] = avg * 0.8;\n      }\n    }\n\n    applyColorEnhancement(imageData) {\n      \/\/ Enhance colors\n      const data = imageData.data;\n      for (let i = 0; i \u003c data.length; i += 4) {\n        data[i] = Math.min(255, data[i] * 1.2);     \/\/ Red\n        data[i + 1] = Math.min(255, data[i + 1] * 1.1); \/\/ Green\n        data[i + 2] = Math.min(255, data[i + 2] * 1.3); \/\/ Blue\n      }\n    }\n\n    applyNoiseReduction(imageData) {\n      \/\/ Simple noise reduction\n      const data = imageData.data;\n      for (let i = 0; i \u003c data.length; i += 4) {\n        data[i] = Math.floor(data[i] \/ 16) * 16;\n        data[i + 1] = Math.floor(data[i + 1] \/ 16) * 16;\n        data[i + 2] = Math.floor(data[i + 2] \/ 16) * 16;\n      }\n    }\n\n    applyEdgeDetection(imageData) {\n      \/\/ Simple edge detection effect\n      const data = imageData.data;\n      for (let i = 0; i \u003c data.length; i += 4) {\n        const gray = (data[i] + data[i + 1] + data[i + 2]) \/ 3;\n        const edge = gray \u003e 128 ? 255 : 0;\n        data[i] = data[i + 1] = data[i + 2] = edge;\n      }\n    }\n\n    applyTimestampOverlay() {\n      const now = new Date();\n      this.processingContext.fillStyle = 'rgba(0, 0, 0, 0.7)';\n      this.processingContext.fillRect(20, 20, 300, 80);\n      this.processingContext.fillStyle = '#FFF';\n      this.processingContext.font = '18px Arial';\n      this.processingContext.fillText(now.toLocaleString(), 30, 45);\n      if (App.currentChannel) {\n        this.processingContext.font = '14px Arial';\n        this.processingContext.fillText(`📍 ${App.currentChannel.location}`, 30, 70);\n        this.processingContext.fillText(`👥 ${App.currentChannel.viewers} viewers`, 30, 90);\n      }\n    }\n\n    applyGenericFilter(imageData, pluginId) {\n      \/\/ Generic filter based on plugin type\n      const data = imageData.data;\n      const hue = MPC_PLUGINS.findIndex(p =\u003e p.id === pluginId) * 30;\n      \n      for (let i = 0; i \u003c data.length; i += 4) {\n        data[i] = Math.min(255, data[i] + Math.sin(hue) * 20);\n        data[i + 1] = Math.min(255, data[i + 1] + Math.cos(hue) * 20);\n        data[i + 2] = Math.min(255, data[i + 2] + Math.sin(hue + 1) * 20);\n      }\n    }\n\n    stopProcessing() {\n      this.processingEnabled = false;\n      log('MPC processing stopped', 'info');\n    }\n\n    async exportResult() {\n      if (!this.processingCanvas) {\n        alert('No processed content to export');\n        return;\n      }\n\n      try {\n        const blob = await new Promise(resolve =\u003e {\n          this.processingCanvas.toBlob(resolve, 'image\/png');\n        });\n\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement('a');\n        a.href = url;\n        a.download = `gibbler-processed-${Date.now()}.png`;\n        a.click();\n\n        log('Processed image exported successfully', 'success');\n        this.addToResults(url, a.download, blob.size);\n      } catch (error) {\n        log(`Export failed: ${error.message}`, 'error');\n      }\n    }\n\n    addToResults(url, filename, size) {\n      const resultsList = $('resultsList');\n      \n      if (resultsList.textContent.includes('No processed content')) {\n        resultsList.innerHTML = '';\n      }\n\n      const resultItem = document.createElement('div');\n      resultItem.className = 'download-item';\n      resultItem.innerHTML = `\n        \u003csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"\u003e\n          \u003cpath d=\"M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z\"\/\u003e\n        \u003c\/svg\u003e\n        \u003cdiv\u003e\n          \u003cdiv style=\"font-weight: 600;\"\u003e${filename}\u003c\/div\u003e\n          \u003cdiv style=\"font-size: 12px; opacity: 0.7;\"\u003e${this.formatBytes(size)} • Processed Image\u003c\/div\u003e\n        \u003c\/div\u003e\n      `;\n\n      resultItem.addEventListener('click', () =\u003e {\n        const link = document.createElement('a');\n        link.href = url;\n        link.download = filename;\n        link.click();\n      });\n\n      resultsList.appendChild(resultItem);\n    }\n\n    formatBytes(bytes) {\n      if (bytes === 0) return '0 Bytes';\n      const k = 1024;\n      const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n      const i = Math.floor(Math.log(bytes) \/ Math.log(k));\n      return parseFloat((bytes \/ Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n    }\n  }\n\n  \/\/ Video Controls Manager\n  class VideoControlsManager {\n    constructor() {\n      this.isRecording = false;\n      this.mediaRecorder = null;\n      this.recordedChunks = [];\n    }\n\n    async toggleFullscreen() {\n      const video = $('mainVideo');\n      try {\n        if (document.fullscreenElement) {\n          await document.exitFullscreen();\n        } else {\n          await video.requestFullscreen();\n        }\n      } catch (error) {\n        log(`Fullscreen error: ${error.message}`, 'error');\n      }\n    }\n\n    async togglePictureInPicture() {\n      const video = $('mainVideo');\n      try {\n        if (document.pictureInPictureElement) {\n          await document.exitPictureInPicture();\n        } else {\n          await video.requestPictureInPicture();\n        }\n      } catch (error) {\n        log(`Picture-in-Picture error: ${error.message}`, 'error');\n      }\n    }\n\n    async startRecording() {\n      const video = $('mainVideo');\n      if (!video.srcObject) {\n        alert('No video stream to record');\n        return;\n      }\n\n      try {\n        this.mediaRecorder = new MediaRecorder(video.srcObject);\n        this.recordedChunks = [];\n\n        this.mediaRecorder.ondataavailable = (event) =\u003e {\n          if (event.data.size \u003e 0) {\n            this.recordedChunks.push(event.data);\n          }\n        };\n\n        this.mediaRecorder.onstop = () =\u003e {\n          this.saveRecording();\n        };\n\n        this.mediaRecorder.start();\n        this.isRecording = true;\n        \n        $('btnRecord').textContent = '⏹️ Stop Recording';\n        $('btnRecord').classList.add('btn-error');\n        \n        log('Recording started', 'success');\n      } catch (error) {\n        log(`Recording error: ${error.message}`, 'error');\n      }\n    }\n\n    stopRecording() {\n      if (this.mediaRecorder \u0026\u0026 this.isRecording) {\n        this.mediaRecorder.stop();\n        this.isRecording = false;\n        \n        $('btnRecord').textContent = '⏺️ Record';\n        $('btnRecord').classList.remove('btn-error');\n        \n        log('Recording stopped', 'info');\n      }\n    }\n\n    saveRecording() {\n      const blob = new Blob(this.recordedChunks, { type: 'video\/webm' });\n      const url = URL.createObjectURL(blob);\n      \n      const a = document.createElement('a');\n      a.href = url;\n      a.download = `gibbler-recording-${Date.now()}.webm`;\n      a.click();\n\n      log(`Recording saved: ${a.download}`, 'success');\n      \n      \/\/ Add to results\n      const resultsList = $('resultsList');\n      if (resultsList.textContent.includes('No processed content')) {\n        resultsList.innerHTML = '';\n      }\n\n      const resultItem = document.createElement('div');\n      resultItem.className = 'download-item';\n      resultItem.innerHTML = `\n        \u003csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"\u003e\n          \u003cpath d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z\"\/\u003e\n        \u003c\/svg\u003e\n        \u003cdiv\u003e\n          \u003cdiv style=\"font-weight: 600;\"\u003e${a.download}\u003c\/div\u003e\n          \u003cdiv style=\"font-size: 12px; opacity: 0.7;\"\u003e${this.formatBytes(blob.size)} • Video Recording\u003c\/div\u003e\n        \u003c\/div\u003e\n      `;\n\n      resultItem.addEventListener('click', () =\u003e {\n        const link = document.createElement('a');\n        link.href = url;\n        link.download = a.download;\n        link.click();\n      });\n\n      resultsList.appendChild(resultItem);\n    }\n\n    async takeSnapshot() {\n      const video = $('mainVideo');\n      if (!video.srcObject) {\n        alert('No video stream for snapshot');\n        return;\n      }\n\n      try {\n        const canvas = document.createElement('canvas');\n        const ctx = canvas.getContext('2d');\n        \n        canvas.width = video.videoWidth || 1280;\n        canvas.height = video.videoHeight || 720;\n        \n        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);\n        \n        const blob = await new Promise(resolve =\u003e {\n          canvas.toBlob(resolve, 'image\/png');\n        });\n\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement('a');\n        a.href = url;\n        a.download = `gibbler-snapshot-${Date.now()}.png`;\n        a.click();\n\n        log(`Snapshot saved: ${a.download}`, 'success');\n      } catch (error) {\n        log(`Snapshot error: ${error.message}`, 'error');\n      }\n    }\n\n    formatBytes(bytes) {\n      if (bytes === 0) return '0 Bytes';\n      const k = 1024;\n      const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n      const i = Math.floor(Math.log(bytes) \/ Math.log(k));\n      return parseFloat((bytes \/ Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n    }\n  }\n\n  \/\/ Initialize managers\n  const channelManager = new ChannelManager();\n  const pluginManager = new MPCPluginManager();\n  const videoControls = new VideoControlsManager();\n\n  \/\/ UI Event Handlers\n  function setupEventHandlers() {\n    \/\/ Channel controls\n    $('btnRefreshChannels').addEventListener('click', () =\u003e {\n      channelManager.refreshChannels();\n    });\n\n    \/\/ Plugin controls\n    $('btnStartProcessing').addEventListener('click', () =\u003e {\n      pluginManager.startProcessing();\n    });\n\n    $('btnStopProcessing').addEventListener('click', () =\u003e {\n      pluginManager.stopProcessing();\n    });\n\n    $('btnExportResult').addEventListener('click', () =\u003e {\n      pluginManager.exportResult();\n    });\n\n    \/\/ Video controls\n    $('btnFullscreen').addEventListener('click', () =\u003e {\n      videoControls.toggleFullscreen();\n    });\n\n    $('btnPiP').addEventListener('click', () =\u003e {\n      videoControls.togglePictureInPicture();\n    });\n\n    $('btnRecord').addEventListener('click', () =\u003e {\n      if (videoControls.isRecording) {\n        videoControls.stopRecording();\n      } else {\n        videoControls.startRecording();\n      }\n    });\n\n    $('btnSnapshot').addEventListener('click', () =\u003e {\n      videoControls.takeSnapshot();\n    });\n\n    \/\/ P2P controls\n    $('btnInit').addEventListener('click', async () =\u003e {\n      log('Initializing P2P network...', 'info');\n      $('networkState').textContent = 'connecting';\n      $('networkState').className = 'status-value connecting';\n      \n      setTimeout(() =\u003e {\n        $('networkState').textContent = 'online';\n        $('networkState').className = 'status-value connected';\n        $('peerCount').textContent = Math.floor(Math.random() * 10 + 1);\n        log('P2P network initialized', 'success');\n      }, 2000);\n    });\n\n    $('btnConnect').addEventListener('click', () =\u003e {\n      log('Connecting to peers...', 'info');\n    });\n\n    $('btnStartBroadcast').addEventListener('click', async () =\u003e {\n      try {\n        const stream = await navigator.mediaDevices.getUserMedia({\n          video: true,\n          audio: true\n        });\n        \n        \/\/ Add user's stream as a new channel\n        const userChannel = {\n          id: 'user-broadcast',\n          name: 'Your Broadcast',\n          url: 'local:\/\/user',\n          type: 'p2p',\n          location: 'Your Location',\n          viewers: 1,\n          quality: 'HD',\n          live: true,\n          stream: stream\n        };\n        \n        channelManager.channels.set(userChannel.id, userChannel);\n        channelManager.renderChannels();\n        \n        log('Started broadcasting your stream', 'success');\n        $('btnStartBroadcast').textContent = '📡 Broadcasting';\n        $('btnStartBroadcast').classList.add('btn-success');\n      } catch (error) {\n        log(`Broadcast failed: ${error.message}`, 'error');\n      }\n    });\n\n    $('btnStopBroadcast').addEventListener('click', () =\u003e {\n      channelManager.channels.delete('user-broadcast');\n      channelManager.renderChannels();\n      log('Stopped broadcasting', 'info');\n      $('btnStartBroadcast').textContent = '📡 Start Broadcast';\n      $('btnStartBroadcast').classList.remove('btn-success');\n    });\n\n    \/\/ Log controls\n    $('btnClearLog').addEventListener('click', () =\u003e {\n      $('logContainer').innerHTML = `\n        \u003cdiv class=\"log-entry\"\u003e\n          \u003cspan class=\"log-timestamp\"\u003e[${new Date().toTimeString().split(' ')[0]}]\u003c\/span\u003e\n          \u003cspan class=\"log-level-info\"\u003eLog cleared\u003c\/span\u003e\n        \u003c\/div\u003e\n      `;\n    });\n  }\n\n  \/\/ Initialize application\n  function init() {\n    log('GIBBLER™ P2P Enhanced with Live Channels starting...', 'info');\n    \n    setupEventHandlers();\n    channelManager.loadChannels();\n    pluginManager.renderPlugins();\n    \n    \/\/ Simulate initial stats\n    $('streamLatency').textContent = '—';\n    $('viewerCount').textContent = '—';\n    $('streamQuality').textContent = '—';\n    $('peerCount').textContent = '0';\n    $('networkState').textContent = 'offline';\n    \n    log('Application initialized with live channel browser', 'success');\n    log(`Loaded ${MPC_PLUGINS.length} MPC plugins`, 'info');\n  }\n\n  \/\/ Start the application\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', init);\n  } else {\n    init();\n  }\n\n})();\n\u003c\/script\u003e\n\n\u003c\/body\u003e","products":[{"product_id":"ipictorial-bag-bucket-hat-bundle","title":"iPictorial Bag \u0026 Bucket Hat Bundle","description":"\u003cbody\u003e\n\n\n  \u003cmeta charset=\"utf-8\"\u003e\n  \u003ctitle\u003eiPictorial Bag \u0026amp; Bucket Hat Bundle | Sold Out\u003c\/title\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n  \u003cmeta name=\"description\" content=\"The iPictorial Bag \u0026amp; Bucket Hat Bundle — limited edition streetwear set. Currently sold out. Sign up with the bell to be notified when it’s back in stock.\"\u003e\n  \u003clink rel=\"canonical\" href=\"https:\/\/ipictorial.shop\/products\/ipictorial-bag-bucket-hat-bundle\"\u003e\n\n  \u003c!-- Open Graph --\u003e\n  \u003cmeta property=\"og:title\" content=\"iPictorial Bag \u0026amp; Bucket Hat Bundle\"\u003e\n  \u003cmeta property=\"og:description\" content=\"Exclusive bundle drop. Sold out — join the restock list to be notified first.\"\u003e\n  \u003cmeta property=\"og:type\" content=\"product\"\u003e\n  \u003cmeta property=\"og:url\" content=\"https:\/\/ipictorial.shop\/products\/ipictorial-bag-bucket-hat-bundle\"\u003e\n  \u003cmeta property=\"og:image\" content=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0797\/9551\/9785\/files\/iPictorial_Bag_Hat.png?v=1757614512\"\u003e\n\n  \u003c!-- Twitter --\u003e\n  \u003cmeta name=\"twitter:card\" content=\"summary_large_image\"\u003e\n  \u003cmeta name=\"twitter:title\" content=\"iPictorial Bag \u0026amp; Bucket Hat Bundle\"\u003e\n  \u003cmeta name=\"twitter:description\" content=\"Exclusive bundle drop. Sold out — join the restock list to be notified first.\"\u003e\n  \u003cmeta name=\"twitter:image\" content=\"https:\/\/ipictorial.shop\/cdn\/bag-bucket-bundle.jpg\"\u003e\n\n  \u003cstyle\u003e\n    body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#111;line-height:1.6}\n    .container{max-width:1000px;margin:0 auto;padding:20px}\n    header,footer{border-bottom:1px solid #eee;padding:14px}\n    h1{font-size:28px;margin-bottom:10px}\n    .price{font-size:20px;font-weight:700;margin:10px 0}\n    .was{text-decoration:line-through;color:#777;margin-left:8px;font-size:16px}\n    .card{border:1px solid #eee;border-radius:12px;padding:20px;background:#fafafa;margin-top:20px}\n    .img-wrap{width:100%;aspect-ratio:1\/1;position:relative;background:#f4f4f4}\n    .img-wrap img{position:absolute;inset:0;margin:auto;max-width:100%;max-height:100%;object-fit:contain}\n    .timer{font-weight:800;color:#b00020;margin-top:10px}\n    .soldout{color:#b00020;font-weight:700;margin:15px 0}\n    .notify{display:flex;align-items:center;gap:10px;margin-top:15px}\n    .notify input{padding:10px;border:1px solid #ccc;border-radius:6px;flex:1}\n    .notify button{background:#111;color:#fff;border:none;padding:10px 14px;border-radius:6px;cursor:pointer;font-weight:600}\n    .bell{font-size:20px}\n    .sticky{position:sticky;bottom:0;background:#fff;border-top:1px solid #eee;padding:10px;display:flex;justify-content:space-between;align-items:center}\n  \u003c\/style\u003e\n\n\n\n\u003cheader\u003e\n  \u003ca href=\"\/\"\u003eiPictorial\u003c\/a\u003e • \u003ca href=\"\/collections\/weekly-drops\"\u003eWeekly Drops\u003c\/a\u003e • \u003ca href=\"\/cart\"\u003eCart\u003c\/a\u003e\n\u003c\/header\u003e\n\n\u003cmain class=\"container\"\u003e\n  \u003ch1\u003eiPictorial Bag \u0026amp; Bucket Hat Bundle\u003c\/h1\u003e\n  \u003cdiv class=\"price\"\u003e$145.00 \u003cspan class=\"was\"\u003e$180.00\u003c\/span\u003e\n\u003c\/div\u003e\n\n  \u003cdiv class=\"img-wrap\"\u003e\n    \u003cimg src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0797\/9551\/9785\/files\/iPictorial_Bag_Hat.png?v=1757614512\" alt=\"iPictorial Bag \u0026amp; Bucket Hat Bundle\"\u003e\n  \u003c\/div\u003e\n\n  \u003cdiv class=\"card\"\u003e\n    \u003cp\u003eExclusive \u003cstrong\u003eBag \u0026amp; Bucket Hat Bundle\u003c\/strong\u003e — limited LA streetwear set. This drop sold out fast.\u003c\/p\u003e\n    \u003cp class=\"soldout\"\u003e🚫 Currently Sold Out\u003c\/p\u003e\n\n    \u003cdiv class=\"timer\"\u003eNext drop countdown: \u003cspan id=\"drop-timer\"\u003e\u003c\/span\u003e\n\u003c\/div\u003e\n\n    \u003cdiv class=\"notify\"\u003e\n      \u003cspan class=\"bell\"\u003e🔔\u003c\/span\u003e\n      \u003cinput type=\"email\" id=\"notify-email\" placeholder=\"Enter your email for restock alert\"\u003e\n      \u003cbutton onclick=\"submitNotify()\"\u003eNotify Me\u003c\/button\u003e\n    \u003c\/div\u003e\n    \u003csmall\u003eWe’ll email you first when it’s back in stock.\u003c\/small\u003e\n  \u003c\/div\u003e\n\u003c\/main\u003e\n\n\u003cdiv class=\"sticky\"\u003e\n  \u003cspan\u003eSold Out • Join Restock List\u003c\/span\u003e\n  \u003cbutton onclick=\"document.getElementById('notify-email').focus()\"\u003e🔔 Notify Me\u003c\/button\u003e\n\u003c\/div\u003e\n\n\u003cfooter class=\"container\"\u003e\n  \u003cspan\u003e© iPictorial\u003c\/span\u003e • \n  \u003ca href=\"\/policies\/privacy-policy\"\u003ePrivacy\u003c\/a\u003e • \n  \u003ca href=\"\/policies\/terms-of-service\"\u003eTerms\u003c\/a\u003e\n\u003c\/footer\u003e\n\n\u003cscript\u003e\n  \/\/ 24h countdown (resets daily)\n  function initTimer(){\n    var el=document.getElementById('drop-timer'); if(!el) return;\n    var end=new Date(); end.setDate(end.getDate()+1);\n    end.setHours(23,59,59,999);\n    function fmt(t){\n      if(t\u003c=0) return 'Ends soon';\n      var h=Math.floor((t%86400000)\/3600000),\n          m=Math.floor((t%3600000)\/60000),\n          s=Math.floor((t%60000)\/1000);\n      return h+'h '+m+'m '+s+'s';\n    }\n    function tick(){\n      var t=end-new Date(); el.textContent=fmt(t);\n    }\n    tick(); setInterval(tick,1000);\n  }\n  initTimer();\n\n  \/\/ Restock notify (demo alert, wire to backend\/email service)\n  function submitNotify(){\n    var email=document.getElementById('notify-email').value;\n    if(!email){alert('Enter a valid email');return;}\n    alert('Thanks! You’ll be notified when this item is back in stock.');\n    document.getElementById('notify-email').value='';\n  }\n\u003c\/script\u003e\n\n\n\n\u003c\/body\u003e","brand":"iPictorial","offers":[{"title":"Default Title","offer_id":51190385869097,"sku":null,"price":145.0,"currency_code":"USD","in_stock":false}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0797\/9551\/9785\/files\/iPictorialLifetyleBag_Buckethat.png?v=1757614473"},{"product_id":"ipictorial-mr-steaming-hot-t-shirt","title":"iPictorial Mr Steaming Hot - t-shirt","description":"This t-shirt is everything you've dreamed of and more. It feels soft and lightweight, with the right amount of stretch. It's comfortable and flattering for all. \u003cbr\u003e\n\u003cbr\u003e\n• 100% combed and ring-spun cotton (Heather colors contain polyester)\u003cbr\u003e\n• Fabric weight: 4.2 oz.\/yd.² (142 g\/m²)\u003cbr\u003e\n• Pre-shrunk fabric\u003cbr\u003e\n• Side-seamed construction\u003cbr\u003e\n• Shoulder-to-shoulder taping\u003cbr\u003e\n• Blank product sourced from Nicaragua, Mexico, Honduras, or the US\u003cbr\u003e\n\u003cbr\u003e\nDisclaimer: The fabric is slightly sheer and may appear see-through, especially in lighter colors or under certain lighting conditions.","brand":"iPictorial","offers":[{"title":"XS","offer_id":52267987861801,"sku":"7219645_9575","price":48.0,"currency_code":"USD","in_stock":true},{"title":"S","offer_id":52267987894569,"sku":"7219645_8923","price":48.0,"currency_code":"USD","in_stock":true},{"title":"M","offer_id":52267987927337,"sku":"7219645_8924","price":48.0,"currency_code":"USD","in_stock":true},{"title":"L","offer_id":52267987960105,"sku":"7219645_8925","price":48.0,"currency_code":"USD","in_stock":true},{"title":"XL","offer_id":52267987992873,"sku":"7219645_8926","price":48.0,"currency_code":"USD","in_stock":true},{"title":"2XL","offer_id":52267988025641,"sku":"7219645_8927","price":48.0,"currency_code":"USD","in_stock":true},{"title":"3XL","offer_id":52267988058409,"sku":"7219645_8928","price":48.0,"currency_code":"USD","in_stock":true},{"title":"4XL","offer_id":52267988091177,"sku":"7219645_8929","price":48.0,"currency_code":"USD","in_stock":true},{"title":"5XL","offer_id":52267988123945,"sku":"7219645_12878","price":48.0,"currency_code":"USD","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0797\/9551\/9785\/files\/unisex-staple-t-shirt-black-heather-front-693da183db8e7.jpg?v=1765646741"}],"url":"https:\/\/ipictorial.shop\/collections\/gibbler-p2p.oembed","provider":"iPictorial","version":"1.0","type":"link"}