diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf45839..2368f05 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,16 +112,29 @@ KoalaSync uses a **single source of truth** for all protocol constants in `share ## Bug Report Template -When filing a bug, include as much of the following as possible: +When filing a bug, the easiest way is to use the **Copy Logs** button in the extension's **Status** tab. It copies a fully formatted Markdown report to your clipboard containing: + +- System info (version, protocol, peer ID, browser) +- Connection status (server, room, peers, reconnect state) +- Video debug info (playback state, readyState, network state, dimensions, error codes, shadow DOM detection, platform) +- Action history (last 20 events) +- Log entries (last 50) + +Simply paste the clipboard contents into your GitHub issue and add: + +| Field | Example | +|-------|---------| +| **Steps to Reproduce** | 1. Create room → 2. Join from second browser → 3. Play video | +| **Expected Behavior** | Both peers play simultaneously | +| **Actual Behavior** | Peer B remains paused | + +If you cannot access the Status tab, include as much of the following manually: | Field | Example | |-------|---------| | **Browser** | Chrome 125, Firefox 128 | -| **Extension Version** | v1.9.3 (visible in Dev tab) | -| **Dev Tab Output** | Connection status, video debug info, log entries | -| **Steps to Reproduce** | 1. Create room → 2. Join from second browser → 3. Play video | -| **Expected Behavior** | Both peers play simultaneously | -| **Actual Behavior** | Peer B remains paused | +| **Extension Version** | v1.9.3 (visible at bottom of Settings tab) | +| **Website/Platform** | Netflix, YouTube, Twitch, Jellyfin, etc. --- diff --git a/TESTED_SERVICES.md b/TESTED_SERVICES.md index fc81093..4f7fba0 100644 --- a/TESTED_SERVICES.md +++ b/TESTED_SERVICES.md @@ -11,7 +11,7 @@ This document tracks which streaming platforms and media servers have been teste | **Jellyfin** | ✅ Full | ✅ Full | ✅ Full | Self-hosted. Full HTML5 player access. | | **Plex** | Not tested | Not tested | Not tested | Community reports indicate compatibility via HTML5 player mode. | | **Disney+** | Not tested | Not tested | Not tested | Widevine DRM may restrict title detection similar to Netflix. | -| **Prime Video** | Not tested | Not tested | Not tested | DRM-protected player may restrict script injection. | +| **Prime Video** | ❌ None | ❌ None | ❌ | Extension cannot detect video elements. Play/Pause do not work. Media title detection fails. DRM and custom player prevent script access. | | **HBO Max / Max** | Not tested | Not tested | Not tested | — | | **Crunchyroll** | Not tested | Not tested | Not tested | — | | **Vimeo** | Not tested | Not tested | Not tested | — | diff --git a/extension/background.js b/extension/background.js index 509922b..d2ec8e7 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1273,7 +1273,12 @@ async function handleAsyncMessage(message, sender, sendResponse) { targetTabId: currentTabId, episodeLobby: episodeLobby, reconnectAttempts, - reconnectSlowMode: reconnectFailed + reconnectSlowMode: reconnectFailed, + roomId: currentRoom ? currentRoom.roomId : null, + serverUrl: currentServerUrl, + version: APP_VERSION, + protocolVersion: PROTOCOL_VERSION, + roomPassword: currentRoom ? currentRoom.password : null }); } else if (message.type === 'LEAVE_ROOM') { emit(EVENTS.LEAVE_ROOM, { peerId }); diff --git a/extension/content.js b/extension/content.js index 79efce8..0bf7fb4 100644 --- a/extension/content.js +++ b/extension/content.js @@ -433,6 +433,41 @@ if (message.type === 'GET_VIDEO_STATE') { const video = findVideo(); + + const platform = (() => { + const h = window.location.hostname.toLowerCase(); + if (h.includes('youtube.com')) return 'YouTube'; + if (h.includes('twitch.tv')) return 'Twitch'; + if (h.includes('netflix.com')) return 'Netflix'; + if (h.includes('primevideo.com') || h.includes('amazon.')) return 'Prime Video'; + if (h.includes('disneyplus.com')) return 'Disney+'; + if (h.includes('hulu.com')) return 'Hulu'; + if (h.includes('max.com') || h.includes('hbomax.com')) return 'Max/HBO'; + if (h.includes('vimeo.com')) return 'Vimeo'; + if (h.includes('dailymotion.com')) return 'Dailymotion'; + return 'Generic'; + })(); + + const networkStates = ['NETWORK_EMPTY', 'NETWORK_IDLE', 'NETWORK_LOADING', 'NETWORK_NO_SOURCE']; + const readyStates = ['HAVE_NOTHING', 'HAVE_METADATA', 'HAVE_CURRENT_DATA', 'HAVE_FUTURE_DATA', 'HAVE_ENOUGH_DATA']; + + const videoCount = document.querySelectorAll('video').length; + const inShadowDom = (() => { + let el = video; + while (el) { + if (el.toString() === '[object ShadowRoot]') return true; + el = el.parentNode; + } + // Also check if any potential host has shadow root (even if no video found) + if (!video) { + const hosts = document.querySelectorAll('[id*="player" i], [class*="player" i], [id*="video" i], [class*="video" i]'); + for (const host of hosts) { + if (host.shadowRoot) return true; + } + } + return false; + })(); + if (video) { const dataAttributes = {}; if (video.attributes) { @@ -450,24 +485,59 @@ artwork: Array.from(navigator.mediaSession.metadata.artwork || []).map(a => a.src) } : null; + const errorInfo = video.error ? { + code: video.error.code, + message: video.error.message + } : null; + sendResponse({ + found: true, paused: video.paused, currentTime: video.currentTime, duration: video.duration || 0, readyState: video.readyState, + readyStateLabel: readyStates[video.readyState] || 'UNKNOWN', + networkState: video.networkState, + networkStateLabel: networkStates[video.networkState] || 'UNKNOWN', muted: video.muted, volume: video.volume, playbackRate: video.playbackRate, + videoWidth: video.videoWidth, + videoHeight: video.videoHeight, + seeking: video.seeking, + ended: video.ended, + error: errorInfo, + buffered: video.buffered && video.buffered.length > 0 + ? Array.from({ length: video.buffered.length }, (_, i) => + `${video.buffered.start(i).toFixed(1)}-${video.buffered.end(i).toFixed(1)}s`).join(', ') + : 'none', + loop: video.loop, url: window.location.href, + pageTitle: document.title, id: video.id || 'none', className: video.className || 'none', src: video.src || 'none', currentSrc: video.currentSrc || 'none', dataAttributes, - metadata + metadata, + videoCount, + inShadowDom, + platform }); } else { - sendResponse({ error: 'No video found' }); + sendResponse({ + found: false, + videoCount, + inShadowDom, + platform, + url: window.location.href, + pageTitle: document.title, + metadata: (navigator.mediaSession && navigator.mediaSession.metadata) ? { + title: navigator.mediaSession.metadata.title, + artist: navigator.mediaSession.metadata.artist, + album: navigator.mediaSession.metadata.album + } : null + }); } } }); diff --git a/extension/popup.js b/extension/popup.js index 412467d..70040ea 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -1419,17 +1419,163 @@ chrome.runtime.onMessage.addListener((msg) => { }); elements.copyLogs.addEventListener('click', () => { - chrome.runtime.sendMessage({ type: 'GET_LOGS' }, (logs) => { - if (!logs || logs.length === 0) return; - const text = logs.map(l => `[${l.timestamp}] [${l.type}] ${l.message}`).join('\n'); + const utcNow = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; + const userAgent = String(navigator.userAgent || ''); + + const safe = (val, fb) => (val != null ? val : fb); + + Promise.all([ + new Promise(resolve => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, resolve)), + new Promise(resolve => chrome.runtime.sendMessage({ type: 'GET_LOGS' }, resolve)), + new Promise(resolve => chrome.runtime.sendMessage({ type: 'GET_HISTORY' }, resolve)) + ]).then(([status, logs, history]) => { + status = status || {}; + logs = logs || []; + history = history || []; + + const videoPromise = (status && status.targetTabId) + ? new Promise(resolve => chrome.runtime.sendMessage({ type: 'GET_VIDEO_STATE', tabId: status.targetTabId }, resolve)) + : Promise.resolve(null); + + videoPromise.then(rawVideo => { + const vs = rawVideo || {}; + const lines = []; + + // ── Header ── + const verStr = safe(status.version, '?'); + lines.push('# KoalaSync Debug Report'); + lines.push(`> **v${verStr}** | ${utcNow}`); + lines.push(''); + + // ── System ── + lines.push('## System'); + lines.push(`- **Protocol:** ${safe(status.protocolVersion, '?')}`); + lines.push(`- **Peer ID:** \`${safe(status.peerId, '?')}\``); + lines.push(`- **User Agent:** ${userAgent}`); + lines.push(''); + + // ── Tab ── + if (rawVideo) { + lines.push('## Tab'); + if (vs.pageTitle) lines.push(`- **Title:** ${vs.pageTitle}`); + if (vs.url) lines.push(`- **URL:** ${vs.url}`); + if (vs.platform) lines.push(`- **Platform:** ${safe(vs.platform, '?')}`); + lines.push(`- **Video Count:** ${safe(vs.videoCount, 0)} | **Shadow DOM:** ${vs.inShadowDom ? 'YES' : 'NO'}`); + lines.push(''); + } + + // ── Connection ── + lines.push('## Connection'); + const st = safe(status.status, 'disconnected'); + const emoji = st === 'connected' ? '\uD83D\uDFE2' + : st === 'connecting' ? '\uD83D\uDFE1' + : st === 'reconnecting' ? '\uD83D\uDFE0' + : '\uD83D\uDD34'; + lines.push(`- **Status:** ${emoji} ${st}`); + lines.push(`- **Server:** \`${safe(status.serverUrl, '?')}\``); + if (status.roomId) { + lines.push(`- **Room:** \`${status.roomId}\``); + const peers = Array.isArray(status.peers) ? status.peers : []; + const peerNames = peers.map(p => (p && (p.username || p.peerId)) || '?').join(', '); + lines.push(`- **Peers (${peers.length}):** ${peers.length > 0 ? peerNames : 'none'}`); + } else { + lines.push('- **Room:** none'); + } + if (safe(status.reconnectAttempts, 0) > 0) lines.push(`- **Reconnect Attempts:** ${status.reconnectAttempts}`); + lines.push(''); + + // ── Video ── + lines.push('## Video'); + if (!rawVideo) { + lines.push('- *No tab selected / communication failed*'); + } else if (!vs.found) { + lines.push('- **Found:** \u274C NO VIDEO ELEMENT'); + if (vs.videoCount != null) lines.push(`- **Video Tags:** ${vs.videoCount}`); + if (vs.inShadowDom != null) lines.push(`- **Shadow DOM:** ${vs.inShadowDom ? 'YES (checked)' : 'NO'}`); + if (vs.metadata) { + if (vs.metadata.title) lines.push(`- **MediaSession Title:** "${vs.metadata.title}"`); + if (vs.metadata.artist) lines.push(`- **MediaSession Artist:** "${vs.metadata.artist}"`); + if (vs.metadata.album) lines.push(`- **MediaSession Album:** "${vs.metadata.album}"`); + } + } else { + const timeStr = (typeof vs.currentTime === 'number' ? vs.currentTime.toFixed(2) : '?') + 's / ' + + (typeof vs.duration === 'number' ? vs.duration.toFixed(2) : '?') + 's'; + const readyLabel = safe(vs.readyStateLabel, '?'); + const readyOk = safe(vs.readyState, -1) >= 3; + const netLabel = safe(vs.networkStateLabel, '?'); + const dimOk = safe(vs.videoWidth, 0) > 0 && safe(vs.videoHeight, 0) > 0; + + lines.push(`- **State:** ${vs.paused ? 'PAUSED' : 'PLAYING'}`); + lines.push(`- **Time:** ${timeStr}`); + lines.push(`- **ReadyState:** ${readyOk ? '\u2705' : '\u26A0\uFE0F'} ${safe(vs.readyState, '?')} (${readyLabel})`); + lines.push(`- **Network:** ${safe(vs.networkState, '?')} (${netLabel})`); + lines.push(`- **Buffered:** ${safe(vs.buffered, '?')}`); + lines.push(`- **Dimensions:** ${safe(vs.videoWidth, '?')}x${safe(vs.videoHeight, '?')}${dimOk ? '' : ' \u26A0\uFE0F 0x0'}`); + lines.push(`- **Muted:** ${safe(vs.muted, '?')} | **Volume:** ${safe(vs.volume, '?')} | **Speed:** ${safe(vs.playbackRate, '?')}x`); + lines.push(`- **Seeking:** ${safe(vs.seeking, '?')} | **Ended:** ${safe(vs.ended, '?')} | **Loop:** ${safe(vs.loop, '?')}`); + if (vs.error && vs.error.code != null) { + lines.push(`- **Error:** code=${vs.error.code}, msg="${safe(vs.error.message, 'none')}"`); + } + lines.push(`- **ID:** \`${safe(vs.id, '')}\` | **Class:** \`${safe(vs.className, '')}\``); + lines.push(`- **Source:** \`${safe(vs.currentSrc, safe(vs.src, 'none'))}\``); + if (vs.metadata) { + if (vs.metadata.title) lines.push(`- **MediaSession Title:** "${vs.metadata.title}"`); + if (vs.metadata.artist) lines.push(`- **MediaSession Artist:** "${vs.metadata.artist}"`); + if (vs.metadata.album) lines.push(`- **MediaSession Album:** "${vs.metadata.album}"`); + } + if (vs.dataAttributes) { + const keys = Object.keys(vs.dataAttributes); + if (keys.length > 0) { + lines.push('- **Data Attributes:**'); + for (const k of keys) lines.push(` - \`${k}\` = "${safe(vs.dataAttributes[k], '')}"`); + } + } + } + lines.push(''); + + // ── Action History (last 20) ── + lines.push('## Action History (last 20)'); + if (history && history.length > 0) { + const recent = history.slice(-20); + lines.push('```'); + for (const h of recent) { + if (!h) continue; + const ts = safe(h.timestamp, ''); + const evt = safe(h.event, safe(h.type, '?')); + const from = h.senderId ? ` (${h.senderId})` : (h.peerId ? ` (${h.peerId})` : ''); + const extra = h.detail ? ` \u2192 ${h.detail}` : ''; + lines.push(`[${ts}] ${evt}${from}${extra}`); + } + lines.push('```'); + } else { + lines.push('*No history entries*'); + } + lines.push(''); + + // ── Logs (last 50) ── + lines.push('## Logs (last 50)'); + if (logs && logs.length > 0) { + const recent = logs.slice(-50); + lines.push('```'); + for (const l of recent) { + if (!l) continue; + lines.push(`[${safe(l.timestamp, '')}] [${safe(l.type, '?')}] ${safe(l.message, '')}`); + } + lines.push('```'); + } else { + lines.push('*No log entries*'); + } + + const text = lines.join('\n'); navigator.clipboard.writeText(text).then(() => { const original = elements.copyLogs.textContent; elements.copyLogs.textContent = getMessage('TOAST_LOGS_COPIED'); - setTimeout(() => elements.copyLogs.textContent = original, 2000); + setTimeout(() => { elements.copyLogs.textContent = original; }, 2000); }).catch(() => { showToast(getMessage('TOAST_COPY_FAILED'), 'error'); }); }); + }); }); function refreshDebugInfo() { @@ -1445,27 +1591,27 @@ function refreshDebugInfo() { // Request direct state from the content script via background chrome.runtime.sendMessage({ type: 'GET_VIDEO_STATE', tabId: res.targetTabId }, (state) => { - if (!state || state.error) { + if (!state || (!state.found && state.error)) { if (elements.videoDebug) elements.videoDebug.textContent = getMessage('DEBUG_COMM_FAIL'); return; } if (elements.videoDebug) { elements.videoDebug.innerHTML = ''; - + const addField = (label, value, color = null) => { const row = document.createElement('div'); row.style.marginBottom = '4px'; if (color) row.style.color = color; - + const b = document.createElement('b'); b.textContent = `${label}: `; b.style.color = 'var(--text-muted)'; - + const span = document.createElement('span'); span.textContent = value; span.style.wordBreak = 'break-all'; - + row.appendChild(b); row.appendChild(span); elements.videoDebug.appendChild(row); @@ -1473,29 +1619,92 @@ function refreshDebugInfo() { const addSection = (title) => { const div = document.createElement('div'); - div.style.cssText = 'margin: 8px 0 4px 0; border-bottom: 1px solid #334155; padding-bottom: 2px; color: var(--accent); font-weight: bold; font-size: 9px;'; - div.textContent = title.toUpperCase(); + div.style.cssText = 'margin: 8px 0 4px 0; border-bottom: 1px solid #334155; padding-bottom: 2px; color: var(--accent); font-weight: bold; font-size: 10px; text-transform: uppercase;'; + div.textContent = title; elements.videoDebug.appendChild(div); }; - addField('STATE', state.paused ? 'PAUSED' : 'PLAYING', 'var(--accent)'); - addField('TIME', `${state.currentTime.toFixed(2)}s / ${(state.duration || 0).toFixed(2)}s`); - addField('READY', state.readyState); - + if (!state.found) { + // No video found — show diagnostic info + addSection('Video Detection'); + const notFound = document.createElement('div'); + notFound.style.cssText = 'color: #ef4444; font-weight: 700; margin-bottom: 8px;'; + notFound.textContent = 'NO VIDEO ELEMENT FOUND'; + elements.videoDebug.appendChild(notFound); + + addField('Platform', state.platform || '?', 'var(--accent)'); + addField('Page Title', state.pageTitle || '?'); + addField('URL', state.url || '?'); + addField('Video Tags', String(state.videoCount || 0)); + addField('Shadow DOM', state.inShadowDom ? 'YES (checked)' : 'NO'); + + if (state.metadata) { + addSection('Media Session API'); + addField('Title', state.metadata.title || 'n/a'); + addField('Artist', state.metadata.artist || 'n/a'); + addField('Album', state.metadata.album || 'n/a'); + } + + const hint = document.createElement('div'); + hint.style.cssText = 'margin-top: 12px; padding: 8px; background: rgba(251,191,36,0.1); border-left: 3px solid #fbbf24; border-radius: 4px; font-size: 10px; color: var(--text-muted);'; + hint.textContent = 'Is a video currently playing? The extension only detects