Region-based Looping (a.k.a. A/B looping) utilizing videojs-abloop plugin (#3904)

* yarn add videojs-abloop
* add abLoop plugin to video player
* adding player keyboard shortcut 'l' for toggling a/b looping

copies mpv behavior:
if a/b loop start not yet set, sets start to current player time
elif a/b loop stop not yet set, sets end to current player time and enables loop
else, disables a/b loop

relates to #3264 (https://github.com/stashapp/stash/issues/3264)

* update help with keyboard shortcut
* Add plugin type definitions
* Make UI elements optional
---------
Co-authored-by: chickenwingavalanche <chickenwingavalanche@example.com>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
chickenwingavalanche
2023-08-23 20:58:47 -06:00
committed by GitHub
parent 922aef3e5a
commit 1f3ed07188
8 changed files with 88 additions and 2 deletions

View File

@@ -68,6 +68,7 @@
"ua-parser-js": "^1.0.34", "ua-parser-js": "^1.0.34",
"universal-cookie": "^4.0.4", "universal-cookie": "^4.0.4",
"video.js": "^7.21.3", "video.js": "^7.21.3",
"videojs-abloop": "^1.2.0",
"videojs-contrib-dash": "^5.1.1", "videojs-contrib-dash": "^5.1.1",
"videojs-mobile-ui": "^0.8.0", "videojs-mobile-ui": "^0.8.0",
"videojs-seek-buttons": "^3.0.1", "videojs-seek-buttons": "^3.0.1",

35
ui/v2.5/src/@types/videojs-abloop.d.ts vendored Normal file
View File

@@ -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;
}
}
}

View File

@@ -8,6 +8,7 @@ import React, {
useState, useState,
} from "react"; } from "react";
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js"; import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
import abLoopPlugin from "videojs-abloop";
import useScript from "src/hooks/useScript"; import useScript from "src/hooks/useScript";
import "videojs-contrib-dash"; import "videojs-contrib-dash";
import "videojs-mobile-ui"; import "videojs-mobile-ui";
@@ -73,6 +74,21 @@ function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) {
player.currentTime(time); 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; let seekFactor = 10;
if (event.shiftKey) { if (event.shiftKey) {
seekFactor = 5; seekFactor = 5;
@@ -111,6 +127,9 @@ function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) {
if (player.isFullscreen()) player.exitFullscreen(); if (player.isFullscreen()) player.exitFullscreen();
else player.requestFullscreen(); else player.requestFullscreen();
break; break;
case 76: // l
toggleABLooping();
break;
case 38: // up arrow case 38: // up arrow
player.volume(player.volume() + 0.1); player.volume(player.volume() + 0.1);
break; break;
@@ -340,6 +359,16 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
skipButtons: {}, skipButtons: {},
trackActivity: {}, trackActivity: {},
vrMenu: {}, 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<IScenePlayerProps> = ({
videoEl.classList.add("vjs-big-play-centered"); videoEl.classList.add("vjs-big-play-centered");
videoRef.current!.appendChild(videoEl); videoRef.current!.appendChild(videoEl);
abLoopPlugin(window, videojs);
const vjs = videojs(videoEl, options); const vjs = videojs(videoEl, options);
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -372,7 +403,8 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
sceneId.current = undefined; sceneId.current = undefined;
}; };
// empty deps - only init once // empty deps - only init once
}, []); // showAbLoopControls is necessary to re-init the player when the config changes
}, [uiConfig?.showAbLoopControls]);
useEffect(() => { useEffect(() => {
const player = getPlayer(); const player = getPlayer();

View File

@@ -364,6 +364,13 @@ export const SettingsInterfacePanel: React.FC = () => {
return <span>{DurationUtils.secondsToString(v ?? 0)}</span>; return <span>{DurationUtils.secondsToString(v ?? 0)}</span>;
}} }}
/> />
<BooleanSetting
id="show-ab-loop"
headingID="config.ui.scene_player.options.show_ab_loop_controls"
checked={ui.showAbLoopControls ?? undefined}
onChange={(v) => saveUI({ showAbLoopControls: v })}
/>
</SettingSection> </SettingSection>
<SettingSection headingID="config.ui.tag_panel.heading"> <SettingSection headingID="config.ui.tag_panel.heading">
<BooleanSetting <BooleanSetting

View File

@@ -55,8 +55,10 @@ export interface IUIConfig {
compactExpandedDetails?: boolean; compactExpandedDetails?: boolean;
// if true show all content details by default // if true show all content details by default
showAllDetails?: boolean; showAllDetails?: boolean;
// if true the chromecast option will enabled // if true the chromecast option will enabled
enableChromecast?: boolean; enableChromecast?: boolean;
// if true continue scene will always play from the beginning // if true continue scene will always play from the beginning
alwaysStartFromBeginning?: boolean; alwaysStartFromBeginning?: boolean;
// if true enable activity tracking // if true enable activity tracking
@@ -65,6 +67,8 @@ export interface IUIConfig {
// before the play count is incremented // before the play count is incremented
minimumPlayPercent?: number; minimumPlayPercent?: number;
showAbLoopControls?: boolean;
// maximum number of items to shown in the dropdown list - defaults to 200 // maximum number of items to shown in the dropdown list - defaults to 200
// upper limit of 1000 // upper limit of 1000
maxOptionsShown?: number; maxOptionsShown?: number;

View File

@@ -78,7 +78,8 @@
| `↑` | Increase volume 10% | | `↑` | Increase volume 10% |
| `↓` | Decrease volume 10% | | `↓` | Decrease volume 10% |
| `m` | Toggle mute | | `m` | Toggle mute |
| `Shift + l` | Toggle player looping | | `l` | A/B looping toggle. Press once to set start point. Press again to set end point. Press again to disable loop. |
| `Shift + l` | Toggle looping of scene when it's over |
### Scene Markers tab shortcuts ### Scene Markers tab shortcuts

View File

@@ -690,6 +690,7 @@
"heading": "Continue playlist by default" "heading": "Continue playlist by default"
}, },
"enable_chromecast": "Enable Chromecast", "enable_chromecast": "Enable Chromecast",
"show_ab_loop_controls": "Show AB Loop plugin controls",
"show_scrubber": "Show Scrubber", "show_scrubber": "Show Scrubber",
"track_activity": "Track Activity", "track_activity": "Track Activity",
"vr_tag": { "vr_tag": {

View File

@@ -8104,6 +8104,11 @@ vfile@^4.0.0:
videojs-font "3.2.0" videojs-font "3.2.0"
videojs-vtt.js "^0.15.4" videojs-vtt.js "^0.15.4"
videojs-abloop@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/videojs-abloop/-/videojs-abloop-1.2.0.tgz#ead4054400e6107d6512553ddff2a97260decf3e"
integrity sha512-6/hvtB5gNQUr5FJ969UhXVg5H+3wxhOzh9AVftlezOXlhzzaWfNfiOJYqNKo01Gc/eSQOvfttrOX7jH+aHpwrw==
videojs-contrib-dash@^5.1.1: videojs-contrib-dash@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/videojs-contrib-dash/-/videojs-contrib-dash-5.1.1.tgz#9f50191677815a7d816c500977811a926aee0643" resolved "https://registry.yarnpkg.com/videojs-contrib-dash/-/videojs-contrib-dash-5.1.1.tgz#9f50191677815a7d816c500977811a926aee0643"