diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 98ec62dea..24039dfba 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -68,6 +68,7 @@ "ua-parser-js": "^1.0.34", "universal-cookie": "^4.0.4", "video.js": "^7.21.3", + "videojs-abloop": "^1.2.0", "videojs-contrib-dash": "^5.1.1", "videojs-mobile-ui": "^0.8.0", "videojs-seek-buttons": "^3.0.1", diff --git a/ui/v2.5/src/@types/videojs-abloop.d.ts b/ui/v2.5/src/@types/videojs-abloop.d.ts new file mode 100644 index 000000000..b44d9f50c --- /dev/null +++ b/ui/v2.5/src/@types/videojs-abloop.d.ts @@ -0,0 +1,35 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +declare module "videojs-abloop" { + import videojs from "video.js"; + + declare function abLoopPlugin( + window: Window & typeof globalThis, + player: videojs + ): abLoopPlugin.Plugin; + + declare namespace abLoopPlugin { + interface Options { + start: number | boolean; + end: number | boolean; + enabled: boolean; + loopIfBeforeStart: boolean; + loopIfAfterEnd: boolean; + pauseBeforeLooping: boolean; + pauseAfterLooping: boolean; + } + + class Plugin extends videojs.Plugin { + getOptions(): Options; + setOptions(o: Options): void; + } + } + + export = abLoopPlugin; + + declare module "video.js" { + interface VideoJsPlayer { + abLoopPlugin: abLoopPlugin.Plugin; + } + } +} diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index bb28f61f3..a1b9a7ae9 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -8,6 +8,7 @@ import React, { useState, } from "react"; import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js"; +import abLoopPlugin from "videojs-abloop"; import useScript from "src/hooks/useScript"; import "videojs-contrib-dash"; import "videojs-mobile-ui"; @@ -73,6 +74,21 @@ function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) { player.currentTime(time); } + function toggleABLooping() { + const opts = player.abLoopPlugin.getOptions(); + if (!opts.start) { + opts.start = player.currentTime(); + } else if (!opts.end) { + opts.end = player.currentTime(); + opts.enabled = true; + } else { + opts.start = 0; + opts.end = 0; + opts.enabled = false; + } + player.abLoopPlugin.setOptions(opts); + } + let seekFactor = 10; if (event.shiftKey) { seekFactor = 5; @@ -111,6 +127,9 @@ function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) { if (player.isFullscreen()) player.exitFullscreen(); else player.requestFullscreen(); break; + case 76: // l + toggleABLooping(); + break; case 38: // up arrow player.volume(player.volume() + 0.1); break; @@ -340,6 +359,16 @@ export const ScenePlayer: React.FC = ({ skipButtons: {}, trackActivity: {}, vrMenu: {}, + abLoopPlugin: { + start: 0, + end: false, + enabled: false, + loopIfBeforeStart: true, + loopIfAfterEnd: true, + pauseAfterLooping: false, + pauseBeforeLooping: false, + createButtons: uiConfig?.showAbLoopControls ?? false, + }, }, }; @@ -349,6 +378,8 @@ export const ScenePlayer: React.FC = ({ videoEl.classList.add("vjs-big-play-centered"); videoRef.current!.appendChild(videoEl); + abLoopPlugin(window, videojs); + const vjs = videojs(videoEl, options); /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -372,7 +403,8 @@ export const ScenePlayer: React.FC = ({ sceneId.current = undefined; }; // empty deps - only init once - }, []); + // showAbLoopControls is necessary to re-init the player when the config changes + }, [uiConfig?.showAbLoopControls]); 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 6b6bf69bc..4d8c5544c 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -364,6 +364,13 @@ export const SettingsInterfacePanel: React.FC = () => { return {DurationUtils.secondsToString(v ?? 0)}; }} /> + + saveUI({ showAbLoopControls: v })} + />