fixes lol
This commit is contained in:
parent
a206a606f7
commit
07b8e12197
10 changed files with 211 additions and 57 deletions
|
|
@ -1109,11 +1109,67 @@ void WatchController::nextVideo(const HttpRequestPtr &req,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock the room state row to prevent concurrent modifications
|
// Lock the room state row and get current video info to check if locked
|
||||||
*trans << "SELECT current_video_id FROM watch_room_state WHERE realm_id = $1 FOR UPDATE"
|
*trans << "SELECT wrs.current_video_id, wp.is_locked, wp.youtube_video_id, wp.title, "
|
||||||
|
"wp.duration_seconds, wp.thumbnail_url "
|
||||||
|
"FROM watch_room_state wrs "
|
||||||
|
"LEFT JOIN watch_playlist wp ON wrs.current_video_id = wp.id "
|
||||||
|
"WHERE wrs.realm_id = $1 FOR UPDATE"
|
||||||
<< id
|
<< id
|
||||||
>> [callback, trans, id](const Result&) {
|
>> [callback, trans, id](const Result& currentResult) {
|
||||||
// Mark current video as played
|
// Check if current video is locked - if so, restart it instead of advancing
|
||||||
|
bool currentIsLocked = false;
|
||||||
|
int64_t currentVideoId = 0;
|
||||||
|
if (!currentResult.empty() && !currentResult[0]["current_video_id"].isNull()) {
|
||||||
|
currentVideoId = currentResult[0]["current_video_id"].as<int64_t>();
|
||||||
|
currentIsLocked = !currentResult[0]["is_locked"].isNull() &&
|
||||||
|
currentResult[0]["is_locked"].as<bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIsLocked && currentVideoId > 0) {
|
||||||
|
// Locked video - restart it instead of advancing
|
||||||
|
std::string youtubeVideoId = currentResult[0]["youtube_video_id"].as<std::string>();
|
||||||
|
std::string title = currentResult[0]["title"].as<std::string>();
|
||||||
|
int durationSeconds = currentResult[0]["duration_seconds"].as<int>();
|
||||||
|
std::string thumbnailUrl = currentResult[0]["thumbnail_url"].isNull() ? "" :
|
||||||
|
currentResult[0]["thumbnail_url"].as<std::string>();
|
||||||
|
|
||||||
|
// Reset time to 0 and update started_at
|
||||||
|
*trans << "UPDATE watch_playlist SET started_at = CURRENT_TIMESTAMP "
|
||||||
|
"WHERE id = $1"
|
||||||
|
<< currentVideoId
|
||||||
|
>> [callback, trans, id, currentVideoId, youtubeVideoId, title, durationSeconds, thumbnailUrl](const Result&) {
|
||||||
|
// Update room state to reset time
|
||||||
|
*trans << "UPDATE watch_room_state SET current_time_seconds = 0, "
|
||||||
|
"playback_state = 'playing', last_sync_at = CURRENT_TIMESTAMP "
|
||||||
|
"WHERE realm_id = $1"
|
||||||
|
<< id
|
||||||
|
>> [callback, currentVideoId, youtubeVideoId, title, durationSeconds, thumbnailUrl](const Result&) {
|
||||||
|
Json::Value resp;
|
||||||
|
resp["success"] = true;
|
||||||
|
resp["playbackState"] = "playing";
|
||||||
|
resp["currentTime"] = 0.0;
|
||||||
|
resp["serverTime"] = getCurrentTimestampMs();
|
||||||
|
resp["event"] = "locked_restart";
|
||||||
|
resp["leadIn"] = true;
|
||||||
|
|
||||||
|
Json::Value video;
|
||||||
|
video["id"] = static_cast<Json::Int64>(currentVideoId);
|
||||||
|
video["youtubeVideoId"] = youtubeVideoId;
|
||||||
|
video["title"] = title;
|
||||||
|
video["durationSeconds"] = durationSeconds;
|
||||||
|
video["thumbnailUrl"] = thumbnailUrl;
|
||||||
|
video["isLocked"] = true;
|
||||||
|
resp["currentVideo"] = video;
|
||||||
|
callback(jsonResp(resp));
|
||||||
|
}
|
||||||
|
>> DB_ERROR(callback, "update room state for locked restart");
|
||||||
|
}
|
||||||
|
>> DB_ERROR(callback, "update playlist for locked restart");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not locked - mark current video as played and advance to next
|
||||||
*trans << "UPDATE watch_playlist SET status = 'played' "
|
*trans << "UPDATE watch_playlist SET status = 'played' "
|
||||||
"WHERE realm_id = $1 AND status = 'playing'"
|
"WHERE realm_id = $1 AND status = 'playing'"
|
||||||
<< id
|
<< id
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <openssl/sha.h>
|
#include <openssl/sha.h>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
std::unordered_map<WebSocketConnectionPtr, ChatWebSocketController::ConnectionInfo>
|
std::unordered_map<WebSocketConnectionPtr, ChatWebSocketController::ConnectionInfo>
|
||||||
ChatWebSocketController::connections_;
|
ChatWebSocketController::connections_;
|
||||||
|
|
@ -28,6 +29,20 @@ std::mutex ChatWebSocketController::connectionsMutex_;
|
||||||
|
|
||||||
// Helper to broadcast participant joined event to realm
|
// Helper to broadcast participant joined event to realm
|
||||||
void ChatWebSocketController::broadcastParticipantJoined(const std::string& realmId, const ConnectionInfo& joinedUser) {
|
void ChatWebSocketController::broadcastParticipantJoined(const std::string& realmId, const ConnectionInfo& joinedUser) {
|
||||||
|
// Check if user already has another connection in this realm (multiple tabs)
|
||||||
|
// If so, don't broadcast - they're already shown as a participant
|
||||||
|
int userConnectionsInRealm = 0;
|
||||||
|
for (const auto& [conn, info] : connections_) {
|
||||||
|
if (info.realmId == realmId && info.userId == joinedUser.userId) {
|
||||||
|
userConnectionsInRealm++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only broadcast if this is the user's first connection to this realm
|
||||||
|
if (userConnectionsInRealm > 1) {
|
||||||
|
return; // User already present, don't broadcast duplicate join
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value broadcast;
|
Json::Value broadcast;
|
||||||
broadcast["type"] = "participant_joined";
|
broadcast["type"] = "participant_joined";
|
||||||
broadcast["realmId"] = realmId;
|
broadcast["realmId"] = realmId;
|
||||||
|
|
@ -44,12 +59,14 @@ void ChatWebSocketController::broadcastParticipantJoined(const std::string& real
|
||||||
participant["joinedAt"] = static_cast<Json::Int64>(joinedAtMs);
|
participant["joinedAt"] = static_cast<Json::Int64>(joinedAtMs);
|
||||||
broadcast["participant"] = participant;
|
broadcast["participant"] = participant;
|
||||||
|
|
||||||
// Count participants in realm
|
// Count unique participants in realm (not connections)
|
||||||
int count = 0;
|
std::unordered_set<std::string> uniqueUserIds;
|
||||||
for (const auto& [conn, info] : connections_) {
|
for (const auto& [conn, info] : connections_) {
|
||||||
if (info.realmId == realmId) count++;
|
if (info.realmId == realmId) {
|
||||||
|
uniqueUserIds.insert(info.userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
broadcast["participantCount"] = count;
|
broadcast["participantCount"] = static_cast<int>(uniqueUserIds.size());
|
||||||
|
|
||||||
std::string messageStr = Json::writeString(Json::StreamWriterBuilder(), broadcast);
|
std::string messageStr = Json::writeString(Json::StreamWriterBuilder(), broadcast);
|
||||||
|
|
||||||
|
|
@ -62,18 +79,34 @@ void ChatWebSocketController::broadcastParticipantJoined(const std::string& real
|
||||||
|
|
||||||
// Helper to broadcast participant left event to realm
|
// Helper to broadcast participant left event to realm
|
||||||
void ChatWebSocketController::broadcastParticipantLeft(const std::string& realmId, const std::string& userId, const std::string& username) {
|
void ChatWebSocketController::broadcastParticipantLeft(const std::string& realmId, const std::string& userId, const std::string& username) {
|
||||||
|
// Check if user still has other connections in this realm (multiple tabs)
|
||||||
|
// This function is called AFTER the connection is removed, so if count > 0, user is still present
|
||||||
|
int userConnectionsInRealm = 0;
|
||||||
|
for (const auto& [conn, info] : connections_) {
|
||||||
|
if (info.realmId == realmId && info.userId == userId) {
|
||||||
|
userConnectionsInRealm++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only broadcast if this was the user's last connection to this realm
|
||||||
|
if (userConnectionsInRealm > 0) {
|
||||||
|
return; // User still has other tabs open, don't broadcast leave
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value broadcast;
|
Json::Value broadcast;
|
||||||
broadcast["type"] = "participant_left";
|
broadcast["type"] = "participant_left";
|
||||||
broadcast["realmId"] = realmId;
|
broadcast["realmId"] = realmId;
|
||||||
broadcast["userId"] = userId;
|
broadcast["userId"] = userId;
|
||||||
broadcast["username"] = username;
|
broadcast["username"] = username;
|
||||||
|
|
||||||
// Count remaining participants in realm
|
// Count remaining unique participants in realm (not connections)
|
||||||
int count = 0;
|
std::unordered_set<std::string> uniqueUserIds;
|
||||||
for (const auto& [conn, info] : connections_) {
|
for (const auto& [conn, info] : connections_) {
|
||||||
if (info.realmId == realmId) count++;
|
if (info.realmId == realmId) {
|
||||||
|
uniqueUserIds.insert(info.userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
broadcast["participantCount"] = count;
|
broadcast["participantCount"] = static_cast<int>(uniqueUserIds.size());
|
||||||
|
|
||||||
std::string messageStr = Json::writeString(Json::StreamWriterBuilder(), broadcast);
|
std::string messageStr = Json::writeString(Json::StreamWriterBuilder(), broadcast);
|
||||||
|
|
||||||
|
|
@ -859,26 +892,43 @@ void ChatWebSocketController::handleGetParticipants(const WebSocketConnectionPtr
|
||||||
response["realmId"] = info.realmId;
|
response["realmId"] = info.realmId;
|
||||||
response["participants"] = Json::arrayValue;
|
response["participants"] = Json::arrayValue;
|
||||||
|
|
||||||
// Get all participants in the same realm
|
// Get all participants in the same realm, deduplicated by userId
|
||||||
|
// For users with multiple connections (multiple tabs), show only the earliest connection
|
||||||
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
||||||
|
std::unordered_map<std::string, const ConnectionInfo*> uniqueUsers;
|
||||||
|
|
||||||
for (const auto& [conn, connInfo] : connections_) {
|
for (const auto& [conn, connInfo] : connections_) {
|
||||||
if (connInfo.realmId == info.realmId) {
|
if (connInfo.realmId == info.realmId) {
|
||||||
Json::Value participant;
|
auto it = uniqueUsers.find(connInfo.userId);
|
||||||
participant["userId"] = connInfo.userId;
|
if (it == uniqueUsers.end()) {
|
||||||
participant["username"] = connInfo.username;
|
// First connection for this user
|
||||||
participant["userColor"] = connInfo.userColor;
|
uniqueUsers[connInfo.userId] = &connInfo;
|
||||||
participant["avatarUrl"] = connInfo.avatarUrl;
|
} else {
|
||||||
participant["isGuest"] = connInfo.isGuest;
|
// User already seen - keep the earlier connection (smaller joinedAt)
|
||||||
participant["isModerator"] = connInfo.isModerator;
|
if (connInfo.connectionTime < it->second->connectionTime) {
|
||||||
participant["isStreamer"] = connInfo.isStreamer;
|
uniqueUsers[connInfo.userId] = &connInfo;
|
||||||
// Include join timestamp for ordering (milliseconds since epoch)
|
}
|
||||||
auto joinedAtMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
}
|
||||||
connInfo.connectionTime.time_since_epoch()).count();
|
|
||||||
participant["joinedAt"] = static_cast<Json::Int64>(joinedAtMs);
|
|
||||||
response["participants"].append(participant);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build response from deduplicated users
|
||||||
|
for (const auto& [userId, connInfoPtr] : uniqueUsers) {
|
||||||
|
Json::Value participant;
|
||||||
|
participant["userId"] = connInfoPtr->userId;
|
||||||
|
participant["username"] = connInfoPtr->username;
|
||||||
|
participant["userColor"] = connInfoPtr->userColor;
|
||||||
|
participant["avatarUrl"] = connInfoPtr->avatarUrl;
|
||||||
|
participant["isGuest"] = connInfoPtr->isGuest;
|
||||||
|
participant["isModerator"] = connInfoPtr->isModerator;
|
||||||
|
participant["isStreamer"] = connInfoPtr->isStreamer;
|
||||||
|
// Include join timestamp for ordering (milliseconds since epoch)
|
||||||
|
auto joinedAtMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
connInfoPtr->connectionTime.time_since_epoch()).count();
|
||||||
|
participant["joinedAt"] = static_cast<Json::Int64>(joinedAtMs);
|
||||||
|
response["participants"].append(participant);
|
||||||
|
}
|
||||||
|
|
||||||
response["count"] = response["participants"].size();
|
response["count"] = response["participants"].size();
|
||||||
wsConnPtr->send(Json::writeString(Json::StreamWriterBuilder(), response));
|
wsConnPtr->send(Json::writeString(Json::StreamWriterBuilder(), response));
|
||||||
}
|
}
|
||||||
|
|
@ -1275,21 +1325,22 @@ void ChatWebSocketController::broadcastToUser(const std::string& userId, const J
|
||||||
|
|
||||||
Json::Value ChatWebSocketController::getRealmStats() {
|
Json::Value ChatWebSocketController::getRealmStats() {
|
||||||
Json::Value result = Json::arrayValue;
|
Json::Value result = Json::arrayValue;
|
||||||
std::map<std::string, int> realmCounts;
|
// Count unique users per realm (not connections)
|
||||||
|
std::map<std::string, std::unordered_set<std::string>> realmUsers;
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
||||||
for (const auto& [conn, info] : connections_) {
|
for (const auto& [conn, info] : connections_) {
|
||||||
if (!info.realmId.empty()) {
|
if (!info.realmId.empty()) {
|
||||||
realmCounts[info.realmId]++;
|
realmUsers[info.realmId].insert(info.userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [realmId, count] : realmCounts) {
|
for (const auto& [realmId, userIds] : realmUsers) {
|
||||||
Json::Value realm;
|
Json::Value realm;
|
||||||
realm["realmId"] = realmId;
|
realm["realmId"] = realmId;
|
||||||
realm["participantCount"] = count;
|
realm["participantCount"] = static_cast<int>(userIds.size());
|
||||||
result.append(realm);
|
result.append(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -199,10 +199,8 @@ button:disabled {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
background: var(--bg-elevated);
|
background: #000;
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-container {
|
.nav-container {
|
||||||
|
|
|
||||||
|
|
@ -200,10 +200,26 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get token from localStorage if available (for authenticated users)
|
// Connect to chat - fetch fresh token if authenticated (uses httpOnly cookies)
|
||||||
const token = typeof localStorage !== 'undefined' ? localStorage.getItem('token') : null;
|
(async () => {
|
||||||
console.log('Chat connecting with realmId:', realmId, token ? '(authenticated)' : '(guest)');
|
let token = null;
|
||||||
chatWebSocket.connect(realmId, token);
|
try {
|
||||||
|
const response = await fetch('/api/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.token) {
|
||||||
|
token = data.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not authenticated or refresh failed - connect as guest
|
||||||
|
}
|
||||||
|
console.log('Chat connecting with realmId:', realmId, token ? '(authenticated)' : '(guest)');
|
||||||
|
chatWebSocket.connect(realmId, token);
|
||||||
|
})();
|
||||||
|
|
||||||
// Function to scroll to newest messages (bottom for UP flow, top for DOWN flow)
|
// Function to scroll to newest messages (bottom for UP flow, top for DOWN flow)
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,22 @@
|
||||||
|
|
||||||
// Auto-connect to global chat on mount (like chat panel)
|
// Auto-connect to global chat on mount (like chat panel)
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const token = typeof localStorage !== 'undefined' ? localStorage.getItem('token') : null;
|
// Fetch fresh token if authenticated (uses httpOnly cookies)
|
||||||
|
let token = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.token) {
|
||||||
|
token = data.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not authenticated or refresh failed - connect as guest
|
||||||
|
}
|
||||||
|
|
||||||
// If already connected, just use that connection
|
// If already connected, just use that connection
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
|
|
|
||||||
|
|
@ -360,8 +360,22 @@ async function joinRealmChat(nameOrId, addSystemMessage, chatWebSocket, setRealm
|
||||||
// Add realm to filter
|
// Add realm to filter
|
||||||
joinRealmFilter(targetRealmId);
|
joinRealmFilter(targetRealmId);
|
||||||
|
|
||||||
// Get token for authenticated connection
|
// Fetch fresh token if authenticated (uses httpOnly cookies)
|
||||||
const token = typeof localStorage !== 'undefined' ? localStorage.getItem('token') : null;
|
let token = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.token) {
|
||||||
|
token = data.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not authenticated or refresh failed - connect as guest
|
||||||
|
}
|
||||||
|
|
||||||
// Connect to the realm's WebSocket
|
// Connect to the realm's WebSocket
|
||||||
await chatWebSocket.connect(targetRealmId, token);
|
await chatWebSocket.connect(targetRealmId, token);
|
||||||
|
|
|
||||||
|
|
@ -314,7 +314,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
background: #000;
|
background: #000;
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ function createWatchSyncStore() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect(realmId, token = null) {
|
async function connect(realmId, token = null) {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
if (ws?.readyState === WebSocket.OPEN && currentRealmId === realmId) return;
|
if (ws?.readyState === WebSocket.OPEN && currentRealmId === realmId) return;
|
||||||
|
|
||||||
|
|
@ -208,8 +208,24 @@ function createWatchSyncStore() {
|
||||||
currentRealmId = realmId;
|
currentRealmId = realmId;
|
||||||
update(state => ({ ...state, loading: true, error: null }));
|
update(state => ({ ...state, loading: true, error: null }));
|
||||||
|
|
||||||
// Get token from localStorage if not provided
|
// Fetch fresh token if not provided (uses httpOnly cookies)
|
||||||
const authToken = token || localStorage.getItem('token');
|
let authToken = token;
|
||||||
|
if (!authToken) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.token) {
|
||||||
|
authToken = data.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not authenticated or refresh failed - connect as guest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build WebSocket URL
|
// Build WebSocket URL
|
||||||
let wsUrl = `${WS_URL.replace('/ws', '')}/watch/ws?realmId=${encodeURIComponent(realmId)}`;
|
let wsUrl = `${WS_URL.replace('/ws', '')}/watch/ws?realmId=${encodeURIComponent(realmId)}`;
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,6 @@
|
||||||
.nav {
|
.nav {
|
||||||
background: #000;
|
background: #000;
|
||||||
padding: var(--nav-padding-y) 0;
|
padding: var(--nav-padding-y) 0;
|
||||||
margin-bottom: var(--nav-margin-bottom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-container {
|
.nav-container {
|
||||||
|
|
|
||||||
|
|
@ -191,18 +191,8 @@
|
||||||
// Prevent duplicate skip calls
|
// Prevent duplicate skip calls
|
||||||
if (skipInProgress) return;
|
if (skipInProgress) return;
|
||||||
|
|
||||||
// Check if current video is locked (looped) - if so, let the server handle the restart
|
// When a video ends, call skip to advance to next (or restart if locked)
|
||||||
// The server will send a 'locked_restart' event to loop the video
|
// The server handles locked videos by restarting them instead of advancing
|
||||||
const currentVid = $currentVideo;
|
|
||||||
if (currentVid?.isLocked) {
|
|
||||||
// Locked video - request sync to get the restart state from server
|
|
||||||
setTimeout(() => {
|
|
||||||
watchSync.requestSync();
|
|
||||||
}, 500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When a video ends, skip to the next one (only for non-locked videos)
|
|
||||||
if ($canControl) {
|
if ($canControl) {
|
||||||
skipInProgress = true;
|
skipInProgress = true;
|
||||||
watchSync.skip();
|
watchSync.skip();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue