This commit is contained in:
parent
a9e3cf2ea5
commit
48f62c8c02
6 changed files with 107 additions and 5 deletions
|
|
@ -221,15 +221,21 @@ void WatchSyncController::broadcastRoomSync(const std::string& realmId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add 1 second buffer to account for timing variations
|
// Add 1 second buffer to account for timing variations
|
||||||
if (it->second.durationSeconds > 0 &&
|
// Also check client-reported video end as fallback when duration is unknown
|
||||||
expectedTime >= static_cast<double>(it->second.durationSeconds) + 1.0) {
|
bool durationBasedEnd = it->second.durationSeconds > 0 &&
|
||||||
|
expectedTime >= static_cast<double>(it->second.durationSeconds) + 1.0;
|
||||||
|
bool clientReportedEnd = it->second.videoEndedReported;
|
||||||
|
|
||||||
|
if (durationBasedEnd || clientReportedEnd) {
|
||||||
shouldAutoAdvance = true;
|
shouldAutoAdvance = true;
|
||||||
// Mark as ended to prevent multiple auto-advance calls
|
// Mark as ended to prevent multiple auto-advance calls
|
||||||
it->second.playbackState = "ended";
|
it->second.playbackState = "ended";
|
||||||
|
it->second.videoEndedReported = false; // Reset the flag
|
||||||
it->second.stateVersion++;
|
it->second.stateVersion++;
|
||||||
LOG_INFO << "Video ended in room " << realmId << ", auto-advancing"
|
LOG_INFO << "Video ended in room " << realmId << ", auto-advancing"
|
||||||
<< " (expectedTime=" << expectedTime
|
<< " (expectedTime=" << expectedTime
|
||||||
<< ", duration=" << it->second.durationSeconds << ")";
|
<< ", duration=" << it->second.durationSeconds
|
||||||
|
<< ", clientReported=" << clientReportedEnd << ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -799,6 +805,8 @@ void WatchSyncController::handleNewMessage(const WebSocketConnectionPtr& wsConnP
|
||||||
handleUpdateDuration(wsConnPtr, info, json);
|
handleUpdateDuration(wsConnPtr, info, json);
|
||||||
} else if (msgType == "lock_update") {
|
} else if (msgType == "lock_update") {
|
||||||
handleLockUpdate(wsConnPtr, info, json);
|
handleLockUpdate(wsConnPtr, info, json);
|
||||||
|
} else if (msgType == "video_ended") {
|
||||||
|
handleVideoEnded(wsConnPtr, info);
|
||||||
} else {
|
} else {
|
||||||
sendError(wsConnPtr, "Unknown message type: " + msgType);
|
sendError(wsConnPtr, "Unknown message type: " + msgType);
|
||||||
}
|
}
|
||||||
|
|
@ -1609,3 +1617,30 @@ void WatchSyncController::handleLockUpdate(const WebSocketConnectionPtr& wsConnP
|
||||||
|
|
||||||
broadcastToRoom(info.realmId, broadcast);
|
broadcastToRoom(info.realmId, broadcast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WatchSyncController::handleVideoEnded(const WebSocketConnectionPtr& wsConnPtr,
|
||||||
|
const ViewerInfo& info) {
|
||||||
|
if (info.realmId.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO << "Video ended reported by client in room " << info.realmId;
|
||||||
|
|
||||||
|
// Set the videoEndedReported flag in room state
|
||||||
|
// This is a fallback for when duration-based detection fails
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(roomStatesMutex_);
|
||||||
|
auto it = roomStates_.find(info.realmId);
|
||||||
|
if (it != roomStates_.end()) {
|
||||||
|
// Only set if we haven't already processed an end event recently
|
||||||
|
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||||
|
if (now - it->second.lastSkipMs >= SKIP_DEBOUNCE_MS) {
|
||||||
|
it->second.videoEndedReported = true;
|
||||||
|
LOG_INFO << "Set videoEndedReported=true for room " << info.realmId;
|
||||||
|
} else {
|
||||||
|
LOG_DEBUG << "Ignoring video_ended - skip was recent for room " << info.realmId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,9 @@ private:
|
||||||
|
|
||||||
// State freshness tracking
|
// State freshness tracking
|
||||||
int64_t lastDbSyncMs = 0; // Last time state was synced from database
|
int64_t lastDbSyncMs = 0; // Last time state was synced from database
|
||||||
|
|
||||||
|
// Client-reported video end (fallback when duration is unknown)
|
||||||
|
bool videoEndedReported = false; // True when client reports video ended
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::unordered_map<WebSocketConnectionPtr, ViewerInfo> viewers_;
|
static std::unordered_map<WebSocketConnectionPtr, ViewerInfo> viewers_;
|
||||||
|
|
@ -128,6 +131,9 @@ private:
|
||||||
const ViewerInfo& info,
|
const ViewerInfo& info,
|
||||||
const Json::Value& data);
|
const Json::Value& data);
|
||||||
|
|
||||||
|
void handleVideoEnded(const WebSocketConnectionPtr& wsConnPtr,
|
||||||
|
const ViewerInfo& info);
|
||||||
|
|
||||||
void sendError(const WebSocketConnectionPtr& wsConnPtr, const std::string& error);
|
void sendError(const WebSocketConnectionPtr& wsConnPtr, const std::string& error);
|
||||||
void sendSuccess(const WebSocketConnectionPtr& wsConnPtr, const Json::Value& data);
|
void sendSuccess(const WebSocketConnectionPtr& wsConnPtr, const Json::Value& data);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,18 @@
|
||||||
playerReady = true;
|
playerReady = true;
|
||||||
dispatch('ready');
|
dispatch('ready');
|
||||||
|
|
||||||
|
// Try to report duration early if available (some videos have it on ready)
|
||||||
|
const storeState = $watchSync;
|
||||||
|
const playlistItemId = storeState.currentVideo?.id;
|
||||||
|
if (playlistItemId && playlistItemId !== durationReportedForItemId) {
|
||||||
|
const playerDuration = player.getDuration();
|
||||||
|
if (playerDuration > 0) {
|
||||||
|
console.log(`Reporting duration on ready for playlist item ${playlistItemId}: ${Math.floor(playerDuration)}s`);
|
||||||
|
watchSync.reportDuration(playlistItemId, playerDuration);
|
||||||
|
durationReportedForItemId = playlistItemId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start sync check interval
|
// Start sync check interval
|
||||||
if (syncCheckInterval) clearInterval(syncCheckInterval);
|
if (syncCheckInterval) clearInterval(syncCheckInterval);
|
||||||
syncCheckInterval = setInterval(checkAndSync, SYNC_CHECK_INTERVAL);
|
syncCheckInterval = setInterval(checkAndSync, SYNC_CHECK_INTERVAL);
|
||||||
|
|
@ -111,6 +123,8 @@
|
||||||
// Always handle ENDED state - crucial for playlist advancement
|
// Always handle ENDED state - crucial for playlist advancement
|
||||||
if (state === window.YT.PlayerState.ENDED) {
|
if (state === window.YT.PlayerState.ENDED) {
|
||||||
dispatch('ended');
|
dispatch('ended');
|
||||||
|
// Also notify server directly - this is a fallback for when duration-based detection fails
|
||||||
|
watchSync.videoEnded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -348,6 +348,11 @@ function createWatchSyncStore() {
|
||||||
send({ type: 'skip' });
|
send({ type: 'skip' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify server that video has ended (fallback for when duration is unknown)
|
||||||
|
function videoEnded() {
|
||||||
|
send({ type: 'video_ended' });
|
||||||
|
}
|
||||||
|
|
||||||
// Report video duration to server (called when YouTube player loads a video)
|
// Report video duration to server (called when YouTube player loads a video)
|
||||||
function reportDuration(playlistItemId, duration) {
|
function reportDuration(playlistItemId, duration) {
|
||||||
if (!playlistItemId || !duration || duration <= 0) return;
|
if (!playlistItemId || !duration || duration <= 0) return;
|
||||||
|
|
@ -389,6 +394,7 @@ function createWatchSyncStore() {
|
||||||
pause,
|
pause,
|
||||||
seek,
|
seek,
|
||||||
skip,
|
skip,
|
||||||
|
videoEnded,
|
||||||
requestSync,
|
requestSync,
|
||||||
checkSync,
|
checkSync,
|
||||||
reportDuration,
|
reportDuration,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { auth, userColor } from '$lib/stores/auth';
|
import { auth, userColor } from '$lib/stores/auth';
|
||||||
import { siteSettings } from '$lib/stores/siteSettings';
|
import { siteSettings } from '$lib/stores/siteSettings';
|
||||||
import { watchSync, viewerCount, canControl, canAddToPlaylist, currentVideo } from '$lib/stores/watchSync';
|
import { watchSync, viewerCount, canControl, canAddToPlaylist, currentVideo, watchConnected } from '$lib/stores/watchSync';
|
||||||
import { chatLayout } from '$lib/stores/chatLayout';
|
import { chatLayout } from '$lib/stores/chatLayout';
|
||||||
import ChatPanel from '$lib/components/chat/ChatPanel.svelte';
|
import ChatPanel from '$lib/components/chat/ChatPanel.svelte';
|
||||||
import YouTubePlayer from '$lib/components/watch/YouTubePlayer.svelte';
|
import YouTubePlayer from '$lib/components/watch/YouTubePlayer.svelte';
|
||||||
|
|
@ -191,10 +191,22 @@
|
||||||
// Prevent duplicate skip calls
|
// Prevent duplicate skip calls
|
||||||
if (skipInProgress) return;
|
if (skipInProgress) return;
|
||||||
|
|
||||||
|
console.log('handleVideoEnded called, canControl:', $canControl, 'connected:', $watchConnected);
|
||||||
|
|
||||||
// When a video ends, call skip to advance to next (or restart if locked)
|
// When a video ends, call skip to advance to next (or restart if locked)
|
||||||
// The server handles locked videos by restarting them instead of advancing
|
// The server handles locked videos by restarting them instead of advancing
|
||||||
|
// Note: YouTubePlayer also calls watchSync.videoEnded() directly as a fallback
|
||||||
if ($canControl) {
|
if ($canControl) {
|
||||||
|
// Check if WebSocket is connected
|
||||||
|
if (!$watchConnected) {
|
||||||
|
console.warn('WebSocket disconnected, cannot send skip');
|
||||||
|
// Try to refresh playlist anyway
|
||||||
|
loadPlaylist();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
skipInProgress = true;
|
skipInProgress = true;
|
||||||
|
console.log('Sending skip command to server');
|
||||||
watchSync.skip();
|
watchSync.skip();
|
||||||
// Reset flag after a delay and refresh playlist
|
// Reset flag after a delay and refresh playlist
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -203,7 +215,7 @@
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
// Non-controllers just request sync to get the updated state
|
// Non-controllers just request sync to get the updated state
|
||||||
// (the server auto-advances after a short delay or owner skips)
|
// (the server auto-advances via the videoEnded fallback)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
watchSync.requestSync();
|
watchSync.requestSync();
|
||||||
loadPlaylist();
|
loadPlaylist();
|
||||||
|
|
|
||||||
|
|
@ -760,6 +760,35 @@ http {
|
||||||
add_header Cache-Control "public, max-age=300" always;
|
add_header Cache-Control "public, max-age=300" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Public watch room endpoints - guests can view playlist and add videos if allowed by settings
|
||||||
|
# Must be before the catch-all /api/ block to avoid JWT validation
|
||||||
|
location ~ ^/api/watch/[0-9]+/(playlist|state)$ {
|
||||||
|
limit_req zone=api_limit burst=20 nodelay;
|
||||||
|
|
||||||
|
# CORS headers
|
||||||
|
add_header Access-Control-Allow-Origin $cors_origin always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "Content-Type" always;
|
||||||
|
add_header Access-Control-Allow-Credentials "true" always;
|
||||||
|
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header Content-Length 0;
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Cookie $http_cookie;
|
||||||
|
|
||||||
|
# Don't cache API responses
|
||||||
|
expires -1;
|
||||||
|
add_header Cache-Control "no-store, no-cache" always;
|
||||||
|
}
|
||||||
|
|
||||||
# Other API endpoints (authenticated)
|
# Other API endpoints (authenticated)
|
||||||
location /api/ {
|
location /api/ {
|
||||||
limit_req zone=api_limit burst=20 nodelay;
|
limit_req zone=api_limit burst=20 nodelay;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue