From 302419739b8cefd29b315ac97864f4634f6a065b Mon Sep 17 00:00:00 2001 From: CJ Date: Wed, 3 Dec 2025 00:12:49 -0600 Subject: [PATCH] add autostart button to videoplayer --- .../components/ScenePlayer/ScenePlayer.tsx | 25 +++- .../ScenePlayer/autostart-button.ts | 131 ++++++++++++++++++ .../src/components/ScenePlayer/styles.scss | 32 +++++ 3 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 ui/v2.5/src/components/ScenePlayer/autostart-button.ts diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 5aeb56e96..b67cac9bf 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -16,6 +16,7 @@ import "./live"; import "./PlaylistButtons"; import "./source-selector"; import "./persist-volume"; +import "./autostart-button"; import MarkersPlugin, { type IMarker } from "./markers"; void MarkersPlugin; import "./vtt-thumbnails"; @@ -389,6 +390,9 @@ export const ScenePlayer: React.FC = PatchComponent( skipButtons: {}, trackActivity: {}, vrMenu: {}, + autostartButton: { + enabled: interfaceConfig?.autostartVideo ?? false, + }, abLoopPlugin: { start: 0, end: false, @@ -675,11 +679,6 @@ export const ScenePlayer: React.FC = PatchComponent( } } - auto.current = - autoplay || - (interfaceConfig?.autostartVideo ?? false) || - _initialTimestamp > 0; - const alwaysStartFromBeginning = uiConfig?.alwaysStartFromBeginning ?? false; const resumeTime = scene.resume_time ?? 0; @@ -698,6 +697,22 @@ export const ScenePlayer: React.FC = PatchComponent( player.load(); player.focus(); + // Check the autostart button plugin for user preference + const autostartButton = player.autostartButton(); + autostartButton.getEnabled().then((buttonEnabled) => { + auto.current = + autoplay || + buttonEnabled || + (interfaceConfig?.autostartVideo ?? false) || + _initialTimestamp > 0; + + // Trigger autoplay if conditions are met and player is ready + if (auto.current && ready && player.paused()) { + player.play(); + auto.current = false; + } + }); + player.ready(() => { player.vttThumbnails().src(scene.paths.vtt ?? null); diff --git a/ui/v2.5/src/components/ScenePlayer/autostart-button.ts b/ui/v2.5/src/components/ScenePlayer/autostart-button.ts new file mode 100644 index 000000000..3c76f6ee1 --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/autostart-button.ts @@ -0,0 +1,131 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import videojs, { VideoJsPlayer } from "video.js"; +import localForage from "localforage"; + +const AUTOSTART_KEY = "video-autostart-enabled"; + +interface IAutostartButtonOptions { + enabled?: boolean; +} + +interface AutostartButtonOptions extends videojs.ComponentOptions { + autostartEnabled: boolean; +} + +class AutostartButton extends videojs.getComponent("Button") { + private autostartEnabled: boolean; + + constructor(player: VideoJsPlayer, options: AutostartButtonOptions) { + super(player, options); + this.autostartEnabled = options.autostartEnabled; + this.updateIcon(); + } + + buildCSSClass() { + return `vjs-autostart-button ${super.buildCSSClass()}`; + } + + private updateIcon() { + this.removeClass("vjs-icon-play-circle"); + this.removeClass("vjs-icon-cancel"); + + if (this.autostartEnabled) { + this.addClass("vjs-icon-play-circle"); + this.controlText(this.localize("Auto-start enabled (click to disable)")); + } else { + this.addClass("vjs-icon-cancel"); + this.controlText(this.localize("Auto-start disabled (click to enable)")); + } + } + + handleClick() { + this.autostartEnabled = !this.autostartEnabled; + this.updateIcon(); + this.trigger("autostartchanged", { enabled: this.autostartEnabled }); + } + + public setEnabled(enabled: boolean) { + this.autostartEnabled = enabled; + this.updateIcon(); + } +} + +class AutostartButtonPlugin extends videojs.getPlugin("plugin") { + private button: AutostartButton; + private autostartEnabled: boolean; + private loaded: boolean = false; + + constructor(player: VideoJsPlayer, options?: IAutostartButtonOptions) { + super(player, options); + + this.autostartEnabled = options?.enabled ?? false; + + // Load the saved preference immediately + this.loadPreference(); + + this.button = new AutostartButton(player, { + autostartEnabled: this.autostartEnabled, + }); + + player.ready(() => { + this.ready(); + }); + } + + private async loadPreference() { + const value = await localForage.getItem(AUTOSTART_KEY); + if (value !== null) { + this.autostartEnabled = value; + if (this.button) { + this.button.setEnabled(value); + } + } + this.loaded = true; + } + + private ready() { + // Add button to control bar, before the fullscreen button + const controlBar = this.player.controlBar; + const fullscreenToggle = controlBar.getChild("fullscreenToggle"); + if (fullscreenToggle) { + controlBar.addChild(this.button); + controlBar.el().insertBefore(this.button.el(), fullscreenToggle.el()); + } else { + controlBar.addChild(this.button); + } + + // Listen for changes + this.button.on("autostartchanged", (_, data: { enabled: boolean }) => { + this.autostartEnabled = data.enabled; + localForage.setItem(AUTOSTART_KEY, data.enabled); + }); + } + + public isEnabled(): boolean { + return this.autostartEnabled; + } + + public async getEnabled(): Promise { + // Wait for the preference to be loaded if it hasn't been yet + if (!this.loaded) { + await this.loadPreference(); + } + return this.autostartEnabled; + } +} + +// Register the plugin with video.js. +videojs.registerComponent("AutostartButton", AutostartButton); +videojs.registerPlugin("autostartButton", AutostartButtonPlugin); + +declare module "video.js" { + interface VideoJsPlayer { + autostartButton: () => AutostartButtonPlugin; + } + interface VideoJsPlayerPluginOptions { + autostartButton?: IAutostartButtonOptions; + } +} + +export default AutostartButtonPlugin; + diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index 0e8041071..e0f3f526f 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -100,6 +100,38 @@ $sceneTabWidth: 450px; width: 1.6em; } + .vjs-autostart-button { + cursor: pointer; + + // use repeat/loop icon + &.vjs-icon-play-circle::before { + align-items: center; + background-color: rgba(80, 80, 80, 0.9); + border-radius: 50%; + color: #fff; + content: "\f101"; + font-size: 1em; + line-height: 1; + padding: 0.3em; + } + + &.vjs-icon-cancel::before { + align-items: center; + background-color: rgba(255, 255, 255, 0.9); + border-radius: 50%; + color: rgba(80, 80, 80, 0.9); + content: "\f103"; + font-size: 1em; + line-height: 1; + padding: 0.3em; + } + + + &:hover { + text-shadow: 0 0 1em rgba(255, 255, 255, 0.75); + } + + .vjs-touch-overlay .vjs-play-control { z-index: 1; }