mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 20:18:14 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc0265c836 | |||
| 6ebff9ab4c | |||
| 35e779c1ff | |||
| 09f0e04891 | |||
| 9eff53ba46 | |||
| bde2f7ea55 | |||
| 901861269a | |||
| ba91e2744c | |||
| b7a44024ab | |||
| 3c49bfe54c | |||
| d2ea7c7423 |
@@ -8,7 +8,7 @@
|
||||
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white" alt="Chrome Extension"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><i>KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback across any website—YouTube, Twitch, Netflix, and custom HTML5 players. Built with a focus on <b>Data Sovereignty</b> and <b>Performance</b>.</i></p>
|
||||
<p align="center"><i>KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback on almost any website with a video element—YouTube, Twitch, Netflix, Emby, Jellyfin, and beyond. Built with a focus on <b>Data Sovereignty</b> and <b>Performance</b>.</i></p>
|
||||
|
||||
### 🌟 Why KoalaSync?
|
||||
|
||||
|
||||
+35
-2
@@ -706,7 +706,9 @@ function handleServerEvent(event, data) {
|
||||
if (!ignoreStatus) {
|
||||
peer.playbackState = data.playbackState !== undefined ? data.playbackState : peer.playbackState;
|
||||
peer.currentTime = data.currentTime !== undefined ? data.currentTime : peer.currentTime;
|
||||
peer.lastHeartbeat = Date.now();
|
||||
if (data.playbackState !== undefined || data.currentTime !== undefined) {
|
||||
peer.lastHeartbeat = Date.now();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Migration: replace string peer with normalized object
|
||||
@@ -1169,7 +1171,8 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
addToHistory(message.action, 'You');
|
||||
|
||||
const isNonEssentialEvent = message.action === EVENTS.PLAY || message.action === EVENTS.PAUSE || message.action === EVENTS.SEEK;
|
||||
const hasOtherPeers = currentRoom && Array.isArray(currentRoom.peers) && currentRoom.peers.length > 0;
|
||||
const otherCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.filter(p => (typeof p === 'object' ? p.peerId : p) !== peerId).length : 0;
|
||||
const hasOtherPeers = otherCount > 0;
|
||||
|
||||
if (isNonEssentialEvent && !hasOtherPeers) {
|
||||
sendResponse({ status: 'ok_solo' });
|
||||
@@ -1384,11 +1387,41 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
// Tab removal listener
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
if (tabId === currentTabId) {
|
||||
const wasInRoom = !!currentRoom;
|
||||
currentTabId = null;
|
||||
currentTabTitle = null;
|
||||
chrome.storage.session.set({ currentTabId: null, currentTabTitle: null });
|
||||
updateBadgeStatus();
|
||||
addLog('Target tab closed.', 'warn');
|
||||
|
||||
if (wasInRoom) {
|
||||
const roomAtClose = currentRoom;
|
||||
getSettings().then(settings => {
|
||||
if (currentRoom !== roomAtClose) return;
|
||||
|
||||
emit(EVENTS.PEER_STATUS, {
|
||||
peerId,
|
||||
playbackState: 'paused',
|
||||
currentTime: null,
|
||||
mediaTitle: null,
|
||||
username: settings.username,
|
||||
tabTitle: null
|
||||
});
|
||||
|
||||
if (currentRoom && Array.isArray(currentRoom.peers)) {
|
||||
const me = currentRoom.peers.find(p => (p.peerId || p) === peerId);
|
||||
if (me && typeof me === 'object') {
|
||||
me.playbackState = 'paused';
|
||||
me.currentTime = null;
|
||||
me.mediaTitle = null;
|
||||
me.tabTitle = null;
|
||||
me.lastHeartbeat = Date.now();
|
||||
if (storageInitialized) chrome.storage.session.set({ currentRoom });
|
||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+10
-7
@@ -22,12 +22,15 @@ window.addEventListener('KOALASYNC_JOIN_REQUEST', (e) => {
|
||||
// 3. Listen for Status Updates from the Extension and relay to Website
|
||||
chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (msg.type === 'JOIN_STATUS') {
|
||||
const event = new CustomEvent('KOALASYNC_STATUS', {
|
||||
detail: {
|
||||
success: msg.success,
|
||||
message: msg.message
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
const detail = { success: msg.success, message: msg.message };
|
||||
// Firefox MV3 content scripts run in an isolated world. When dispatching
|
||||
// a CustomEvent with a detail object, Firefox wraps it in an XrayWrapper
|
||||
// that the page's JavaScript cannot destructure (Permission denied).
|
||||
// cloneInto() exposes the object to the page's context correctly.
|
||||
// Chrome doesn't have this issue — cloneInto() is undefined there.
|
||||
const safeDetail = typeof cloneInto === 'function'
|
||||
? cloneInto(detail, document.defaultView)
|
||||
: detail;
|
||||
window.dispatchEvent(new CustomEvent('KOALASYNC_STATUS', { detail: safeDetail }));
|
||||
}
|
||||
});
|
||||
|
||||
+42
-4
@@ -417,6 +417,10 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Suppress only SEEK during visibility grace period (tab re-focus ghost jump).
|
||||
// Play/Pause pass through — user may want to immediately pause after tabbing back.
|
||||
if (Date.now() < visibilityGraceUntil && action === EVENTS.SEEK) return;
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'CONTENT_EVENT',
|
||||
@@ -433,6 +437,38 @@
|
||||
scheduleProactiveHeartbeat();
|
||||
}
|
||||
|
||||
// --- Tab Visibility Handling ---
|
||||
// Browsers (especially Firefox) aggressively throttle background tabs.
|
||||
// When the user returns to a video tab, the video element may have lost
|
||||
// time-sync and fires spurious seek events as it recovers (jumping back).
|
||||
// We suppress only SEEK for a short grace period after tab re-focus.
|
||||
// Play/Pause are NOT suppressed — the user may legitimately want to
|
||||
// pause immediately after switching back.
|
||||
let pageVisible = !document.hidden;
|
||||
let visibilityGraceUntil = 0;
|
||||
const VISIBILITY_GRACE_MS = 1000;
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
pageVisible = false;
|
||||
} else if (!pageVisible) {
|
||||
pageVisible = true;
|
||||
visibilityGraceUntil = Date.now() + VISIBILITY_GRACE_MS;
|
||||
reportLog(`Tab re-focused — suppressing seeks for ${VISIBILITY_GRACE_MS / 1000}s to prevent ghost relay`, 'warn');
|
||||
}
|
||||
});
|
||||
|
||||
// Reset on page hide/show (bfcache, tab discard)
|
||||
window.addEventListener('pagehide', () => { pageVisible = false; });
|
||||
window.addEventListener('pageshow', (event) => {
|
||||
// event.persisted is true ONLY when restored from bfcache, not on initial load
|
||||
if (event.persisted && !pageVisible) {
|
||||
pageVisible = true;
|
||||
visibilityGraceUntil = Date.now() + VISIBILITY_GRACE_MS;
|
||||
reportLog(`Page restored from cache — suppressing seeks for ${VISIBILITY_GRACE_MS / 1000}s`, 'warn');
|
||||
}
|
||||
});
|
||||
|
||||
const handlePlay = () => reportEvent(EVENTS.PLAY);
|
||||
const handlePause = () => reportEvent(EVENTS.PAUSE);
|
||||
|
||||
@@ -444,7 +480,7 @@
|
||||
const current = video.currentTime;
|
||||
if (!Number.isFinite(current)) return;
|
||||
|
||||
// Step 1: Check expectedEvents (programmatic seek suppression)
|
||||
// Step 1: Check expectedEvents (programmatic seek from remote peer — ALWAYS process)
|
||||
if (expectedEvents.has('seek')) {
|
||||
expectedEvents.delete('seek');
|
||||
if (expectedTimeouts['seek']) {
|
||||
@@ -452,20 +488,22 @@
|
||||
delete expectedTimeouts['seek'];
|
||||
}
|
||||
lastReportedSeekTime = current;
|
||||
// No log — this is routine programmatic behavior (Force Sync, lobby, peer command)
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Suppress during visibility grace period (tab re-focus ghost events)
|
||||
if (Date.now() < visibilityGraceUntil) return;
|
||||
|
||||
const delta = lastReportedSeekTime !== null ? Math.abs(current - lastReportedSeekTime) : null;
|
||||
const deltaStr = delta !== null ? `Δ${delta.toFixed(2)}s` : 'Δ?';
|
||||
|
||||
// Step 2: Delta check — skip micro-seeks (buffering, chapter markers, etc.)
|
||||
// Step 3: Delta check — skip micro-seeks (buffering, chapter markers, etc.)
|
||||
if (lastReportedSeekTime !== null && delta < MIN_SEEK_DELTA) {
|
||||
reportLog(`[Seek] Filtered (${deltaStr} < ${MIN_SEEK_DELTA}s threshold) @ ${current.toFixed(2)}s — not relayed`, 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Debounce rapid consecutive seeks (e.g. scrubbing)
|
||||
// Step 4: Debounce rapid consecutive seeks (e.g. scrubbing)
|
||||
// — wait 800ms for the user to settle before relaying
|
||||
if (seekDebounceTimer) clearTimeout(seekDebounceTimer);
|
||||
seekDebounceTimer = setTimeout(() => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "KoalaSync",
|
||||
"version": "1.8.5",
|
||||
"description": "Watch party extension to synchronize video playback on YouTube, Twitch, Netflix, and HTML5 sites in real-time with friends.",
|
||||
"version": "1.8.10",
|
||||
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"tabs",
|
||||
|
||||
+38
-4
@@ -27,15 +27,20 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 16px 0;
|
||||
margin: 0;
|
||||
color: var(--accent);
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -46,6 +51,27 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.popup-version {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s, color 0.2s;
|
||||
}
|
||||
.popup-version:hover {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
.popup-version svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
@@ -288,7 +314,15 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="toast-container"></div>
|
||||
<h1><img src="icons/icon128.png" alt="KoalaSync Logo">KoalaSync</h1>
|
||||
<div class="header-row">
|
||||
<h1><img src="icons/icon128.png" alt="KoalaSync Logo">KoalaSync</h1>
|
||||
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="popup-version">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
<span id="popupVersion">v0.0.0</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="tab-room" title="Room settings and connection">Room</button>
|
||||
|
||||
+20
-5
@@ -105,10 +105,11 @@ async function init() {
|
||||
if (elements.autoCopyInvite) elements.autoCopyInvite.checked = data.autoCopyInvite !== false;
|
||||
|
||||
// Set Version Info
|
||||
const versionTxt = `v${chrome.runtime.getManifest().version}`;
|
||||
const versionEl = document.getElementById('appVersion');
|
||||
if (versionEl) {
|
||||
versionEl.textContent = `v${chrome.runtime.getManifest().version}`;
|
||||
}
|
||||
if (versionEl) versionEl.textContent = versionTxt;
|
||||
const popupVerEl = document.getElementById('popupVersion');
|
||||
if (popupVerEl) popupVerEl.textContent = versionTxt;
|
||||
|
||||
if (data.useCustomServer) {
|
||||
setServerMode(true);
|
||||
@@ -333,7 +334,9 @@ function startInterpolation() {
|
||||
const peer = activePeers.find(p => p.peerId === peerId);
|
||||
if (peer && peer.playbackState === 'playing' && peer.currentTime != null && peer.lastHeartbeat) {
|
||||
const elapsed = (Date.now() - peer.lastHeartbeat) / 1000;
|
||||
el.textContent = formatTime(peer.currentTime + elapsed);
|
||||
if (elapsed < 45) {
|
||||
el.textContent = formatTime(peer.currentTime + elapsed);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
@@ -476,7 +479,9 @@ function updatePeerList(peers) {
|
||||
let displayTime = p.currentTime;
|
||||
if (p.playbackState === 'playing' && p.lastHeartbeat && p.currentTime != null) {
|
||||
const elapsed = (Date.now() - p.lastHeartbeat) / 1000;
|
||||
displayTime += elapsed;
|
||||
if (elapsed < 45) {
|
||||
displayTime += elapsed;
|
||||
}
|
||||
}
|
||||
timeSpan.textContent = formatTime(displayTime);
|
||||
statusLine.appendChild(timeSpan);
|
||||
@@ -1128,6 +1133,11 @@ elements.playBtn.addEventListener('click', () => {
|
||||
type: 'CONTENT_EVENT',
|
||||
action: EVENTS.PLAY,
|
||||
payload: {}
|
||||
}, (response) => {
|
||||
if (response && response.status === 'ok_solo') {
|
||||
elements.playBtn.textContent = '▶ Play';
|
||||
elements.playBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
// Safety reset: restore button after 2.5s in case no peers respond
|
||||
setTimeout(() => {
|
||||
@@ -1149,6 +1159,11 @@ elements.pauseBtn.addEventListener('click', () => {
|
||||
type: 'CONTENT_EVENT',
|
||||
action: EVENTS.PAUSE,
|
||||
payload: {}
|
||||
}, (response) => {
|
||||
if (response && response.status === 'ok_solo') {
|
||||
elements.pauseBtn.textContent = '⏸ Pause';
|
||||
elements.pauseBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
// Safety reset: restore button after 2.5s in case no peers respond
|
||||
setTimeout(() => {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "1.8.6",
|
||||
"version": "1.8.9",
|
||||
"description": "KoalaSync Build Scripts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
export const PROTOCOL_VERSION = "1.0.0";
|
||||
export const APP_VERSION = "1.3.1";
|
||||
export const APP_VERSION = "1.8.6";
|
||||
|
||||
export const OFFICIAL_SERVER_URL = 'wss://syncserver.koalastuff.net';
|
||||
export const OFFICIAL_LANDING_PAGE_URL = 'https://sync.koalastuff.net';
|
||||
|
||||
+9
-6
@@ -3,15 +3,18 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KoalaSync | Real-time Video Synchronization for Friends</title>
|
||||
<meta name="description" content="Watch YouTube, Twitch, and HTML5 videos in sync with friends. KoalaSync is a privacy-first, open-source browser extension for Chrome and Firefox.">
|
||||
<title>KoalaSync | Sync Netflix, Emby, Jellyfin & Any Video with Friends – Browser Extension</title>
|
||||
<meta name="description" content="Watch Netflix, Emby, Jellyfin, YouTube, Twitch and any HTML5 video in perfect sync with friends. KoalaSync is a privacy-first, open-source browser extension for Chrome and Firefox. Works on almost any site with a video element.">
|
||||
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/png" href="assets/logo.png">
|
||||
<meta property="og:title" content="KoalaSync | Sync your videos">
|
||||
<meta property="og:description" content="Watch together, stay in sync. Privacy-first video synchronization.">
|
||||
<link rel="canonical" href="https://sync.koalastuff.net/">
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta property="og:title" content="KoalaSync | Sync Netflix, Emby, Jellyfin & Any Video with Friends">
|
||||
<meta property="og:description" content="Watch Netflix, Emby, Jellyfin, YouTube, Twitch and any HTML5 video in perfect sync. Privacy-first, open-source browser extension for Chrome & Firefox.">
|
||||
<meta property="og:image" content="https://sync.koalastuff.net/assets/logo.png">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://sync.koalastuff.net/">
|
||||
|
||||
<script src="lang-init.js"></script>
|
||||
</head>
|
||||
@@ -100,7 +103,7 @@
|
||||
<div class="mock-label"><span lang="en">Invite Link</span><span lang="de">Einladungs-Link</span></div>
|
||||
<div class="mock-invite-box">
|
||||
<input type="text" class="mock-input" value="https://sync.koalastuff.net/join.html#join:brave-eagle-80:pass" readonly>
|
||||
<button class="mock-btn" style="padding: 0.35rem 0.5rem;" onclick="navigator.clipboard.writeText('https://sync.koalastuff.net/join.html#join:brave-eagle-80:pass')"><span lang="en">Copy</span><span lang="de">Kopieren</span></button>
|
||||
<button class="mock-btn" style="padding: 0.35rem 0.5rem;"><span lang="en">Copy</span><span lang="de">Kopieren</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mock-card" style="margin-bottom: 12px;">
|
||||
@@ -454,7 +457,7 @@
|
||||
<span class="t-key">-</span> <span class="t-str">MAX_PEERS_PER_ROOM=50</span>
|
||||
<span class="t-key">pids_limit:</span> <span class="t-val">2048</span></code></pre>
|
||||
<div style="margin-top: 1rem; font-size: 0.75rem; text-align: right; padding-right: 0.5rem;">
|
||||
<a href="https://github.com/Shik3i/KoalaSync/pkgs/container/koalasync" target="_blank" style="color: var(--accent); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; font-weight: 600; transition: opacity 0.2s; opacity: 0.85;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.85'">
|
||||
<a href="https://github.com/Shik3i/KoalaSync/pkgs/container/koalasync" target="_blank" style="color: var(--accent); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; font-weight: 600; transition: opacity 0.2s; opacity: 0.85;">
|
||||
📦 <span lang="en">View all image tags on GitHub Packages</span><span lang="de">Alle Image-Tags auf GitHub Packages ansehen</span> →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# KoalaSync Website — Allow all crawlers, full indexing
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Sitemap for search engines
|
||||
Sitemap: https://sync.koalastuff.net/sitemap.xml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "1.8.5",
|
||||
"date": "2026-05-25T21:56:58Z"
|
||||
"version": "1.8.9",
|
||||
"date": "2026-05-26T01:07:16Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user