Remove speed control

This commit is contained in:
Peter Stockings
2026-03-02 21:28:24 +11:00
parent bec873a9c7
commit 2020b59259
4 changed files with 46 additions and 109 deletions

View File

@@ -10,8 +10,6 @@
let joinFile = null; // File object for the joiner
let ignoreSync = false; // flag to avoid feedback loops
let syncTimeout = null;
let driftInterval = null;
let serverState = { playing: false, position: 0, speed: 1 };
// Reconnection state
let reconnectAttempts = 0;
@@ -73,7 +71,6 @@
const volumeSlider = $("volume-slider");
const currentTimeEl = $("current-time");
const durationEl = $("duration");
const speedSelect = $("speed-select");
const fullscreenBtn = $("fullscreen-btn");
const userCountEl = $("user-count");
const usersList = $("users-list");
@@ -243,7 +240,6 @@
applySync(msg.state);
}
addSystemMessage("Reconnected");
startDriftCorrection();
flushMessageQueue();
break;
@@ -298,82 +294,22 @@
// Load local file into video player
const file = localFile || joinFile;
if (file) {
loadVideoSource(file);
if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl);
currentBlobUrl = URL.createObjectURL(file);
videoPlayer.src = currentBlobUrl;
}
// --- Handle false "ended" events (Chromium MKV blob bug) ---
// Chrome can't properly index MKV containers loaded via blob URLs.
// After ~30s it may jump to the end and fire "ended" even though
// the video isn't actually over. Recovery: reload the blob and seek.
let recoveryAttempts = 0;
const MAX_RECOVERY_ATTEMPTS = 5;
videoPlayer.addEventListener("ended", () => {
const duration = videoPlayer.duration || 0;
// Only attempt recovery if we have server state showing we're not near the end
if (duration > 0 && lastServerState.position < duration - 5 && recoveryAttempts < MAX_RECOVERY_ATTEMPTS) {
recoveryAttempts++;
console.log(`[MKV-RECOVERY] False ended detected. Server pos=${lastServerState.position.toFixed(1)}, duration=${duration.toFixed(1)}. Reloading source (attempt ${recoveryAttempts}/${MAX_RECOVERY_ATTEMPTS})`);
const targetPos = lastServerState.position;
const wasPlaying = lastServerState.playing;
const currentFile = localFile || joinFile;
if (currentFile) {
// Reload the video with a fresh blob URL
loadVideoSource(currentFile, targetPos, wasPlaying);
}
}
});
// Reset recovery counter when video plays successfully for a while
let recoveryResetTimer = null;
videoPlayer.addEventListener("timeupdate", () => {
if (recoveryAttempts > 0) {
clearTimeout(recoveryResetTimer);
recoveryResetTimer = setTimeout(() => {
recoveryAttempts = 0;
}, 10000); // Reset after 10s of successful playback
}
});
// --- Resync on tab focus (handles background tab throttling) ---
document.addEventListener("visibilitychange", () => {
if (!document.hidden && roomCode && ws && ws.readyState === WebSocket.OPEN) {
console.log("[SYNC] Tab became visible, requesting state resync");
send({ type: "request_state" });
}
});
// Start drift correction
startDriftCorrection();
}
// --- Video Source Loading ---
// --- State tracking ---
let currentBlobUrl = null;
let lastServerState = { playing: false, position: 0, speed: 1 };
function loadVideoSource(file, seekTo, shouldPlay) {
// Revoke old blob URL to free memory
if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl);
}
currentBlobUrl = URL.createObjectURL(file);
videoPlayer.src = currentBlobUrl;
if (seekTo !== undefined) {
videoPlayer.addEventListener("loadedmetadata", function onMeta() {
videoPlayer.removeEventListener("loadedmetadata", onMeta);
videoPlayer.currentTime = seekTo;
if (shouldPlay) {
videoPlayer.play().then(() => {
videoWrapper.classList.add("playing");
updatePlayPauseIcon();
}).catch(() => {});
}
});
}
}
let lastServerState = { playing: false, position: 0 };
// --- File Check Modal ---
let pendingRoomFileInfo = null;
@@ -459,12 +395,7 @@
// Track latest server state for recovery
if (data.position !== undefined) lastServerState.position = data.position;
if (data.playing !== undefined) lastServerState.playing = data.playing;
if (data.speed !== undefined) lastServerState.speed = data.speed;
if (data.speed !== undefined && videoPlayer.playbackRate !== data.speed) {
videoPlayer.playbackRate = data.speed;
speedSelect.value = String(data.speed);
}
if (data.position !== undefined) {
const diff = Math.abs(videoPlayer.currentTime - data.position);
@@ -510,14 +441,7 @@
}
}
function startDriftCorrection() {
if (driftInterval) clearInterval(driftInterval);
driftInterval = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
send({ type: "request_state" });
}
}, 5000);
}
// --- Get video duration from a file (used for file info) ---
function getVideoDuration(file) {
@@ -544,6 +468,17 @@
container.classList.remove("hidden");
}
function isMkvFile(file) {
return file.name.toLowerCase().endsWith(".mkv");
}
function showFormatWarning(container) {
const warning = document.createElement("div");
warning.className = "format-warning";
warning.innerHTML = `⚠️ <strong>MKV files may not play correctly</strong> in browsers. Convert to MP4 for best results:<br><code>ffmpeg -i file.mkv -c copy file.mp4</code>`;
container.parentNode.insertBefore(warning, container.nextSibling);
}
// ===== EVENT LISTENERS =====
// --- Lobby: Username ---
@@ -560,7 +495,11 @@
localFile = file;
const duration = await getVideoDuration(file);
localFile._duration = duration;
// Remove any previous format warning
const oldWarning = createFileInfo.parentNode.querySelector(".format-warning");
if (oldWarning) oldWarning.remove();
renderFileInfo(createFileInfo, file, duration);
if (isMkvFile(file)) showFormatWarning(createFileInfo);
createRoomBtn.disabled = !usernameInput.value.trim();
});
@@ -679,7 +618,6 @@
joinFile = null;
roomFileInfo = null;
messageQueue = [];
if (driftInterval) clearInterval(driftInterval);
videoPlayer.src = "";
chatMessages.innerHTML = '<div class="chat-welcome" id="chat-welcome"><p>Welcome to the room! 👋</p></div>';
createFileInfo.classList.add("hidden");
@@ -766,12 +704,6 @@
volOffIcon.classList.toggle("hidden", !videoPlayer.muted);
});
// Speed
speedSelect.addEventListener("change", () => {
const speed = parseFloat(speedSelect.value);
videoPlayer.playbackRate = speed;
send({ type: "sync", action: "speed", speed });
});
// Fullscreen
fullscreenBtn.addEventListener("click", () => {

View File

@@ -158,15 +158,6 @@
</span>
</div>
<div class="controls-right">
<select id="speed-select" class="speed-select" title="Playback speed">
<option value="0.25">0.25x</option>
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
<button id="fullscreen-btn" class="ctrl-btn" title="Fullscreen">
<svg viewBox="0 0 24 24" width="22" height="22" fill="white">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />

View File

@@ -282,6 +282,30 @@ select {
font-size: 0.75rem;
}
/* Format warning (MKV etc.) */
.format-warning {
width: 100%;
margin-top: 8px;
padding: 10px 14px;
background: rgba(243, 156, 18, 0.1);
border: 1px solid rgba(243, 156, 18, 0.3);
border-radius: var(--radius);
color: var(--warning);
font-size: 0.78rem;
line-height: 1.5;
}
.format-warning code {
display: inline-block;
margin-top: 4px;
padding: 2px 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
font-family: monospace;
font-size: 0.75rem;
color: var(--text-primary);
}
/* Buttons */
.btn-primary {
width: 100%;

View File

@@ -13,7 +13,6 @@ interface RoomState {
fileInfo: FileInfo;
playing: boolean;
position: number;
speed: number;
lastUpdate: number; // timestamp when position was last set
users: Map<string, WebSocket>;
chatHistory: ChatMessage[];
@@ -41,7 +40,7 @@ function generateRoomCode(): string {
function getCurrentPosition(room: RoomState): number {
if (!room.playing) return room.position;
const elapsed = (Date.now() - room.lastUpdate) / 1000;
return room.position + elapsed * room.speed;
return room.position + elapsed; // assume 1x speed
}
function broadcastToRoom(room: RoomState, message: object, excludeWs?: WebSocket) {
@@ -159,7 +158,6 @@ const server = Bun.serve<WSData>({
},
playing: false,
position: 0,
speed: 1,
lastUpdate: Date.now(),
users: new Map(),
chatHistory: [],
@@ -249,7 +247,6 @@ const server = Bun.serve<WSData>({
state: {
playing: room.playing,
position: getCurrentPosition(room),
speed: room.speed,
},
chatHistory: room.chatHistory.slice(-50),
})
@@ -285,10 +282,6 @@ const server = Bun.serve<WSData>({
} else if (msg.action === "seek") {
room.position = msg.position;
room.lastUpdate = Date.now();
} else if (msg.action === "speed") {
room.position = getCurrentPosition(room);
room.speed = msg.speed;
room.lastUpdate = Date.now();
}
// Broadcast to others
@@ -299,7 +292,6 @@ const server = Bun.serve<WSData>({
action: msg.action,
position: room.position,
playing: room.playing,
speed: room.speed,
username: ws.data.username,
timestamp: Date.now(),
},
@@ -343,7 +335,6 @@ const server = Bun.serve<WSData>({
state: {
playing: room.playing,
position: getCurrentPosition(room),
speed: room.speed,
},
})
);
@@ -388,7 +379,6 @@ const server = Bun.serve<WSData>({
state: {
playing: room.playing,
position: getCurrentPosition(room),
speed: room.speed,
},
chatHistory: room.chatHistory.slice(-50),
})