mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-27 04:20:25 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35e779c1ff | |||
| 09f0e04891 | |||
| 9eff53ba46 | |||
| bde2f7ea55 | |||
| 901861269a | |||
| ba91e2744c | |||
| b7a44024ab | |||
| 3c49bfe54c | |||
| d2ea7c7423 | |||
| db11812bd6 | |||
| 6db8fdbf75 |
@@ -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?
|
||||
|
||||
|
||||
@@ -1167,6 +1167,15 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
}, FORCE_SYNC_TIMEOUT);
|
||||
}
|
||||
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;
|
||||
|
||||
if (isNonEssentialEvent && !hasOtherPeers) {
|
||||
sendResponse({ status: 'ok_solo' });
|
||||
return;
|
||||
}
|
||||
|
||||
emit(message.action, { ...message.payload, peerId });
|
||||
sendResponse({ status: 'ok' });
|
||||
};
|
||||
|
||||
+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.8",
|
||||
"description": "Watch party extension to synchronize video playback on YouTube, Twitch, 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>
|
||||
|
||||
+4
-3
@@ -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);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "1.8.5",
|
||||
"version": "1.8.8",
|
||||
"description": "KoalaSync Build Scripts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
+6
-1
@@ -44,9 +44,10 @@ const httpServer = createServer(app);
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: (origin, callback) => {
|
||||
if (!origin || origin === 'https://sync.koalastuff.net' || origin.startsWith('chrome-extension://')) {
|
||||
if (!origin || origin === 'https://sync.koalastuff.net' || origin.startsWith('chrome-extension://') || origin.startsWith('moz-extension://')) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
log('CORS', `Rejected origin: ${origin}`);
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
},
|
||||
@@ -296,6 +297,7 @@ io.on('connection', (socket) => {
|
||||
|
||||
const ip = socket._clientIp || socket.handshake.address;
|
||||
if (!checkAuthRate(ip, roomId)) {
|
||||
log('AUTH', `Auth rate limit blocked ${ip} from room ${roomId.substring(0, 3)}***`);
|
||||
socket.emit(EVENTS.ERROR, { message: "Too many failed attempts. Try again later." });
|
||||
return;
|
||||
}
|
||||
@@ -317,6 +319,7 @@ io.on('connection', (socket) => {
|
||||
roomCreationLocks.set(roomId, lockPromise);
|
||||
try {
|
||||
if (rooms.size >= MAX_ROOMS) {
|
||||
log('ROOM', `Server at capacity: ${rooms.size}/${MAX_ROOMS} rooms — rejecting join`);
|
||||
socket.emit(EVENTS.ERROR, { message: "Server capacity reached" });
|
||||
return;
|
||||
}
|
||||
@@ -343,11 +346,13 @@ io.on('connection', (socket) => {
|
||||
if (room.passwordHash) {
|
||||
if (!password || !(await bcrypt.compare(password, room.passwordHash))) {
|
||||
recordAuthFailure(ip, roomId);
|
||||
log('AUTH', `Invalid password from ${ip} for room ${roomId.substring(0, 3)}***`);
|
||||
socket.emit(EVENTS.ERROR, { message: "Invalid password" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (room.peers.size >= MAX_PEERS_PER_ROOM) {
|
||||
log('ROOM', `Room full (${room.peers.size}/${MAX_PEERS_PER_ROOM}): ${roomId.substring(0, 3)}***`);
|
||||
socket.emit(EVENTS.ERROR, { message: "Room full" });
|
||||
return;
|
||||
}
|
||||
|
||||
+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.3",
|
||||
"date": "2026-05-25T21:43:51Z"
|
||||
"version": "1.8.8",
|
||||
"date": "2026-05-26T00:34:04Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user