/* Plyr video player management */ console.log('[player.js] build 20260911-tactile-episode-color-scale'); const players = {}; // {camera_name: Plyr instance} let currentEpisodeId = null; let currentCameras = []; let _episodeMediaLoadController = null; // Every episode selection gets a new generation. Episode ids normally differ, // but reopening the same episode must also invalidate old async callbacks. let _playbackSessionToken = 0; let workspaceSources = []; let workspaceCameras = []; let currentHasSkeleton = false; let _defaultLayoutApplied = false; // balanced layout (videos + hand heatmaps) applied once // The review canvas always plays the raw camera video. Hand skeletons are // rendered by hand-overlay.js as an SVG layer on top of that video. async function pickVideoUrl(episodeId, camera, preferSkeleton = false) { return `/api/v1/video/${episodeId}/${camera}/preview-stream`; } function getMediaLoadSignal() { return _episodeMediaLoadController ? _episodeMediaLoadController.signal : null; } function getPlaybackSessionToken() { return _playbackSessionToken; } function isCurrentPlaybackSession(episodeId, token) { return currentEpisodeId === episodeId && (token == null || token === _playbackSessionToken); } function _destroyEpisodePlayers() { Object.values(players).forEach(player => { try { player.pause(); } catch (_) {} try { player.destroy(); } catch (_) {} }); Object.keys(players).forEach(key => delete players[key]); } function _cancelEpisodeMediaLoad(clearCanvas = true) { // Invalidate callbacks before aborting requests. Abort is not guaranteed // to prevent a response already resolved from reaching its .then(). _playbackSessionToken += 1; if (_episodeMediaLoadController) { try { _episodeMediaLoadController.abort(); } catch (_) {} _episodeMediaLoadController = null; } clearTimeout(_loadWatchdog); masterPlaying = false; try { pauseAll(); } catch (_) {} if (typeof stopHeatmapSync === 'function') stopHeatmapSync(); if (typeof cancelSegmentPlayback === 'function') cancelSegmentPlayback(); if (typeof disconnectAnnotationSocket === 'function') disconnectAnnotationSocket(); _destroyEpisodePlayers(); currentImageTiles = []; currentDepthPreviewTiles = []; currentHand3dTiles = []; currentSlamTiles = []; currentTactileTiles = []; workspaceGrid = null; workspaceMainRow = null; workspaceHandFooter = null; workspaceHandSlots = { left: null, right: null }; workspaceBottomExtra = null; if (typeof clearHandTiles === 'function') clearHandTiles(); if (typeof destroyAllHandOverlays === 'function') destroyAllHandOverlays(); if (clearCanvas) { const grid = document.getElementById('video-grid'); if (grid) { grid.innerHTML = '