Compare commits

..

3 Commits

Author SHA1 Message Date
Timo ebf3178e32 feat: multi-video overview in debug report
- GET_VIDEO_STATE now returns allVideos[] with summary per video element
- Copy report shows markdown table when multiple videos on page
- Table includes: resolution, muted, paused, readyState, duration, and marks selected target
- Helps diagnose Prime Video scenarios where preview vs main video both exist
2026-06-01 15:58:31 +02:00
Timo ba96cf2765 fix: prioritize largest video element, fix history action field in debug report
- findVideo() now scores all video elements by size + unmuted + duration
- Fixes Prime Video selecting 0x0 placeholder instead of actual player
- Fix history entries showing '?' by using h.action instead of h.event/h.type
- Update Prime Video status in TESTED_SERVICES.md to partial support
2026-06-01 15:57:11 +02:00
GitHub Action d026ed891a chore(release): update versions to v2.0.0 [skip ci] 2026-06-01 13:50:26 +00:00
6 changed files with 77 additions and 19 deletions
+1 -1
View File
@@ -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** | ❌ None | ❌ None | ❌ | Extension cannot detect video elements. Play/Pause do not work. Media title detection fails. DRM and custom player prevent script access. |
| **Prime Video** | ⚠️ Partial | ⚠️ Partial | ❌ | Video elements detected (2 on page, picks larger one). Playback state + time readable. However, the preview/trailer video may be selected instead of the main content. Play/Pause commands may not reach the correct player. Title detection from MediaSession API may work for some content. |
| **HBO Max / Max** | Not tested | Not tested | Not tested | — |
| **Crunchyroll** | Not tested | Not tested | Not tested | — |
| **Vimeo** | Not tested | Not tested | Not tested | — |
+53 -13
View File
@@ -78,21 +78,41 @@
}
// --- Helper: find the best video element on the page ---
// Prefers larger, visible videos over tiny preview/trailer elements.
function findVideo(root = document) {
const video = root.querySelector('video');
if (video) return video;
// Optimize: scan only potential player, video, media, and stream hosts by matching typical keywords (case-insensitive)
// or common custom element tags. This prevents recursive scanning of thousands of standard DOM nodes (div, span, a, etc.)
// while guaranteeing 100% airtight compatibility with all video web components in the wild.
const potentialHosts = root.querySelectorAll('[id*="player" i], [class*="player" i], [id*="video" i], [class*="video" i], [id*="media" i], [class*="media" i], [id*="stream" i], [class*="stream" i], ytd-player, netflix-player, emby-player, jellyfin-player, video-player');
for (const el of potentialHosts) {
if (el.shadowRoot) {
const found = findVideo(el.shadowRoot);
if (found) return found;
const allVideos = root.querySelectorAll('video');
if (allVideos.length === 0) {
// Optimize: scan only potential player, video, media, and stream hosts by matching typical keywords (case-insensitive)
// or common custom element tags. This prevents recursive scanning of thousands of standard DOM nodes (div, span, a, etc.)
// while guaranteeing 100% airtight compatibility with all video web components in the wild.
const potentialHosts = root.querySelectorAll('[id*="player" i], [class*="player" i], [id*="video" i], [class*="video" i], [id*="media" i], [class*="media" i], [id*="stream" i], [class*="stream" i], ytd-player, netflix-player, emby-player, jellyfin-player, video-player');
for (const el of potentialHosts) {
if (el.shadowRoot) {
const found = findVideo(el.shadowRoot);
if (found) return found;
}
}
return null;
}
// Multiple videos found → pick the best one
if (allVideos.length === 1) return allVideos[0];
let best = null;
let bestScore = -1;
for (const v of allVideos) {
if (v.tagName !== 'VIDEO') continue;
// Score: visible area + bonus for unmuted + bonus for longer duration
const area = (v.videoWidth || v.offsetWidth || 0) * (v.videoHeight || v.offsetHeight || 0);
const unmutedBonus = v.muted ? 0 : 100000;
const durationBonus = (v.duration && isFinite(v.duration) ? v.duration : 0) * 100;
const score = area + unmutedBonus + durationBonus;
if (score > bestScore) {
bestScore = score;
best = v;
}
}
return null;
return best;
}
// --- Episode Auto-Sync: Detection ---
@@ -468,6 +488,24 @@
return false;
})();
// Build multi-video summary for debug reports
const allVideos = [];
const allVideoEls = document.querySelectorAll('video');
for (let i = 0; i < allVideoEls.length; i++) {
const v = allVideoEls[i];
allVideos.push({
index: i,
width: v.videoWidth || v.offsetWidth || 0,
height: v.videoHeight || v.offsetHeight || 0,
muted: v.muted,
paused: v.paused,
duration: (v.duration && isFinite(v.duration)) ? Math.round(v.duration) : 0,
readyState: v.readyState,
src: (v.currentSrc || v.src || '').substring(0, 80),
selected: v === video
});
}
if (video) {
const dataAttributes = {};
if (video.attributes) {
@@ -522,7 +560,8 @@
metadata,
videoCount,
inShadowDom,
platform
platform,
allVideos
});
} else {
sendResponse({
@@ -530,6 +569,7 @@
videoCount,
inShadowDom,
platform,
allVideos,
url: window.location.href,
pageTitle: document.title,
metadata: (navigator.mediaSession && navigator.mediaSession.metadata) ? {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "KoalaSync",
"version": "1.9.3",
"version": "2.0.0",
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
"permissions": [
"storage",
+19 -1
View File
@@ -1462,6 +1462,24 @@ elements.copyLogs.addEventListener('click', () => {
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('');
// Multi-video overview
const videos = Array.isArray(vs.allVideos) ? vs.allVideos : [];
if (videos.length > 1) {
lines.push('### All Videos on Page');
lines.push('');
lines.push('| # | Resolution | Muted | Paused | Ready | Duration | Selected |');
lines.push('|---|------------|-------|--------|-------|----------|----------|');
const rl = ['HAVE_NOTHING', 'HAVE_METADATA', 'HAVE_CURRENT_DATA', 'HAVE_FUTURE_DATA', 'HAVE_ENOUGH_DATA'];
for (const v of videos) {
if (!v) continue;
const sel = v.selected ? ' **\u2190 TARGET**' : '';
const dim = `${safe(v.width, '?')}x${safe(v.height, '?')}`;
const rs = (v.readyState != null && v.readyState >= 0 && v.readyState <= 4) ? rl[v.readyState] : '?';
lines.push(`| ${safe(v.index, '?')} | ${dim} | ${safe(v.muted, '?')} | ${safe(v.paused, '?')} | ${rs} | ${safe(v.duration, 0)}s |${sel} |`);
}
lines.push('');
}
}
// ── Connection ──
@@ -1541,7 +1559,7 @@ elements.copyLogs.addEventListener('click', () => {
for (const h of recent) {
if (!h) continue;
const ts = safe(h.timestamp, '');
const evt = safe(h.event, safe(h.type, '?'));
const evt = safe(h.action, '?');
const from = h.senderId ? ` (${h.senderId})` : (h.peerId ? ` (${h.peerId})` : '');
const extra = h.detail ? ` \u2192 ${h.detail}` : '';
lines.push(`[${ts}] ${evt}${from}${extra}`);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "koalasync",
"version": "1.9.3",
"version": "2.0.0",
"description": "KoalaSync Build Scripts",
"private": true,
"scripts": {
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "1.9.3",
"date": "2026-05-30T00:01:24Z"
"version": "2.0.0",
"date": "2026-06-01T13:50:26Z"
}