From b703aaaf28253b2d92d1fe3ccb9d0142f05c1fb2 Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 28 Jan 2025 14:36:04 -0500 Subject: [PATCH 01/23] commit --- .../components/ScenePlayer/ScenePlayer.tsx | 49 +++- .../src/components/ScenePlayer/markers.css | 42 ++++ ui/v2.5/src/components/ScenePlayer/markers.ts | 228 +++++++++++++----- 3 files changed, 241 insertions(+), 78 deletions(-) create mode 100644 ui/v2.5/src/components/ScenePlayer/markers.css diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 24453043bd8..d8839373628 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -45,6 +45,7 @@ import airplay from "@silvermine/videojs-airplay"; import chromecast from "@silvermine/videojs-chromecast"; import abLoopPlugin from "videojs-abloop"; import ScreenUtils from "src/utils/screen"; +import { IMarker } from "./markers"; // register videojs plugins airplay(videojs); @@ -697,20 +698,46 @@ export const ScenePlayer: React.FC = ({ const player = getPlayer(); if (!player) return; - const markers = player.markers(); - markers.clearMarkers(); - for (const marker of scene.scene_markers) { - markers.addMarker({ + // Ensure markers are added after player is fully ready and sources are loaded + player.on('loadedmetadata', () => { + const markerData = scene.scene_markers.map(marker => ({ title: getMarkerTitle(marker), - time: marker.seconds, + seconds: marker.seconds, + end_seconds: marker.end_seconds ?? null, + primaryTag: marker.primary_tag + })); + + const markers = player.markers(); + markers.clearMarkers(); + const uniqueTagNames = markerData.map(marker => marker.primaryTag.name).filter((value, index, self) => self.indexOf(value) === index); + markers.findColors(uniqueTagNames); + + const timestampMarkers: IMarker[] = []; + const rangeMarkers: IMarker[] = []; + for (const marker of markerData) { + if (marker.end_seconds === null) { + timestampMarkers.push(marker); + } else { + rangeMarkers.push(marker); + } + } + // Add markers in chunks to avoid blocking + const CHUNK_SIZE = 10; + for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) { + const chunk = timestampMarkers.slice(i, i + CHUNK_SIZE); + requestAnimationFrame(() => { + chunk.forEach(m => markers.addDotMarker(m)); + }); + } + + requestAnimationFrame(() => { + markers.addRangeMarkersNew(rangeMarkers); }); - } + }); - if (scene.paths.screenshot) { - player.poster(scene.paths.screenshot); - } else { - player.poster(""); - } + return () => { + player.off('loadedmetadata'); + }; }, [getPlayer, scene]); useEffect(() => { diff --git a/ui/v2.5/src/components/ScenePlayer/markers.css b/ui/v2.5/src/components/ScenePlayer/markers.css new file mode 100644 index 00000000000..5f8d75f54d1 --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/markers.css @@ -0,0 +1,42 @@ +.vjs-marker-dot { + position: absolute; + background-color: #10b981; + width: 8px; + height: 8px; + border-radius: 50%; + cursor: pointer; + z-index: 2; + transform: translate(-50%, -50%); + top: 50%; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + transition: transform 0.2s ease; +} + +.vjs-marker-dot:hover { + transform: translate(-50%, -50%) scale(1.2); +} + +.vjs-marker-range { + position: absolute; + background-color: rgba(0, 255, 255, 0.4); + height: 5px; +} + + +.contained-marker-range { + background-color: rgba(59, 130, 246, 0.4); + z-index: 0; + transform: translateY(-15px); +} + +.marker-time-label { + position: absolute; + font-size: 10px; + color: white; + background: rgba(0, 0, 0, 0.7); + padding: 2px 4px; + border-radius: 2px; + transform: translateY(-100%); + white-space: nowrap; + z-index: 2; +} diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index 97eb0ff31cd..fd464ac1f85 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -1,8 +1,12 @@ import videojs, { VideoJsPlayer } from "video.js"; +import "./markers.css"; +import { layer } from "@fortawesome/fontawesome-svg-core"; -interface IMarker { +export interface IMarker { title: string; - time: number; + seconds: number; + end_seconds?: number | null; + layer?: number; } interface IMarkersOptions { @@ -11,108 +15,194 @@ interface IMarkersOptions { class MarkersPlugin extends videojs.getPlugin("plugin") { private markers: IMarker[] = []; - private markerDivs: HTMLDivElement[] = []; + private markerDivs: { + dot?: HTMLDivElement; + range?: HTMLDivElement; + containedRanges?: HTMLDivElement[]; + }[] = []; private markerTooltip: HTMLElement | null = null; private defaultTooltip: HTMLElement | null = null; + private layers: IMarker[][] = []; + + private layerHeight: number = 6; + + private tagColors: { [tag: string]: string } = {}; + constructor(player: VideoJsPlayer, options?: IMarkersOptions) { super(player); - player.ready(() => { - // create marker tooltip const tooltip = videojs.dom.createEl("div") as HTMLElement; tooltip.className = "vjs-marker-tooltip"; tooltip.style.visibility = "hidden"; - const parent = player - .el() - .querySelector(".vjs-progress-holder .vjs-mouse-display"); + const parent = player.el().querySelector(".vjs-progress-holder .vjs-mouse-display"); if (parent) parent.appendChild(tooltip); this.markerTooltip = tooltip; - // save default tooltip - this.defaultTooltip = player - .el() - .querySelector( - ".vjs-progress-holder .vjs-mouse-display .vjs-time-tooltip" - ); - - options?.markers?.forEach(this.addMarker, this); - }); - - player.on("loadedmetadata", () => { - const seekBar = player.el().querySelector(".vjs-progress-holder"); - const duration = this.player.duration(); - - for (let i = 0; i < this.markers.length; i++) { - const marker = this.markers[i]; - const markerDiv = this.markerDivs[i]; - - if (duration) { - // marker is 6px wide - adjust by 3px to align to center not left side - markerDiv.style.left = `calc(${ - (marker.time / duration) * 100 - }% - 3px)`; - markerDiv.style.visibility = "visible"; - } - if (seekBar) seekBar.appendChild(markerDiv); - } + this.defaultTooltip = player.el().querySelector( + ".vjs-progress-holder .vjs-mouse-display .vjs-time-tooltip" + ); }); } - private showMarkerTooltip(title: string) { + private showMarkerTooltip(title: string, layer:number = 0) { if (!this.markerTooltip) return; - this.markerTooltip.innerText = title; this.markerTooltip.style.right = `${-this.markerTooltip.clientWidth / 2}px`; + this.markerTooltip.style.top = `-${this.layerHeight * layer + 50}px`; this.markerTooltip.style.visibility = "visible"; - - // hide default tooltip if (this.defaultTooltip) this.defaultTooltip.style.visibility = "hidden"; } private hideMarkerTooltip() { if (this.markerTooltip) this.markerTooltip.style.visibility = "hidden"; - - // show default tooltip if (this.defaultTooltip) this.defaultTooltip.style.visibility = "visible"; } - addMarker(marker: IMarker) { - const markerDiv = videojs.dom.createEl("div") as HTMLDivElement; - markerDiv.className = "vjs-marker"; + private formatTime(seconds: number): string { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + } + addDotMarker(marker: IMarker) { const duration = this.player.duration(); + const markerSet: { + dot?: HTMLDivElement; + range?: HTMLDivElement; + } = {}; + const seekBar = this.player.el().querySelector(".vjs-progress-holder"); + if (marker.end_seconds) { + throw new Error("Cannot add range marker with addDotMarker"); + } + markerSet.dot = videojs.dom.createEl("div") as HTMLDivElement + markerSet.dot.className = "vjs-marker-dot"; if (duration) { - // marker is 6px wide - adjust by 3px to align to center not left side - markerDiv.style.left = `calc(${(marker.time / duration) * 100}% - 3px)`; - markerDiv.style.visibility = "visible"; + markerSet.dot.style.left = `calc(${(marker.seconds / duration) * 100}% - 3px)`; } - // bind click event to seek to marker time - markerDiv.addEventListener("click", () => - this.player.currentTime(marker.time) - ); + // Add event listeners to dot + markerSet.dot.addEventListener("click", () => this.player.currentTime(marker.seconds)); + markerSet.dot.toggleAttribute("marker-tooltip-shown", true); - // show/hide tooltip on hover - markerDiv.addEventListener("mouseenter", () => { + markerSet.dot.addEventListener("mouseenter", () => { this.showMarkerTooltip(marker.title); - markerDiv.toggleAttribute("marker-tooltip-shown", true); + markerSet.dot?.toggleAttribute("marker-tooltip-shown", true); }); - markerDiv.addEventListener("mouseout", () => { + + markerSet.dot.addEventListener("mouseout", () => { this.hideMarkerTooltip(); - markerDiv.toggleAttribute("marker-tooltip-shown", false); + markerSet.dot?.toggleAttribute("marker-tooltip-shown", false); }); + if (seekBar) { + seekBar.appendChild(markerSet.dot); + } + } + + private renderRangeMarkers(markers: IMarker[], layer: number) { + const duration = this.player.duration(); const seekBar = this.player.el().querySelector(".vjs-progress-holder"); - if (seekBar) seekBar.appendChild(markerDiv); + if (!seekBar || !duration) return; + + markers.forEach(marker => { + this.renderRangeMarker(marker, layer, duration, seekBar); + }); + } + + private renderRangeMarker(marker: IMarker, layer: number, duration: number, seekBar: Element) { + if (!marker.end_seconds) return; + + const markerSet: { + dot?: HTMLDivElement; + range?: HTMLDivElement; + } = { + }; + const rangeDiv = videojs.dom.createEl("div") as HTMLDivElement; + rangeDiv.className = "vjs-marker-range"; + + const startPercent = (marker.seconds / duration) * 100; + const endPercent = (marker.end_seconds / duration) * 100; + const width = endPercent - startPercent; + + rangeDiv.style.left = `${startPercent}%`; + rangeDiv.style.width = `${width}%`; + rangeDiv.style.bottom = `${layer * this.layerHeight + 10}px`; // Adjust height based on layer + rangeDiv.style.display = 'none'; // Initially hidden + + markerSet.range = rangeDiv; + markerSet.range.style.display = 'block'; + markerSet.range.addEventListener("mouseenter", () => { + this.showMarkerTooltip(marker.title, layer); + markerSet.range?.toggleAttribute("marker-tooltip-shown", true); + }); + markerSet.range.addEventListener("mouseout", () => { + this.hideMarkerTooltip(); + markerSet.range?.toggleAttribute("marker-tooltip-shown", false); + }); + seekBar.appendChild(rangeDiv); this.markers.push(marker); - this.markerDivs.push(markerDiv); + this.markerDivs.push(markerSet); } - addMarkers(markers: IMarker[]) { - markers.forEach(this.addMarker, this); + addRangeMarkersNew(markers: IMarker[]) { + let remainingMarkers = [...markers]; + this.layers = []; + let layerNum = 0; + + while (remainingMarkers.length > 0) { + const mwis = this.findMWIS(remainingMarkers); + if (!mwis.length) break; + + this.layers.push(mwis); + console.log("Rendering layer", layerNum, mwis); + this.renderRangeMarkers(mwis, layerNum); + remainingMarkers = remainingMarkers.filter(marker => !mwis.includes(marker)); + layerNum++; + } + } + + private findMWIS(markers: IMarker[]): IMarker[] { + if (!markers.length) return []; + + // Sort markers by end time + markers = markers.slice().sort((a, b) => (a.end_seconds || 0) - (b.end_seconds || 0)); + const n = markers.length; + + // Compute p(j) for each marker + const p: number[] = new Array(n).fill(-1); + for (let j = 0; j < n; j++) { + for (let i = j - 1; i >= 0; i--) { + if ((markers[i].end_seconds || 0) <= markers[j].seconds) { + p[j] = i; + break; + } + } + } + + // Initialize M[j] + const M: number[] = new Array(n).fill(0); + for (let j = 0; j < n; j++) { + const include = (markers[j].end_seconds || 0) - markers[j].seconds + (M[p[j]] || 0); + const exclude = j > 0 ? M[j - 1] : 0; + M[j] = Math.max(include, exclude); + } + + // Reconstruct optimal solution + const findSolution = (j: number): IMarker[] => { + if (j < 0) return []; + const include = (markers[j].end_seconds || 0) - markers[j].seconds + (M[p[j]] || 0); + const exclude = j > 0 ? M[j - 1] : 0; + if (include >= exclude) { + return [...findSolution(p[j]), markers[j]]; + } else { + return findSolution(j - 1); + } + }; + + return findSolution(n - 1); } removeMarker(marker: IMarker) { @@ -120,12 +210,14 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { if (i === -1) return; this.markers.splice(i, 1); - - const div = this.markerDivs.splice(i, 1)[0]; - if (div.hasAttribute("marker-tooltip-shown")) { + const markerSet = this.markerDivs.splice(i, 1)[0]; + + if (markerSet.dot?.hasAttribute("marker-tooltip-shown")) { this.hideMarkerTooltip(); } - div.remove(); + + markerSet.dot?.remove(); + if (markerSet.range) markerSet.range.remove(); } removeMarkers(markers: IMarker[]) { @@ -135,12 +227,14 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { clearMarkers() { this.removeMarkers([...this.markers]); } + + findColors(tagNames: string[]){ + + } } -// Register the plugin with video.js. videojs.registerPlugin("markers", MarkersPlugin); -/* eslint-disable @typescript-eslint/naming-convention */ declare module "video.js" { interface VideoJsPlayer { markers: () => MarkersPlugin; @@ -150,4 +244,4 @@ declare module "video.js" { } } -export default MarkersPlugin; +export default MarkersPlugin; \ No newline at end of file From 484c39d8cda6c86f0a6b47f1813a34cddc084c54 Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 28 Jan 2025 15:21:02 -0500 Subject: [PATCH 02/23] marker ui improvements --- .../src/components/ScenePlayer/markers.css | 2 +- ui/v2.5/src/components/ScenePlayer/markers.ts | 178 ++++++++++++++++-- 2 files changed, 165 insertions(+), 15 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/markers.css b/ui/v2.5/src/components/ScenePlayer/markers.css index 5f8d75f54d1..9389dacd6de 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.css +++ b/ui/v2.5/src/components/ScenePlayer/markers.css @@ -19,7 +19,7 @@ .vjs-marker-range { position: absolute; background-color: rgba(0, 255, 255, 0.4); - height: 5px; + height: 8px; } diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index fd464ac1f85..6d51ac8993e 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -1,6 +1,5 @@ import videojs, { VideoJsPlayer } from "video.js"; import "./markers.css"; -import { layer } from "@fortawesome/fontawesome-svg-core"; export interface IMarker { title: string; @@ -25,7 +24,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { private layers: IMarker[][] = []; - private layerHeight: number = 6; + private layerHeight: number = 9; private tagColors: { [tag: string]: string } = {}; @@ -46,7 +45,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { }); } - private showMarkerTooltip(title: string, layer:number = 0) { + private showMarkerTooltip(title: string, layer: number = 0) { if (!this.markerTooltip) return; this.markerTooltip.innerText = title; this.markerTooltip.style.right = `${-this.markerTooltip.clientWidth / 2}px`; @@ -90,7 +89,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { this.showMarkerTooltip(marker.title); markerSet.dot?.toggleAttribute("marker-tooltip-shown", true); }); - + markerSet.dot.addEventListener("mouseout", () => { this.hideMarkerTooltip(); markerSet.dot?.toggleAttribute("marker-tooltip-shown", false); @@ -113,24 +112,28 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { private renderRangeMarker(marker: IMarker, layer: number, duration: number, seekBar: Element) { if (!marker.end_seconds) return; - + const markerSet: { dot?: HTMLDivElement; range?: HTMLDivElement; - } = { - }; + } = {}; const rangeDiv = videojs.dom.createEl("div") as HTMLDivElement; rangeDiv.className = "vjs-marker-range"; - + const startPercent = (marker.seconds / duration) * 100; const endPercent = (marker.end_seconds / duration) * 100; const width = endPercent - startPercent; - + rangeDiv.style.left = `${startPercent}%`; rangeDiv.style.width = `${width}%`; rangeDiv.style.bottom = `${layer * this.layerHeight + 10}px`; // Adjust height based on layer rangeDiv.style.display = 'none'; // Initially hidden + // Set background color based on tag (if available) + if (marker.title && this.tagColors[marker.title]) { + rangeDiv.style.backgroundColor = this.tagColors[marker.title]; + } + markerSet.range = rangeDiv; markerSet.range.style.display = 'block'; markerSet.range.addEventListener("mouseenter", () => { @@ -211,11 +214,11 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { this.markers.splice(i, 1); const markerSet = this.markerDivs.splice(i, 1)[0]; - + if (markerSet.dot?.hasAttribute("marker-tooltip-shown")) { this.hideMarkerTooltip(); } - + markerSet.dot?.remove(); if (markerSet.range) markerSet.range.remove(); } @@ -228,8 +231,155 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { this.removeMarkers([...this.markers]); } - findColors(tagNames: string[]){ - + // Implementing the findColors method + async findColors(tagNames: string[]) { + // Compute base hues for each tag + const baseHues: { [tag: string]: number } = {}; + for (const tag of tagNames) { + baseHues[tag] = await this.computeBaseHue(tag); + } + + // Adjust hues to avoid similar colors + const adjustedHues = this.adjustHues(baseHues); + + // Convert adjusted hues to colors and store in tagColors + for (const tag of tagNames) { + this.tagColors[tag] = this.hueToColor(adjustedHues[tag]); + } + } + + // Helper methods translated from Python + + // Compute base hue from tag name + private async computeBaseHue(tag: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(tag); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + const hashInt = BigInt(`0x${hashHex}`); + const baseHue = Number(hashInt % BigInt(360)); // Map to [0, 360) + return baseHue; + } + + // Calculate minimum acceptable hue difference based on number of tags + private calculateDeltaMin(N: number): number { + const maxDeltaNeeded = 30; + let scalingFactor: number; + + if (N <= 4) { + scalingFactor = 0.8; + } else if (N <= 10) { + scalingFactor = 0.6; + } else { + scalingFactor = 0.4; + } + + const deltaMin = Math.min((360 / N) * scalingFactor, maxDeltaNeeded); + return deltaMin; + } + + // Adjust hues to ensure minimum difference + private adjustHues(baseHues: { [tag: string]: number }): { [tag: string]: number } { + const adjustedHues: { [tag: string]: number } = {}; + const tags = Object.keys(baseHues); + const N = tags.length; + const deltaMin = this.calculateDeltaMin(N); + + // Sort the tags by base hue + const sortedTags = tags.sort((a, b) => baseHues[a] - baseHues[b]); + // Get sorted base hues + const baseHuesSorted = sortedTags.map(tag => baseHues[tag]); + + // Unwrap hues to handle circular nature + const unwrappedHues = [...baseHuesSorted]; + for (let i = 1; i < N; i++) { + if (unwrappedHues[i] <= unwrappedHues[i - 1]) { + unwrappedHues[i] += 360; // Unwrap by adding 360 degrees + } + } + + // Adjust hues to ensure minimum difference + for (let i = 1; i < N; i++) { + const requiredHue = unwrappedHues[i - 1] + deltaMin; + if (unwrappedHues[i] < requiredHue) { + unwrappedHues[i] = requiredHue; // Adjust hue minimally + } + } + + // Handle wrap-around difference + const endGap = (unwrappedHues[0] + 360) - unwrappedHues[N - 1]; + if (endGap < deltaMin) { + // Adjust first and last hues minimally to increase end gap + const adjustmentNeeded = (deltaMin - endGap) / 2; + // Adjust the first hue backward, ensure it doesn't go below other hues + unwrappedHues[0] = Math.max( + unwrappedHues[0] - adjustmentNeeded, + unwrappedHues[1] - 360 + deltaMin + ); + // Adjust the last hue forward + unwrappedHues[N - 1] += adjustmentNeeded; + } + + // Wrap adjusted hues back to [0, 360) + const adjustedHuesList = unwrappedHues.map(hue => hue % 360); + + // Map adjusted hues back to tags + for (let i = 0; i < N; i++) { + adjustedHues[sortedTags[i]] = adjustedHuesList[i]; + } + + return adjustedHues; + } + + // Convert hue to RGB color in hex format + private hueToColor(hue: number): string { + // Convert hue from degrees to [0, 1) + const hueNormalized = hue / 360.0; + const saturation = 0.65; + const value = 0.95; + const rgb = this.hsvToRgb(hueNormalized, saturation, value); + const alpha = 0.6; // Set the desired alpha value here + const rgbColor = `#${this.toHex(rgb[0])}${this.toHex(rgb[1])}${this.toHex(rgb[2])}${this.toHex(Math.round(alpha * 255))}`; + return rgbColor; + } + + // Convert HSV to RGB + private hsvToRgb(h: number, s: number, v: number): [number, number, number] { + const i = Math.floor(h * 6); + const f = h * 6 - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + + let r, g, b; + switch (i % 6) { + case 0: + r = v; g = t; b = p; break; + case 1: + r = q; g = v; b = p; break; + case 2: + r = p; g = v; b = t; break; + case 3: + r = p; g = q; b = v; break; + case 4: + r = t; g = p; b = v; break; + case 5: + r = v; g = p; b = q; break; + default: + r = v; g = t; b = p; break; + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255) + ]; + } + + // Convert a number to two-digit hex string + private toHex(value: number): string { + return value.toString(16).padStart(2, '0'); } } @@ -244,4 +394,4 @@ declare module "video.js" { } } -export default MarkersPlugin; \ No newline at end of file +export default MarkersPlugin; From 1c5a1833ad7a772ce6081a4a95d13b3d71ef4574 Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 28 Jan 2025 17:17:40 -0500 Subject: [PATCH 03/23] working marker ui implementation --- .../components/ScenePlayer/ScenePlayer.tsx | 2 +- .../src/components/ScenePlayer/markers.css | 24 +++----------- ui/v2.5/src/components/ScenePlayer/markers.ts | 32 ++++++++----------- 3 files changed, 19 insertions(+), 39 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index d8839373628..ba46afec946 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -731,7 +731,7 @@ export const ScenePlayer: React.FC = ({ } requestAnimationFrame(() => { - markers.addRangeMarkersNew(rangeMarkers); + markers.addRangeMarkers(rangeMarkers); }); }); diff --git a/ui/v2.5/src/components/ScenePlayer/markers.css b/ui/v2.5/src/components/ScenePlayer/markers.css index 9389dacd6de..d823d68b78f 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.css +++ b/ui/v2.5/src/components/ScenePlayer/markers.css @@ -18,25 +18,9 @@ .vjs-marker-range { position: absolute; - background-color: rgba(0, 255, 255, 0.4); + background-color: rgba(255, 255, 255, 0.4); height: 8px; -} - - -.contained-marker-range { - background-color: rgba(59, 130, 246, 0.4); - z-index: 0; - transform: translateY(-15px); -} - -.marker-time-label { - position: absolute; - font-size: 10px; - color: white; - background: rgba(0, 0, 0, 0.7); - padding: 2px 4px; border-radius: 2px; - transform: translateY(-100%); - white-space: nowrap; - z-index: 2; -} + transform: translateY(-10px); + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} \ No newline at end of file diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index 6d51ac8993e..d1cee26cebb 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -5,7 +5,6 @@ export interface IMarker { title: string; seconds: number; end_seconds?: number | null; - layer?: number; } interface IMarkersOptions { @@ -22,8 +21,6 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { private markerTooltip: HTMLElement | null = null; private defaultTooltip: HTMLElement | null = null; - private layers: IMarker[][] = []; - private layerHeight: number = 9; private tagColors: { [tag: string]: string } = {}; @@ -59,12 +56,6 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { if (this.defaultTooltip) this.defaultTooltip.style.visibility = "visible"; } - private formatTime(seconds: number): string { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = Math.floor(seconds % 60); - return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; - } - addDotMarker(marker: IMarker) { const duration = this.player.duration(); const markerSet: { @@ -85,6 +76,10 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { markerSet.dot.addEventListener("click", () => this.player.currentTime(marker.seconds)); markerSet.dot.toggleAttribute("marker-tooltip-shown", true); + // Set background color based on tag (if available) + if (marker.title && this.tagColors[marker.title]) { + markerSet.dot.style.backgroundColor = this.tagColors[marker.title]; + } markerSet.dot.addEventListener("mouseenter", () => { this.showMarkerTooltip(marker.title); markerSet.dot?.toggleAttribute("marker-tooltip-shown", true); @@ -126,7 +121,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { rangeDiv.style.left = `${startPercent}%`; rangeDiv.style.width = `${width}%`; - rangeDiv.style.bottom = `${layer * this.layerHeight + 10}px`; // Adjust height based on layer + rangeDiv.style.bottom = `${layer * this.layerHeight}px`; // Adjust height based on layer rangeDiv.style.display = 'none'; // Initially hidden // Set background color based on tag (if available) @@ -150,23 +145,23 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { this.markerDivs.push(markerSet); } - addRangeMarkersNew(markers: IMarker[]) { + addRangeMarkers(markers: IMarker[]) { let remainingMarkers = [...markers]; - this.layers = []; let layerNum = 0; while (remainingMarkers.length > 0) { + // Get the set of markers that currently have the highest total duration that don't overlap. We do this layer by layer to prioritize filling + // the lower layers when possible const mwis = this.findMWIS(remainingMarkers); if (!mwis.length) break; - this.layers.push(mwis); - console.log("Rendering layer", layerNum, mwis); this.renderRangeMarkers(mwis, layerNum); remainingMarkers = remainingMarkers.filter(marker => !mwis.includes(marker)); layerNum++; } } + // Use dynamic programming to find maximum weight independent set (ie the set of markers that have the highest total duration that don't overlap) private findMWIS(markers: IMarker[]): IMarker[] { if (!markers.length) return []; @@ -174,7 +169,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { markers = markers.slice().sort((a, b) => (a.end_seconds || 0) - (b.end_seconds || 0)); const n = markers.length; - // Compute p(j) for each marker + // Compute p(j) for each marker. This is the index of the marker that has the highest end time that doesn't overlap with marker j const p: number[] = new Array(n).fill(-1); for (let j = 0; j < n; j++) { for (let i = j - 1; i >= 0; i--) { @@ -184,8 +179,9 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { } } } - + // Initialize M[j] + // Compute M[j] for each marker. This is the maximum total duration of markers that don't overlap with marker j const M: number[] = new Array(n).fill(0); for (let j = 0; j < n; j++) { const include = (markers[j].end_seconds || 0) - markers[j].seconds + (M[p[j]] || 0); @@ -242,7 +238,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { // Adjust hues to avoid similar colors const adjustedHues = this.adjustHues(baseHues); - // Convert adjusted hues to colors and store in tagColors + // Convert adjusted hues to colors and store in tagColors dictionary for (const tag of tagNames) { this.tagColors[tag] = this.hueToColor(adjustedHues[tag]); } @@ -264,7 +260,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { // Calculate minimum acceptable hue difference based on number of tags private calculateDeltaMin(N: number): number { - const maxDeltaNeeded = 30; + const maxDeltaNeeded = 35; let scalingFactor: number; if (N <= 4) { From dd33dbecd621751bcb3683d5bad569a7ae27c48e Mon Sep 17 00:00:00 2001 From: skier233 Date: Wed, 29 Jan 2025 12:51:10 -0500 Subject: [PATCH 04/23] linter fixes --- ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx | 4 ++-- ui/v2.5/src/components/ScenePlayer/markers.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index ba46afec946..6f05709a741 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -17,7 +17,8 @@ import "./live"; import "./PlaylistButtons"; import "./source-selector"; import "./persist-volume"; -import "./markers"; +import MarkersPlugin, { type IMarker } from "./markers"; +void MarkersPlugin; import "./vtt-thumbnails"; import "./big-buttons"; import "./track-activity"; @@ -45,7 +46,6 @@ import airplay from "@silvermine/videojs-airplay"; import chromecast from "@silvermine/videojs-chromecast"; import abLoopPlugin from "videojs-abloop"; import ScreenUtils from "src/utils/screen"; -import { IMarker } from "./markers"; // register videojs plugins airplay(videojs); diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index d1cee26cebb..0d35cad85b5 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -25,7 +25,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { private tagColors: { [tag: string]: string } = {}; - constructor(player: VideoJsPlayer, options?: IMarkersOptions) { + constructor(player: VideoJsPlayer) { super(player); player.ready(() => { const tooltip = videojs.dom.createEl("div") as HTMLElement; @@ -381,6 +381,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { videojs.registerPlugin("markers", MarkersPlugin); +/* eslint-disable @typescript-eslint/naming-convention */ declare module "video.js" { interface VideoJsPlayer { markers: () => MarkersPlugin; From bb1eaaaade72621f031cce1acc96a07053d368b3 Mon Sep 17 00:00:00 2001 From: skier233 Date: Wed, 29 Jan 2025 12:57:56 -0500 Subject: [PATCH 05/23] format ui --- .../components/ScenePlayer/ScenePlayer.tsx | 16 +-- .../src/components/ScenePlayer/markers.css | 6 +- ui/v2.5/src/components/ScenePlayer/markers.ts | 114 ++++++++++++------ 3 files changed, 90 insertions(+), 46 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 6f05709a741..70537f00738 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -699,19 +699,21 @@ export const ScenePlayer: React.FC = ({ if (!player) return; // Ensure markers are added after player is fully ready and sources are loaded - player.on('loadedmetadata', () => { - const markerData = scene.scene_markers.map(marker => ({ + player.on("loadedmetadata", () => { + const markerData = scene.scene_markers.map((marker) => ({ title: getMarkerTitle(marker), seconds: marker.seconds, end_seconds: marker.end_seconds ?? null, - primaryTag: marker.primary_tag + primaryTag: marker.primary_tag, })); const markers = player.markers(); markers.clearMarkers(); - const uniqueTagNames = markerData.map(marker => marker.primaryTag.name).filter((value, index, self) => self.indexOf(value) === index); + const uniqueTagNames = markerData + .map((marker) => marker.primaryTag.name) + .filter((value, index, self) => self.indexOf(value) === index); markers.findColors(uniqueTagNames); - + const timestampMarkers: IMarker[] = []; const rangeMarkers: IMarker[] = []; for (const marker of markerData) { @@ -726,7 +728,7 @@ export const ScenePlayer: React.FC = ({ for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) { const chunk = timestampMarkers.slice(i, i + CHUNK_SIZE); requestAnimationFrame(() => { - chunk.forEach(m => markers.addDotMarker(m)); + chunk.forEach((m) => markers.addDotMarker(m)); }); } @@ -736,7 +738,7 @@ export const ScenePlayer: React.FC = ({ }); return () => { - player.off('loadedmetadata'); + player.off("loadedmetadata"); }; }, [getPlayer, scene]); diff --git a/ui/v2.5/src/components/ScenePlayer/markers.css b/ui/v2.5/src/components/ScenePlayer/markers.css index d823d68b78f..d8a9ba1bb53 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.css +++ b/ui/v2.5/src/components/ScenePlayer/markers.css @@ -8,7 +8,7 @@ z-index: 2; transform: translate(-50%, -50%); top: 50%; - box-shadow: 0 2px 4px rgba(0,0,0,0.2); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); transition: transform 0.2s ease; } @@ -22,5 +22,5 @@ height: 8px; border-radius: 2px; transform: translateY(-10px); - box-shadow: 0 4px 6px rgba(0,0,0,0.1); -} \ No newline at end of file + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index 0d35cad85b5..406a53e136d 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -32,13 +32,17 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { tooltip.className = "vjs-marker-tooltip"; tooltip.style.visibility = "hidden"; - const parent = player.el().querySelector(".vjs-progress-holder .vjs-mouse-display"); + const parent = player + .el() + .querySelector(".vjs-progress-holder .vjs-mouse-display"); if (parent) parent.appendChild(tooltip); this.markerTooltip = tooltip; - this.defaultTooltip = player.el().querySelector( - ".vjs-progress-holder .vjs-mouse-display .vjs-time-tooltip" - ); + this.defaultTooltip = player + .el() + .querySelector( + ".vjs-progress-holder .vjs-mouse-display .vjs-time-tooltip" + ); }); } @@ -66,14 +70,18 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { if (marker.end_seconds) { throw new Error("Cannot add range marker with addDotMarker"); } - markerSet.dot = videojs.dom.createEl("div") as HTMLDivElement + markerSet.dot = videojs.dom.createEl("div") as HTMLDivElement; markerSet.dot.className = "vjs-marker-dot"; if (duration) { - markerSet.dot.style.left = `calc(${(marker.seconds / duration) * 100}% - 3px)`; + markerSet.dot.style.left = `calc(${ + (marker.seconds / duration) * 100 + }% - 3px)`; } // Add event listeners to dot - markerSet.dot.addEventListener("click", () => this.player.currentTime(marker.seconds)); + markerSet.dot.addEventListener("click", () => + this.player.currentTime(marker.seconds) + ); markerSet.dot.toggleAttribute("marker-tooltip-shown", true); // Set background color based on tag (if available) @@ -100,12 +108,17 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { const seekBar = this.player.el().querySelector(".vjs-progress-holder"); if (!seekBar || !duration) return; - markers.forEach(marker => { + markers.forEach((marker) => { this.renderRangeMarker(marker, layer, duration, seekBar); }); } - private renderRangeMarker(marker: IMarker, layer: number, duration: number, seekBar: Element) { + private renderRangeMarker( + marker: IMarker, + layer: number, + duration: number, + seekBar: Element + ) { if (!marker.end_seconds) return; const markerSet: { @@ -122,7 +135,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { rangeDiv.style.left = `${startPercent}%`; rangeDiv.style.width = `${width}%`; rangeDiv.style.bottom = `${layer * this.layerHeight}px`; // Adjust height based on layer - rangeDiv.style.display = 'none'; // Initially hidden + rangeDiv.style.display = "none"; // Initially hidden // Set background color based on tag (if available) if (marker.title && this.tagColors[marker.title]) { @@ -130,7 +143,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { } markerSet.range = rangeDiv; - markerSet.range.style.display = 'block'; + markerSet.range.style.display = "block"; markerSet.range.addEventListener("mouseenter", () => { this.showMarkerTooltip(marker.title, layer); markerSet.range?.toggleAttribute("marker-tooltip-shown", true); @@ -156,7 +169,9 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { if (!mwis.length) break; this.renderRangeMarkers(mwis, layerNum); - remainingMarkers = remainingMarkers.filter(marker => !mwis.includes(marker)); + remainingMarkers = remainingMarkers.filter( + (marker) => !mwis.includes(marker) + ); layerNum++; } } @@ -166,7 +181,9 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { if (!markers.length) return []; // Sort markers by end time - markers = markers.slice().sort((a, b) => (a.end_seconds || 0) - (b.end_seconds || 0)); + markers = markers + .slice() + .sort((a, b) => (a.end_seconds || 0) - (b.end_seconds || 0)); const n = markers.length; // Compute p(j) for each marker. This is the index of the marker that has the highest end time that doesn't overlap with marker j @@ -179,12 +196,13 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { } } } - + // Initialize M[j] // Compute M[j] for each marker. This is the maximum total duration of markers that don't overlap with marker j const M: number[] = new Array(n).fill(0); for (let j = 0; j < n; j++) { - const include = (markers[j].end_seconds || 0) - markers[j].seconds + (M[p[j]] || 0); + const include = + (markers[j].end_seconds || 0) - markers[j].seconds + (M[p[j]] || 0); const exclude = j > 0 ? M[j - 1] : 0; M[j] = Math.max(include, exclude); } @@ -192,7 +210,8 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { // Reconstruct optimal solution const findSolution = (j: number): IMarker[] => { if (j < 0) return []; - const include = (markers[j].end_seconds || 0) - markers[j].seconds + (M[p[j]] || 0); + const include = + (markers[j].end_seconds || 0) - markers[j].seconds + (M[p[j]] || 0); const exclude = j > 0 ? M[j - 1] : 0; if (include >= exclude) { return [...findSolution(p[j]), markers[j]]; @@ -250,9 +269,11 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { private async computeBaseHue(tag: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(tag); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); const hashInt = BigInt(`0x${hashHex}`); const baseHue = Number(hashInt % BigInt(360)); // Map to [0, 360) return baseHue; @@ -276,7 +297,9 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { } // Adjust hues to ensure minimum difference - private adjustHues(baseHues: { [tag: string]: number }): { [tag: string]: number } { + private adjustHues(baseHues: { [tag: string]: number }): { + [tag: string]: number; + } { const adjustedHues: { [tag: string]: number } = {}; const tags = Object.keys(baseHues); const N = tags.length; @@ -285,7 +308,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { // Sort the tags by base hue const sortedTags = tags.sort((a, b) => baseHues[a] - baseHues[b]); // Get sorted base hues - const baseHuesSorted = sortedTags.map(tag => baseHues[tag]); + const baseHuesSorted = sortedTags.map((tag) => baseHues[tag]); // Unwrap hues to handle circular nature const unwrappedHues = [...baseHuesSorted]; @@ -304,7 +327,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { } // Handle wrap-around difference - const endGap = (unwrappedHues[0] + 360) - unwrappedHues[N - 1]; + const endGap = unwrappedHues[0] + 360 - unwrappedHues[N - 1]; if (endGap < deltaMin) { // Adjust first and last hues minimally to increase end gap const adjustmentNeeded = (deltaMin - endGap) / 2; @@ -318,7 +341,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { } // Wrap adjusted hues back to [0, 360) - const adjustedHuesList = unwrappedHues.map(hue => hue % 360); + const adjustedHuesList = unwrappedHues.map((hue) => hue % 360); // Map adjusted hues back to tags for (let i = 0; i < N; i++) { @@ -336,7 +359,9 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { const value = 0.95; const rgb = this.hsvToRgb(hueNormalized, saturation, value); const alpha = 0.6; // Set the desired alpha value here - const rgbColor = `#${this.toHex(rgb[0])}${this.toHex(rgb[1])}${this.toHex(rgb[2])}${this.toHex(Math.round(alpha * 255))}`; + const rgbColor = `#${this.toHex(rgb[0])}${this.toHex(rgb[1])}${this.toHex( + rgb[2] + )}${this.toHex(Math.round(alpha * 255))}`; return rgbColor; } @@ -351,31 +376,48 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { let r, g, b; switch (i % 6) { case 0: - r = v; g = t; b = p; break; + r = v; + g = t; + b = p; + break; case 1: - r = q; g = v; b = p; break; + r = q; + g = v; + b = p; + break; case 2: - r = p; g = v; b = t; break; + r = p; + g = v; + b = t; + break; case 3: - r = p; g = q; b = v; break; + r = p; + g = q; + b = v; + break; case 4: - r = t; g = p; b = v; break; + r = t; + g = p; + b = v; + break; case 5: - r = v; g = p; b = q; break; + r = v; + g = p; + b = q; + break; default: - r = v; g = t; b = p; break; + r = v; + g = t; + b = p; + break; } - return [ - Math.round(r * 255), - Math.round(g * 255), - Math.round(b * 255) - ]; + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } // Convert a number to two-digit hex string private toHex(value: number): string { - return value.toString(16).padStart(2, '0'); + return value.toString(16).padStart(2, "0"); } } From 6ed00cdca5e2bd6d449ca30d021dcf10ad26e575 Mon Sep 17 00:00:00 2001 From: skier233 Date: Thu, 30 Jan 2025 10:37:06 -0500 Subject: [PATCH 06/23] ensure markers.findcolors is awaited --- .../components/ScenePlayer/ScenePlayer.tsx | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 70537f00738..f40e03bab51 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -700,41 +700,50 @@ export const ScenePlayer: React.FC = ({ // Ensure markers are added after player is fully ready and sources are loaded player.on("loadedmetadata", () => { - const markerData = scene.scene_markers.map((marker) => ({ - title: getMarkerTitle(marker), - seconds: marker.seconds, - end_seconds: marker.end_seconds ?? null, - primaryTag: marker.primary_tag, - })); - - const markers = player.markers(); - markers.clearMarkers(); - const uniqueTagNames = markerData - .map((marker) => marker.primaryTag.name) - .filter((value, index, self) => self.indexOf(value) === index); - markers.findColors(uniqueTagNames); - - const timestampMarkers: IMarker[] = []; - const rangeMarkers: IMarker[] = []; - for (const marker of markerData) { - if (marker.end_seconds === null) { - timestampMarkers.push(marker); - } else { - rangeMarkers.push(marker); + const loadMarkers = async () => { + const markerData = scene.scene_markers.map((marker) => ({ + title: getMarkerTitle(marker), + seconds: marker.seconds, + end_seconds: marker.end_seconds ?? null, + primaryTag: marker.primary_tag, + })); + + const markers = player.markers(); + markers.clearMarkers(); + + const uniqueTagNames = markerData + .map((marker) => marker.primaryTag.name) + .filter((value, index, self) => self.indexOf(value) === index); + + // Wait for colors + await markers.findColors(uniqueTagNames); + + const timestampMarkers: IMarker[] = []; + const rangeMarkers: IMarker[] = []; + for (const marker of markerData) { + if (marker.end_seconds === null) { + timestampMarkers.push(marker); + } else { + rangeMarkers.push(marker); + } } - } - // Add markers in chunks to avoid blocking - const CHUNK_SIZE = 10; - for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) { - const chunk = timestampMarkers.slice(i, i + CHUNK_SIZE); + + // Add markers in chunks + const CHUNK_SIZE = 10; + for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) { + const chunk = timestampMarkers.slice(i, i + CHUNK_SIZE); + requestAnimationFrame(() => { + chunk.forEach((m) => markers.addDotMarker(m)); + }); + } + requestAnimationFrame(() => { - chunk.forEach((m) => markers.addDotMarker(m)); + markers.addRangeMarkers(rangeMarkers); }); - } + }; - requestAnimationFrame(() => { - markers.addRangeMarkers(rangeMarkers); - }); + // Call our async function + void loadMarkers(); }); return () => { From 6fdc027b84345d1b2d51f371500765565ca46f17 Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 4 Feb 2025 13:31:08 -0500 Subject: [PATCH 07/23] tweak hover behavior to be more fluid --- ui/v2.5/src/components/ScenePlayer/markers.css | 3 ++- ui/v2.5/src/components/ScenePlayer/markers.ts | 13 +++++++++++-- .../src/components/ScenePlayer/vtt-thumbnails.ts | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/markers.css b/ui/v2.5/src/components/ScenePlayer/markers.css index d8a9ba1bb53..398fc90c6cf 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.css +++ b/ui/v2.5/src/components/ScenePlayer/markers.css @@ -21,6 +21,7 @@ background-color: rgba(255, 255, 255, 0.4); height: 8px; border-radius: 2px; - transform: translateY(-10px); + transform: translateY(-28px); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: none; } diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index 406a53e136d..71f71b93fdf 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -66,7 +66,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { dot?: HTMLDivElement; range?: HTMLDivElement; } = {}; - const seekBar = this.player.el().querySelector(".vjs-progress-holder"); + const seekBar = this.player.el().querySelector(".vjs-progress-control"); if (marker.end_seconds) { throw new Error("Cannot add range marker with addDotMarker"); } @@ -105,7 +105,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { private renderRangeMarkers(markers: IMarker[], layer: number) { const duration = this.player.duration(); - const seekBar = this.player.el().querySelector(".vjs-progress-holder"); + const seekBar = this.player.el().querySelector(".vjs-progress-control"); if (!seekBar || !duration) return; markers.forEach((marker) => { @@ -144,6 +144,15 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { markerSet.range = rangeDiv; markerSet.range.style.display = "block"; + markerSet.range.addEventListener("pointermove", (e) => { + e.stopPropagation(); + }); + markerSet.range.addEventListener("pointerover", (e) => { + e.stopPropagation(); + }); + markerSet.range.addEventListener("pointerout", (e) => { + e.stopPropagation(); + }); markerSet.range.addEventListener("mouseenter", () => { this.showMarkerTooltip(marker.title, layer); markerSet.range?.toggleAttribute("marker-tooltip-shown", true); diff --git a/ui/v2.5/src/components/ScenePlayer/vtt-thumbnails.ts b/ui/v2.5/src/components/ScenePlayer/vtt-thumbnails.ts index 33495eec794..a9c8f9d9e2b 100644 --- a/ui/v2.5/src/components/ScenePlayer/vtt-thumbnails.ts +++ b/ui/v2.5/src/components/ScenePlayer/vtt-thumbnails.ts @@ -151,8 +151,8 @@ class VTTThumbnailsPlugin extends videojs.getPlugin("plugin") { this.player.$(".vjs-mouse-display")?.classList.add("vjs-hidden"); } - progressBar.addEventListener("pointerenter", this.onBarPointerEnter); - progressBar.addEventListener("pointerleave", this.onBarPointerLeave); + progressBar.addEventListener("pointerover", this.onBarPointerEnter); + progressBar.addEventListener("pointerout", this.onBarPointerLeave); } private onBarPointerEnter = () => { From 1e661b15089ac2206cd39e48cec3d75be8374cc9 Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 4 Feb 2025 15:11:33 -0500 Subject: [PATCH 08/23] ensure thumbnail display is always on top --- ui/v2.5/src/components/ScenePlayer/styles.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index b3ed8445eca..bb3b4b9a5cd 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -183,6 +183,7 @@ $sceneTabWidth: 450px; pointer-events: none; position: absolute; transition: opacity 0.2s; + z-index: 100; } .vjs-big-play-button, From 7f2b22f3b807bc3b7132f1025efc0f685f320b7c Mon Sep 17 00:00:00 2001 From: skier233 Date: Fri, 7 Feb 2025 09:45:05 -0500 Subject: [PATCH 09/23] swap to using crypto-js library for hashing --- ui/v2.5/package.json | 1 + ui/v2.5/src/components/ScenePlayer/markers.ts | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 210b750fe0c..1f49806fd92 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -39,6 +39,7 @@ "base64-blob": "^1.4.1", "bootstrap": "^4.6.2", "classnames": "^2.3.2", + "crypto-js": "^4.2.0", "event-target-polyfill": "^0.0.4", "flag-icons": "^6.6.6", "flexbin": "^0.2.0", diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index 71f71b93fdf..ea8e1e80c12 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -1,5 +1,6 @@ import videojs, { VideoJsPlayer } from "video.js"; import "./markers.css"; +import CryptoJS from "crypto-js"; export interface IMarker { title: string; @@ -276,13 +277,8 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { // Compute base hue from tag name private async computeBaseHue(tag: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(tag); - const hashBuffer = await crypto.subtle.digest("SHA-256", data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); + const hash = CryptoJS.SHA256(tag); + const hashHex = hash.toString(CryptoJS.enc.Hex); const hashInt = BigInt(`0x${hashHex}`); const baseHue = Number(hashInt % BigInt(360)); // Map to [0, 360) return baseHue; From 437af944074f3bb9fb9489e48273fca5165c7d2d Mon Sep 17 00:00:00 2001 From: skier233 Date: Fri, 7 Feb 2025 09:51:37 -0500 Subject: [PATCH 10/23] update yarn.lock --- ui/v2.5/yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index aa27a347444..a661203e0ad 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -3506,6 +3506,11 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +crypto-js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + css-functions-list@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.1.0.tgz#cf5b09f835ad91a00e5959bcfc627cd498e1321b" From e15bf44ff5eb2d2b031ae93e70ad8be89b856e45 Mon Sep 17 00:00:00 2001 From: skier233 Date: Fri, 7 Feb 2025 10:28:57 -0500 Subject: [PATCH 11/23] add dev dependency --- ui/v2.5/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 1f49806fd92..9a71f453fe1 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -90,6 +90,7 @@ "@graphql-codegen/typescript-operations": "^4.0.1", "@graphql-codegen/typescript-react-apollo": "^4.1.0", "@types/apollo-upload-client": "^18.0.0", + "@types/crypto-js": "^4.2.2", "@types/lodash-es": "^4.17.6", "@types/mousetrap": "^1.6.11", "@types/node": "^18.13.0", From 8c46b72a7b8fe9471f91cec778c6055ae10f69ab Mon Sep 17 00:00:00 2001 From: skier233 Date: Fri, 7 Feb 2025 10:35:09 -0500 Subject: [PATCH 12/23] update lockfile after adding dev dependency --- ui/v2.5/yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index a661203e0ad..a39a5335a05 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -2176,6 +2176,11 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== +"@types/crypto-js@^4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea" + integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ== + "@types/extract-files@*": version "13.0.1" resolved "https://registry.yarnpkg.com/@types/extract-files/-/extract-files-13.0.1.tgz#3ec057a3fa25f778245a76a17271d23b71ee31d7" From 25152a15d35013ba53c558b3c0b6e24211c04e7d Mon Sep 17 00:00:00 2001 From: skier233 Date: Fri, 7 Feb 2025 18:37:58 -0500 Subject: [PATCH 13/23] Add interface option to show range markers --- graphql/schema/types/config.graphql | 6 ++++++ internal/api/resolver_mutation_configure.go | 1 + internal/api/resolver_query_configuration.go | 2 ++ internal/manager/config/config.go | 7 +++++++ ui/v2.5/graphql/data/config.graphql | 1 + .../src/components/ScenePlayer/ScenePlayer.tsx | 16 ++++++++++++---- ui/v2.5/src/components/ScenePlayer/markers.ts | 4 +--- .../SettingsInterfacePanel.tsx | 6 ++++++ ui/v2.5/src/locales/en-GB.json | 1 + 9 files changed, 37 insertions(+), 7 deletions(-) diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 4d6d2080b59..5677082eb62 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -355,6 +355,9 @@ input ConfigInterfaceInput { "Show scene scrubber by default" showScrubber: Boolean + "Show scene range markers by default" + showRangeMarkers: Boolean + "Maximum duration (in seconds) in which a scene video will loop in the scene player" maximumLoopDuration: Int "If true, video will autostart on load in the scene player" @@ -421,6 +424,9 @@ type ConfigInterfaceResult { "Show scene scrubber by default" showScrubber: Boolean + "Show scene range markers by default" + showRangeMarkers: Boolean + "Maximum duration (in seconds) in which a scene video will loop in the scene player" maximumLoopDuration: Int "True if we should not auto-open a browser window on startup" diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index d9c71b09fca..21741c4ff10 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -457,6 +457,7 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI r.setConfigBool(config.NotificationsEnabled, input.NotificationsEnabled) r.setConfigBool(config.ShowScrubber, input.ShowScrubber) + r.setConfigBool(config.ShowRangeMarkers, input.ShowRangeMarkers) r.setConfigString(config.WallPlayback, input.WallPlayback) r.setConfigInt(config.MaximumLoopDuration, input.MaximumLoopDuration) diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index 3328e4a356b..4f223eff824 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -141,6 +141,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { soundOnPreview := config.GetSoundOnPreview() wallShowTitle := config.GetWallShowTitle() showScrubber := config.GetShowScrubber() + showRangeMarkers := config.GetShowRangeMarkers() wallPlayback := config.GetWallPlayback() noBrowser := config.GetNoBrowser() notificationsEnabled := config.GetNotificationsEnabled() @@ -168,6 +169,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { WallShowTitle: &wallShowTitle, WallPlayback: &wallPlayback, ShowScrubber: &showScrubber, + ShowRangeMarkers: &showRangeMarkers, MaximumLoopDuration: &maximumLoopDuration, NoBrowser: &noBrowser, NotificationsEnabled: ¬ificationsEnabled, diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index aa7999c5386..66ccdf746d6 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -195,6 +195,9 @@ const ( ShowScrubber = "show_scrubber" showScrubberDefault = true + ShowRangeMarkers = "show_rangemarkers" + showRangeMarkersDef = true + WallPlayback = "wall_playback" defaultWallPlayback = "video" @@ -1212,6 +1215,10 @@ func (i *Config) GetShowScrubber() bool { return i.getBoolDefault(ShowScrubber, showScrubberDefault) } +func (i *Config) GetShowRangeMarkers() bool { + return i.getBoolDefault(ShowRangeMarkers, showRangeMarkersDef) +} + func (i *Config) GetMaximumLoopDuration() int { return i.getInt(MaximumLoopDuration) } diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index ae15aa939c4..65ecd0ef622 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -75,6 +75,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { wallShowTitle wallPlayback showScrubber + showRangeMarkers maximumLoopDuration noBrowser notificationsEnabled diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index f40e03bab51..b57d5a7e7a1 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -718,13 +718,21 @@ export const ScenePlayer: React.FC = ({ // Wait for colors await markers.findColors(uniqueTagNames); + const showRangeTags = interfaceConfig?.showRangeMarkers ?? true; const timestampMarkers: IMarker[] = []; const rangeMarkers: IMarker[] = []; - for (const marker of markerData) { - if (marker.end_seconds === null) { + + if (!showRangeTags) { + for (const marker of markerData) { timestampMarkers.push(marker); - } else { - rangeMarkers.push(marker); + } + } else { + for (const marker of markerData) { + if (marker.end_seconds === null) { + timestampMarkers.push(marker); + } else { + rangeMarkers.push(marker); + } } } diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index ea8e1e80c12..cda350144dd 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -68,9 +68,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { range?: HTMLDivElement; } = {}; const seekBar = this.player.el().querySelector(".vjs-progress-control"); - if (marker.end_seconds) { - throw new Error("Cannot add range marker with addDotMarker"); - } + markerSet.dot = videojs.dom.createEl("div") as HTMLDivElement; markerSet.dot.className = "vjs-marker-dot"; if (duration) { diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index c174847c982..96b3fa620d8 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -351,6 +351,12 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( checked={iface.showScrubber ?? undefined} onChange={(v) => saveInterface({ showScrubber: v })} /> + saveInterface({ showRangeMarkers: v })} + /> Date: Fri, 7 Feb 2025 18:44:46 -0500 Subject: [PATCH 14/23] update react dependency list --- ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index b57d5a7e7a1..f9c4e5b44f5 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -757,7 +757,7 @@ export const ScenePlayer: React.FC = ({ return () => { player.off("loadedmetadata"); }; - }, [getPlayer, scene]); + }, [getPlayer, scene, interfaceConfig]); useEffect(() => { const player = getPlayer(); From 1c463d7a9d4e54d888a38cfb1d9dbf982e66ec5f Mon Sep 17 00:00:00 2001 From: skier233 Date: Fri, 7 Feb 2025 18:59:39 -0500 Subject: [PATCH 15/23] Don't show range markers on mobile --- ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index f9c4e5b44f5..10e72944c0d 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -718,7 +718,9 @@ export const ScenePlayer: React.FC = ({ // Wait for colors await markers.findColors(uniqueTagNames); - const showRangeTags = interfaceConfig?.showRangeMarkers ?? true; + const showRangeTags = + !ScreenUtils.isMobile() && + (interfaceConfig?.showRangeMarkers ?? true); const timestampMarkers: IMarker[] = []; const rangeMarkers: IMarker[] = []; From 0dad53913c38aec1fcd58d2dc4e6ebd424c0f7eb Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 11 Feb 2025 15:37:08 -0500 Subject: [PATCH 16/23] fix markers not loading when creating/updating/deleting new marker, add min range marker width --- .../components/ScenePlayer/ScenePlayer.tsx | 126 ++++++++++-------- ui/v2.5/src/components/ScenePlayer/markers.ts | 8 +- 2 files changed, 73 insertions(+), 61 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 10e72944c0d..d5aa0d64500 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -699,67 +699,17 @@ export const ScenePlayer: React.FC = ({ if (!player) return; // Ensure markers are added after player is fully ready and sources are loaded - player.on("loadedmetadata", () => { - const loadMarkers = async () => { - const markerData = scene.scene_markers.map((marker) => ({ - title: getMarkerTitle(marker), - seconds: marker.seconds, - end_seconds: marker.end_seconds ?? null, - primaryTag: marker.primary_tag, - })); - - const markers = player.markers(); - markers.clearMarkers(); - - const uniqueTagNames = markerData - .map((marker) => marker.primaryTag.name) - .filter((value, index, self) => self.indexOf(value) === index); - - // Wait for colors - await markers.findColors(uniqueTagNames); - - const showRangeTags = - !ScreenUtils.isMobile() && - (interfaceConfig?.showRangeMarkers ?? true); - const timestampMarkers: IMarker[] = []; - const rangeMarkers: IMarker[] = []; - - if (!showRangeTags) { - for (const marker of markerData) { - timestampMarkers.push(marker); - } - } else { - for (const marker of markerData) { - if (marker.end_seconds === null) { - timestampMarkers.push(marker); - } else { - rangeMarkers.push(marker); - } - } - } - - // Add markers in chunks - const CHUNK_SIZE = 10; - for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) { - const chunk = timestampMarkers.slice(i, i + CHUNK_SIZE); - requestAnimationFrame(() => { - chunk.forEach((m) => markers.addDotMarker(m)); - }); - } - - requestAnimationFrame(() => { - markers.addRangeMarkers(rangeMarkers); - }); - }; - - // Call our async function - void loadMarkers(); - }); - + if (player.readyState() >= 1) { + loadMarkers(); + } else { + player.on("loadedmetadata", () => { + loadMarkers(); + }); + } return () => { - player.off("loadedmetadata"); + player.off("loadedmetadata", loadMarkers); }; - }, [getPlayer, scene, interfaceConfig]); + }, [getPlayer, scene]); useEffect(() => { const player = getPlayer(); @@ -845,6 +795,64 @@ export const ScenePlayer: React.FC = ({ } } + function loadMarkers() { + const player = getPlayer(); + if (!player) return; + const loadMarkersAsync = async () => { + const markerData = scene.scene_markers.map((marker) => ({ + title: getMarkerTitle(marker), + seconds: marker.seconds, + end_seconds: marker.end_seconds ?? null, + primaryTag: marker.primary_tag, + })); + + const markers = player.markers(); + markers.clearMarkers(); + + const uniqueTagNames = markerData + .map((marker) => marker.primaryTag.name) + .filter((value, index, self) => self.indexOf(value) === index); + + // Wait for colors + await markers.findColors(uniqueTagNames); + + const showRangeTags = + !ScreenUtils.isMobile() && (interfaceConfig?.showRangeMarkers ?? true); + const timestampMarkers: IMarker[] = []; + const rangeMarkers: IMarker[] = []; + + if (!showRangeTags) { + for (const marker of markerData) { + timestampMarkers.push(marker); + } + } else { + for (const marker of markerData) { + if (marker.end_seconds === null) { + timestampMarkers.push(marker); + } else { + rangeMarkers.push(marker); + } + } + } + + // Add markers in chunks + const CHUNK_SIZE = 10; + for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) { + const chunk = timestampMarkers.slice(i, i + CHUNK_SIZE); + requestAnimationFrame(() => { + chunk.forEach((m) => markers.addDotMarker(m)); + }); + } + + requestAnimationFrame(() => { + markers.addRangeMarkers(rangeMarkers); + }); + }; + + // Call our async function + void loadMarkersAsync(); + } + function onScrubberSeek(seconds: number) { if (started.current) { getPlayer()?.currentTime(seconds); diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index cda350144dd..2b232d68661 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -129,8 +129,12 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { const startPercent = (marker.seconds / duration) * 100; const endPercent = (marker.end_seconds / duration) * 100; - const width = endPercent - startPercent; - + let width = endPercent - startPercent; + // Ensure the width is at least 8px + const minWidth = (10 / seekBar.clientWidth) * 100; // Convert 8px to percentage + if (width < minWidth) { + width = minWidth; + } rangeDiv.style.left = `${startPercent}%`; rangeDiv.style.width = `${width}%`; rangeDiv.style.bottom = `${layer * this.layerHeight}px`; // Adjust height based on layer From e4c4ce8534312ecb38f4c05be8742e1023b451f8 Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 11 Feb 2025 15:43:33 -0500 Subject: [PATCH 17/23] small cleanup --- ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index d5aa0d64500..c71ebabf8cd 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -701,14 +701,15 @@ export const ScenePlayer: React.FC = ({ // Ensure markers are added after player is fully ready and sources are loaded if (player.readyState() >= 1) { loadMarkers(); + return; } else { player.on("loadedmetadata", () => { loadMarkers(); }); + return () => { + player.off("loadedmetadata", loadMarkers); + }; } - return () => { - player.off("loadedmetadata", loadMarkers); - }; }, [getPlayer, scene]); useEffect(() => { From 081db5e12a7ce29ba222322df11600e9d5060d28 Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 11 Feb 2025 15:47:32 -0500 Subject: [PATCH 18/23] add missing react dependency --- ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index c71ebabf8cd..3a230822577 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -710,7 +710,7 @@ export const ScenePlayer: React.FC = ({ player.off("loadedmetadata", loadMarkers); }; } - }, [getPlayer, scene]); + }, [getPlayer, scene, loadMarkers]); useEffect(() => { const player = getPlayer(); From 1076e48c18d2339b0500128849d1dda77c8010b7 Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 11 Feb 2025 15:52:36 -0500 Subject: [PATCH 19/23] move loadmarkers --- .../components/ScenePlayer/ScenePlayer.tsx | 117 +++++++++--------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 3a230822577..f4c86cbe45b 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -698,6 +698,63 @@ export const ScenePlayer: React.FC = ({ const player = getPlayer(); if (!player) return; + function loadMarkers() { + const player = getPlayer(); + if (!player) return; + const loadMarkersAsync = async () => { + const markerData = scene.scene_markers.map((marker) => ({ + title: getMarkerTitle(marker), + seconds: marker.seconds, + end_seconds: marker.end_seconds ?? null, + primaryTag: marker.primary_tag, + })); + + const markers = player.markers(); + markers.clearMarkers(); + + const uniqueTagNames = markerData + .map((marker) => marker.primaryTag.name) + .filter((value, index, self) => self.indexOf(value) === index); + + // Wait for colors + await markers.findColors(uniqueTagNames); + + const showRangeTags = + !ScreenUtils.isMobile() && (interfaceConfig?.showRangeMarkers ?? true); + const timestampMarkers: IMarker[] = []; + const rangeMarkers: IMarker[] = []; + + if (!showRangeTags) { + for (const marker of markerData) { + timestampMarkers.push(marker); + } + } else { + for (const marker of markerData) { + if (marker.end_seconds === null) { + timestampMarkers.push(marker); + } else { + rangeMarkers.push(marker); + } + } + } + + // Add markers in chunks + const CHUNK_SIZE = 10; + for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) { + const chunk = timestampMarkers.slice(i, i + CHUNK_SIZE); + requestAnimationFrame(() => { + chunk.forEach((m) => markers.addDotMarker(m)); + }); + } + + requestAnimationFrame(() => { + markers.addRangeMarkers(rangeMarkers); + }); + }; + + // Call our async function + void loadMarkersAsync(); + } // Ensure markers are added after player is fully ready and sources are loaded if (player.readyState() >= 1) { loadMarkers(); @@ -710,7 +767,7 @@ export const ScenePlayer: React.FC = ({ player.off("loadedmetadata", loadMarkers); }; } - }, [getPlayer, scene, loadMarkers]); + }, [getPlayer, scene]); useEffect(() => { const player = getPlayer(); @@ -796,64 +853,6 @@ export const ScenePlayer: React.FC = ({ } } - function loadMarkers() { - const player = getPlayer(); - if (!player) return; - const loadMarkersAsync = async () => { - const markerData = scene.scene_markers.map((marker) => ({ - title: getMarkerTitle(marker), - seconds: marker.seconds, - end_seconds: marker.end_seconds ?? null, - primaryTag: marker.primary_tag, - })); - - const markers = player.markers(); - markers.clearMarkers(); - - const uniqueTagNames = markerData - .map((marker) => marker.primaryTag.name) - .filter((value, index, self) => self.indexOf(value) === index); - - // Wait for colors - await markers.findColors(uniqueTagNames); - - const showRangeTags = - !ScreenUtils.isMobile() && (interfaceConfig?.showRangeMarkers ?? true); - const timestampMarkers: IMarker[] = []; - const rangeMarkers: IMarker[] = []; - - if (!showRangeTags) { - for (const marker of markerData) { - timestampMarkers.push(marker); - } - } else { - for (const marker of markerData) { - if (marker.end_seconds === null) { - timestampMarkers.push(marker); - } else { - rangeMarkers.push(marker); - } - } - } - - // Add markers in chunks - const CHUNK_SIZE = 10; - for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) { - const chunk = timestampMarkers.slice(i, i + CHUNK_SIZE); - requestAnimationFrame(() => { - chunk.forEach((m) => markers.addDotMarker(m)); - }); - } - - requestAnimationFrame(() => { - markers.addRangeMarkers(rangeMarkers); - }); - }; - - // Call our async function - void loadMarkersAsync(); - } - function onScrubberSeek(seconds: number) { if (started.current) { getPlayer()?.currentTime(seconds); From ee9356db4c988aff4f3f328fae88fb6c606debee Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 11 Feb 2025 16:01:48 -0500 Subject: [PATCH 20/23] fix ui validation --- .../components/ScenePlayer/ScenePlayer.tsx | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index f4c86cbe45b..e9128e4cf6a 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -699,8 +699,6 @@ export const ScenePlayer: React.FC = ({ if (!player) return; function loadMarkers() { - const player = getPlayer(); - if (!player) return; const loadMarkersAsync = async () => { const markerData = scene.scene_markers.map((marker) => ({ title: getMarkerTitle(marker), @@ -708,22 +706,23 @@ export const ScenePlayer: React.FC = ({ end_seconds: marker.end_seconds ?? null, primaryTag: marker.primary_tag, })); - - const markers = player.markers(); + + const markers = player!.markers(); markers.clearMarkers(); - + const uniqueTagNames = markerData .map((marker) => marker.primaryTag.name) .filter((value, index, self) => self.indexOf(value) === index); - + // Wait for colors await markers.findColors(uniqueTagNames); - + const showRangeTags = - !ScreenUtils.isMobile() && (interfaceConfig?.showRangeMarkers ?? true); + !ScreenUtils.isMobile() && + (interfaceConfig?.showRangeMarkers ?? true); const timestampMarkers: IMarker[] = []; const rangeMarkers: IMarker[] = []; - + if (!showRangeTags) { for (const marker of markerData) { timestampMarkers.push(marker); @@ -737,7 +736,7 @@ export const ScenePlayer: React.FC = ({ } } } - + // Add markers in chunks const CHUNK_SIZE = 10; for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) { @@ -746,12 +745,12 @@ export const ScenePlayer: React.FC = ({ chunk.forEach((m) => markers.addDotMarker(m)); }); } - + requestAnimationFrame(() => { markers.addRangeMarkers(rangeMarkers); }); }; - + // Call our async function void loadMarkersAsync(); } @@ -767,7 +766,7 @@ export const ScenePlayer: React.FC = ({ player.off("loadedmetadata", loadMarkers); }; } - }, [getPlayer, scene]); + }, [getPlayer, scene, interfaceConfig]); useEffect(() => { const player = getPlayer(); From 2e0e34927d737a7b537e489878537bb22e9eb540 Mon Sep 17 00:00:00 2001 From: skier233 Date: Tue, 11 Feb 2025 19:58:35 -0500 Subject: [PATCH 21/23] move showrangemarkers option to ui --- graphql/schema/types/config.graphql | 3 --- internal/api/resolver_mutation_configure.go | 1 - internal/api/resolver_query_configuration.go | 2 -- internal/manager/config/config.go | 7 ------- ui/v2.5/graphql/data/config.graphql | 1 - ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx | 4 ++-- .../SettingsInterfacePanel/SettingsInterfacePanel.tsx | 4 ++-- ui/v2.5/src/core/config.ts | 2 ++ 8 files changed, 6 insertions(+), 18 deletions(-) diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 5677082eb62..c75222958cc 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -355,9 +355,6 @@ input ConfigInterfaceInput { "Show scene scrubber by default" showScrubber: Boolean - "Show scene range markers by default" - showRangeMarkers: Boolean - "Maximum duration (in seconds) in which a scene video will loop in the scene player" maximumLoopDuration: Int "If true, video will autostart on load in the scene player" diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 21741c4ff10..d9c71b09fca 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -457,7 +457,6 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI r.setConfigBool(config.NotificationsEnabled, input.NotificationsEnabled) r.setConfigBool(config.ShowScrubber, input.ShowScrubber) - r.setConfigBool(config.ShowRangeMarkers, input.ShowRangeMarkers) r.setConfigString(config.WallPlayback, input.WallPlayback) r.setConfigInt(config.MaximumLoopDuration, input.MaximumLoopDuration) diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index 4f223eff824..3328e4a356b 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -141,7 +141,6 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { soundOnPreview := config.GetSoundOnPreview() wallShowTitle := config.GetWallShowTitle() showScrubber := config.GetShowScrubber() - showRangeMarkers := config.GetShowRangeMarkers() wallPlayback := config.GetWallPlayback() noBrowser := config.GetNoBrowser() notificationsEnabled := config.GetNotificationsEnabled() @@ -169,7 +168,6 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { WallShowTitle: &wallShowTitle, WallPlayback: &wallPlayback, ShowScrubber: &showScrubber, - ShowRangeMarkers: &showRangeMarkers, MaximumLoopDuration: &maximumLoopDuration, NoBrowser: &noBrowser, NotificationsEnabled: ¬ificationsEnabled, diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 66ccdf746d6..aa7999c5386 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -195,9 +195,6 @@ const ( ShowScrubber = "show_scrubber" showScrubberDefault = true - ShowRangeMarkers = "show_rangemarkers" - showRangeMarkersDef = true - WallPlayback = "wall_playback" defaultWallPlayback = "video" @@ -1215,10 +1212,6 @@ func (i *Config) GetShowScrubber() bool { return i.getBoolDefault(ShowScrubber, showScrubberDefault) } -func (i *Config) GetShowRangeMarkers() bool { - return i.getBoolDefault(ShowRangeMarkers, showRangeMarkersDef) -} - func (i *Config) GetMaximumLoopDuration() int { return i.getInt(MaximumLoopDuration) } diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index 65ecd0ef622..ae15aa939c4 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -75,7 +75,6 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { wallShowTitle wallPlayback showScrubber - showRangeMarkers maximumLoopDuration noBrowser notificationsEnabled diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index e9128e4cf6a..321e2b75a58 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -719,7 +719,7 @@ export const ScenePlayer: React.FC = ({ const showRangeTags = !ScreenUtils.isMobile() && - (interfaceConfig?.showRangeMarkers ?? true); + (uiConfig?.showRangeMarkers ?? true); const timestampMarkers: IMarker[] = []; const rangeMarkers: IMarker[] = []; @@ -766,7 +766,7 @@ export const ScenePlayer: React.FC = ({ player.off("loadedmetadata", loadMarkers); }; } - }, [getPlayer, scene, interfaceConfig]); + }, [getPlayer, scene, uiConfig]); useEffect(() => { const player = getPlayer(); diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 96b3fa620d8..dc56557ef50 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -354,8 +354,8 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( saveInterface({ showRangeMarkers: v })} + checked={ui.showRangeMarkers ?? undefined} + onChange={(v) => saveUI({ showRangeMarkers: v })} /> Date: Tue, 11 Feb 2025 20:04:28 -0500 Subject: [PATCH 22/23] cleanup --- graphql/schema/types/config.graphql | 3 --- ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index c75222958cc..4d6d2080b59 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -421,9 +421,6 @@ type ConfigInterfaceResult { "Show scene scrubber by default" showScrubber: Boolean - "Show scene range markers by default" - showRangeMarkers: Boolean - "Maximum duration (in seconds) in which a scene video will loop in the scene player" maximumLoopDuration: Int "True if we should not auto-open a browser window on startup" diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 321e2b75a58..fb1c6beb3ec 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -718,8 +718,7 @@ export const ScenePlayer: React.FC = ({ await markers.findColors(uniqueTagNames); const showRangeTags = - !ScreenUtils.isMobile() && - (uiConfig?.showRangeMarkers ?? true); + !ScreenUtils.isMobile() && (uiConfig?.showRangeMarkers ?? true); const timestampMarkers: IMarker[] = []; const rangeMarkers: IMarker[] = []; From 59966f75f2e0dede3d70104db51c97fdadfc7b5c Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:49:31 +1100 Subject: [PATCH 23/23] i8n label fix --- .../SettingsInterfacePanel/SettingsInterfacePanel.tsx | 4 ++-- ui/v2.5/src/locales/en-GB.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index dc56557ef50..35f51f3eb62 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -352,8 +352,8 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( onChange={(v) => saveInterface({ showScrubber: v })} /> saveUI({ showRangeMarkers: v })} /> diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 084b79d2e0b..0d80a7894a0 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -750,7 +750,7 @@ "enable_chromecast": "Enable Chromecast", "show_ab_loop_controls": "Show AB Loop plugin controls", "show_scrubber": "Show Scrubber", - "show_rangemarkers": "Show Range Markers", + "show_range_markers": "Show Range Markers", "track_activity": "Enable Scene Play history", "vr_tag": { "description": "The VR button will only be displayed for scenes with this tag.",