Compare commits

...

3 Commits

Author SHA1 Message Date
MacBook 0f611fa112 chore: bump version to 1.1.0 2026-04-22 18:31:28 +02:00
MacBook 67f872ffd2 feat(sync): implement end-to-end media title synchronization 2026-04-22 18:29:53 +02:00
MacBook e40e2e5101 feat(dev): implement extended video metadata and data-attribute inspector 2026-04-22 18:22:59 +02:00
5 changed files with 94 additions and 33 deletions
+28 -2
View File
@@ -191,6 +191,22 @@
if (message.type === 'GET_VIDEO_STATE') {
const video = findVideo();
if (video) {
const dataAttributes = {};
if (video.attributes) {
for (const attr of video.attributes) {
if (attr.name.startsWith('data-')) {
dataAttributes[attr.name] = attr.value;
}
}
}
const metadata = (navigator.mediaSession && navigator.mediaSession.metadata) ? {
title: navigator.mediaSession.metadata.title,
artist: navigator.mediaSession.metadata.artist,
album: navigator.mediaSession.metadata.album,
artwork: Array.from(navigator.mediaSession.metadata.artwork || []).map(a => a.src)
} : null;
sendResponse({
paused: video.paused,
currentTime: video.currentTime,
@@ -199,7 +215,12 @@
muted: video.muted,
playbackRate: video.playbackRate,
url: window.location.href,
id: video.id || 'none'
id: video.id || 'none',
className: video.className || 'none',
src: video.src || 'none',
currentSrc: video.currentSrc || 'none',
dataAttributes,
metadata
});
} else {
sendResponse({ error: 'No video found' });
@@ -215,6 +236,8 @@
const current = video.currentTime;
if (!Number.isFinite(current)) return;
const mediaTitle = (navigator.mediaSession && navigator.mediaSession.metadata) ? navigator.mediaSession.metadata.title : null;
const eventState = action === EVENTS.PLAY ? 'playing' : (action === EVENTS.PAUSE ? 'paused' : (action === EVENTS.SEEK ? 'seek' : null));
if (eventState && lastTargetState === eventState) {
@@ -228,6 +251,7 @@
payload: {
currentTime: current,
targetTime: current,
mediaTitle: mediaTitle,
timestamp: Date.now()
}
});
@@ -286,11 +310,13 @@
const heartbeatInterval = setInterval(() => {
const video = findVideo();
if (video) {
const mediaTitle = (navigator.mediaSession && navigator.mediaSession.metadata) ? navigator.mediaSession.metadata.title : null;
chrome.runtime.sendMessage({
type: 'HEARTBEAT',
payload: {
playbackState: video.paused ? 'paused' : 'playing',
currentTime: video.currentTime
currentTime: video.currentTime,
mediaTitle: mediaTitle
}
}).catch(err => {
if (err.message.includes('Extension context invalidated')) {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "KoalaSync",
"version": "1.0.4",
"version": "1.1.0",
"description": "Synchronize video playback across different tabs and users.",
"permissions": [
"storage",
+1 -1
View File
@@ -328,7 +328,7 @@
</div>
<label>Video Debug Info</label>
<div id="videoDebug" class="info-card" style="font-size: 10px; font-family: monospace; color: var(--text-muted); min-height: 60px; line-height: 1.4;">
<div id="videoDebug" class="info-card" style="font-size: 10px; font-family: monospace; color: var(--text-muted); max-height: 250px; overflow-y: auto; line-height: 1.4;">
No tab selected or video detected.
</div>
+58 -25
View File
@@ -241,10 +241,17 @@ function updatePeerList(peers) {
peerItem.appendChild(header);
if (p.mediaTitle) {
const mediaDiv = document.createElement('div');
mediaDiv.style.cssText = 'font-size:11px; color:var(--star); font-weight: 600; margin-top: 2px;';
mediaDiv.textContent = `🎬 ${p.mediaTitle}`;
peerItem.appendChild(mediaDiv);
}
if (pTabTitle) {
const titleDiv = document.createElement('div');
titleDiv.style.cssText = 'font-size:10px; color:var(--text-muted);';
titleDiv.textContent = pTabTitle;
titleDiv.style.cssText = 'font-size:10px; color:var(--text-muted); opacity: 0.8;';
titleDiv.textContent = p.mediaTitle ? `via ${pTabTitle}` : pTabTitle;
peerItem.appendChild(titleDiv);
}
@@ -806,31 +813,57 @@ function refreshDebugInfo() {
if (elements.videoDebug) {
elements.videoDebug.innerHTML = '';
const status = document.createElement('div');
status.style.cssText = 'color:var(--accent); margin-bottom:4px;';
status.textContent = `VIDEO STATE: ${state.paused ? 'PAUSED' : 'PLAYING'}`;
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);
};
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();
elements.videoDebug.appendChild(div);
};
addField('STATE', state.paused ? 'PAUSED' : 'PLAYING', 'var(--accent)');
addField('TIME', `${state.currentTime.toFixed(2)}s / ${state.duration.toFixed(2)}s`);
addField('READY', state.readyState);
const time = document.createElement('div');
time.style.fontSize = '11px';
time.textContent = `Time: ${state.currentTime.toFixed(2)}s / ${state.duration.toFixed(2)}s`;
addSection('Identification');
addField('URL', state.url);
addField('ID', state.id);
addField('CLASS', state.className);
const readyState = document.createElement('div');
readyState.style.fontSize = '11px';
readyState.textContent = `ReadyState: ${state.readyState}`;
const misc = document.createElement('div');
misc.style.fontSize = '11px';
misc.textContent = `Muted: ${state.muted} | PlaybackRate: ${state.playbackRate}`;
const url = document.createElement('div');
url.style.cssText = 'font-size:9px; margin-top:4px; opacity:0.7;';
url.textContent = `URL: ${state.url.substring(0, 40)}...`;
elements.videoDebug.appendChild(status);
elements.videoDebug.appendChild(time);
elements.videoDebug.appendChild(readyState);
elements.videoDebug.appendChild(misc);
elements.videoDebug.appendChild(url);
addSection('Media Source');
addField('CURRENT_SRC', state.currentSrc);
addField('SRC', state.src);
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');
}
if (state.dataAttributes && Object.keys(state.dataAttributes).length > 0) {
addSection('Data Attributes');
for (const [key, val] of Object.entries(state.dataAttributes)) {
addField(key.replace('data-', '').toUpperCase(), val);
}
}
}
});
});
+6 -4
View File
@@ -155,7 +155,7 @@ io.on('connection', (socket) => {
return;
}
if (!payload || typeof payload.roomId !== 'string') return;
const { roomId, password, peerId, username, tabTitle, protocolVersion } = payload;
const { roomId, password, peerId, username, tabTitle, mediaTitle, protocolVersion } = payload;
try {
// Protocol check
if (protocolVersion !== '1.0.0') {
@@ -244,12 +244,13 @@ io.on('connection', (socket) => {
peerId,
username: username || null,
tabTitle: tabTitle || null,
mediaTitle: mediaTitle || null,
lastSeen: Date.now()
});
socketToRoom.set(socket.id, { roomId, peerId });
peerToSocket.set(peerId, socket.id);
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, username: username || null, tabTitle: tabTitle || null, status: 'joined' });
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, username: username || null, tabTitle: tabTitle || null, mediaTitle: mediaTitle || null, status: 'joined' });
socket.emit(EVENTS.ROOM_DATA, {
roomId,
peers: Array.from(room.peers).map(sid => room.peerData.get(sid))
@@ -288,8 +289,9 @@ io.on('connection', (socket) => {
const existing = room.peerData.get(socket.id) || { peerId: mapping.peerId };
room.peerData.set(socket.id, {
...existing,
username: data.username || existing.username,
tabTitle: data.tabTitle || existing.tabTitle,
username: data.username !== undefined ? data.username : existing.username,
tabTitle: data.tabTitle !== undefined ? data.tabTitle : existing.tabTitle,
mediaTitle: data.mediaTitle !== undefined ? data.mediaTitle : existing.mediaTitle,
lastSeen: Date.now()
});