From 5f86df62cff72438565ccd62befd8defa5acb3d2 Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 10 Jun 2026 19:54:37 +0200 Subject: [PATCH] Use explicit YouTube play pause actions --- kkkmeet-server.el | 10 +++++-- public/app.js | 68 ++++++++++++++++++++++++++++++++++++----------- server.js | 7 +++-- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/kkkmeet-server.el b/kkkmeet-server.el index d984ec1..3e5f839 100644 --- a/kkkmeet-server.el +++ b/kkkmeet-server.el @@ -316,6 +316,7 @@ When KEEP-OPEN is nil, close PROCESS after writing." (from (alist-get 'from payload)) (type (alist-get 'type payload)) (video-id (format "%s" (or (alist-get 'videoId payload) ""))) + (action (format "%s" (or (alist-get 'action payload) ""))) (playlist (kkkmeet-server--normalize-playlist (alist-get 'playlist payload))) (safe-room-id (if (string-empty-p room-id) kkkmeet-server--default-room-id @@ -324,9 +325,13 @@ When KEEP-OPEN is nil, close PROCESS after writing." (let* ((active-video-id (if (seq-some (lambda (item) (equal video-id (alist-get 'videoId item))) - (append playlist nil)) + (append playlist nil)) video-id "")) + (safe-action (if (and (equal action "play") + (not (string-empty-p active-video-id))) + "play" + "pause")) (watch-state `((type . "watch-state") (from . ,from) (name . ,(kkkmeet-server--normalize-name @@ -334,7 +339,8 @@ When KEEP-OPEN is nil, close PROCESS after writing." (videoId . ,active-video-id) (playlist . ,playlist) (position . ,(or (alist-get 'position payload) 0)) - (playing . ,(if (alist-get 'playing payload) t :false)) + (playing . ,(if (equal safe-action "play") t :false)) + (action . ,safe-action) (updatedAt . ,(or (alist-get 'updatedAt payload) (floor (* 1000 (float-time)))))))) (puthash safe-room-id watch-state kkkmeet-server--watch-states) diff --git a/public/app.js b/public/app.js index fa014e7..d047648 100644 --- a/public/app.js +++ b/public/app.js @@ -44,6 +44,8 @@ let youtubePlayer; let youtubeReady = false; let currentYoutubeVideoId = ""; let applyingWatchStateUntil = 0; +let pauseEnforcementUntil = 0; +let desiredYoutubePlaying = false; let watchPlaylist = []; const peers = new Map(); @@ -297,9 +299,15 @@ async function ensureYoutubePlayer() { return youtubePlayer; } -async function sendWatchState({ videoId = currentYoutubeVideoId, position = 0, playing = false }) { +async function sendWatchState({ + videoId = currentYoutubeVideoId, + position = 0, + playing = false, + action = playing ? "play" : "pause" +}) { if (!peerId) return; + const watchAction = action === "play" ? "play" : "pause"; const playlist = playlistWithActivePosition(position); await fetch(`/watch/${encodeURIComponent(roomId)}`, { @@ -312,7 +320,8 @@ async function sendWatchState({ videoId = currentYoutubeVideoId, position = 0, p videoId, playlist, position, - playing, + playing: watchAction === "play" && Boolean(videoId), + action: watchAction, updatedAt: Date.now() }) }); @@ -326,7 +335,8 @@ function currentWatchState(playing) { videoId: currentYoutubeVideoId, playlist: watchPlaylist, position: snapshot.position, - playing + playing, + action: playing ? "play" : "pause" }; } @@ -336,11 +346,25 @@ function handleYoutubeStateChange(event) { return; } - const state = currentWatchState(event.data === YT.PlayerState.PLAYING); + if ( + event.data === YT.PlayerState.PLAYING && + !desiredYoutubePlaying && + Date.now() < pauseEnforcementUntil + ) { + youtubePlayer?.pauseVideo?.(); + return; + } + + const action = event.data === YT.PlayerState.PLAYING ? "play" : "pause"; + desiredYoutubePlaying = action === "play"; + const state = currentWatchState(action === "play"); if (state) { + state.action = action; if (event.data === YT.PlayerState.ENDED) { state.position = 0; state.playing = false; + state.action = "pause"; + desiredYoutubePlaying = false; watchPlaylist = playlistWithActivePosition(0); state.playlist = watchPlaylist; renderPlaylist(); @@ -352,9 +376,11 @@ function handleYoutubeStateChange(event) { } } -async function applyWatchState(state) { +async function applyWatchState(state, { suppressEvents = true } = {}) { + const shouldPlay = state?.action === "play" || (!state?.action && Boolean(state?.playing)); watchPlaylist = normalizedPlaylist(state?.playlist); currentYoutubeVideoId = state?.videoId || ""; + desiredYoutubePlaying = shouldPlay; if (currentYoutubeVideoId) { const position = Math.max(0, Number(state.position) || 0); watchPlaylist = watchPlaylist.map((item) => ( @@ -364,25 +390,31 @@ async function applyWatchState(state) { renderPlaylist(); if (!currentYoutubeVideoId) { + desiredYoutubePlaying = false; youtubePlayer?.stopVideo?.(); youtubeShell.classList.remove("is-active"); setWatchCollapsed(true); return; } - if (state.playing) { + if (shouldPlay) { setWatchCollapsed(false); } youtubeShell.classList.add("is-active"); const player = await ensureYoutubePlayer(); - applyingWatchStateUntil = Date.now() + 1200; + if (suppressEvents) { + applyingWatchStateUntil = Date.now() + 3000; + } + if (!shouldPlay) { + pauseEnforcementUntil = Math.max(pauseEnforcementUntil, Date.now() + 3000); + } - const elapsed = state.playing ? Math.max(0, (Date.now() - state.updatedAt) / 1000) : 0; + const elapsed = shouldPlay ? Math.max(0, (Date.now() - state.updatedAt) / 1000) : 0; const position = Math.max(0, Number(state.position || 0) + elapsed); if (youtubeReady) { - if (state.playing) { + if (shouldPlay) { const sameVideo = player.getVideoData?.().video_id === currentYoutubeVideoId; const playerState = player.getPlayerState?.(); if (sameVideo && playerState === YT.PlayerState.PLAYING) { @@ -405,12 +437,18 @@ async function applyWatchState(state) { setTimeout(() => { player.pauseVideo(); }, 150); + setTimeout(() => { + if (!desiredYoutubePlaying) player.pauseVideo(); + }, 1200); + setTimeout(() => { + if (!desiredYoutubePlaying) player.pauseVideo(); + }, 2500); } } else { const interval = setInterval(() => { if (!youtubeReady) return; clearInterval(interval); - applyWatchState(state).catch(console.error); + applyWatchState(state, { suppressEvents }).catch(console.error); }, 100); } } @@ -803,7 +841,7 @@ async function addYoutubeVideo(value) { position: savedPositionForVideo(videoId), playing: true, updatedAt: Date.now() - }); + }, { suppressEvents: false }); await sendWatchState({ videoId, position: savedPositionForVideo(videoId), playing: true }); } @@ -818,7 +856,7 @@ async function switchYoutubeVideo(videoId) { position, playing: true, updatedAt: Date.now() - }); + }, { suppressEvents: false }); await sendWatchState({ videoId, position, playing: true }); } @@ -835,7 +873,7 @@ async function deleteYoutubeVideo(videoId) { position, playing: Boolean(nextVideoId), updatedAt: Date.now() - }); + }, { suppressEvents: false }); await sendWatchState({ videoId: nextVideoId, position, playing: Boolean(nextVideoId) }); } @@ -847,8 +885,8 @@ async function clearYoutubePlaylist() { position: 0, playing: false, updatedAt: Date.now() - }); - await sendWatchState({ videoId: "", position: 0, playing: false }); + }, { suppressEvents: false }); + await sendWatchState({ videoId: "", position: 0, playing: false, action: "pause" }); } async function startScreenShare() { diff --git a/server.js b/server.js index bd1582d..c10558c 100644 --- a/server.js +++ b/server.js @@ -221,14 +221,17 @@ const server = http.createServer(async (req, res) => { const playlist = normalizePlaylist(payload.playlist); const videoId = String(payload.videoId || "").trim(); + const activeVideoId = playlist.some((item) => item.videoId === videoId) ? videoId : ""; + const action = payload.action === "play" && activeVideoId ? "play" : "pause"; const watchState = { type: "watch-state", from: payload.from, name: normalizeName(payload.name), - videoId: playlist.some((item) => item.videoId === videoId) ? videoId : "", + videoId: activeVideoId, playlist, position: Number(payload.position) || 0, - playing: Boolean(payload.playing), + playing: action === "play", + action, updatedAt: Number(payload.updatedAt) || Date.now() };