diff --git a/ui/v2.5/src/@types/videojs-vr.d.ts b/ui/v2.5/src/@types/videojs-vr.d.ts new file mode 100644 index 000000000..54111718f --- /dev/null +++ b/ui/v2.5/src/@types/videojs-vr.d.ts @@ -0,0 +1,116 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +declare module "videojs-vr" { + import videojs from "video.js"; + + declare function videojsVR(options?: videojsVR.Options): videojsVR.Plugin; + + declare namespace videojsVR { + const VERSION: typeof videojs.VERSION; + + type ProjectionType = + // The video is half sphere and the user should not be able to look behind themselves + | "180" + // Used for side-by-side 180 videos The video is half sphere and the user should not be able to look behind themselves + | "180_LR" + // Used for monoscopic 180 videos The video is half sphere and the user should not be able to look behind themselves + | "180_MONO" + // The video is a sphere + | "360" + | "Sphere" + | "equirectangular" + // The video is a cube + | "360_CUBE" + | "Cube" + // This video is not a 360 video + | "NONE" + // Check player.mediainfo.projection to see if the current video is a 360 video. + | "AUTO" + // Used for side-by-side 360 videos + | "360_LR" + // Used for top-to-bottom 360 videos + | "360_TB" + // Used for Equi-Angular Cubemap videos + | "EAC" + // Used for side-by-side Equi-Angular Cubemap videos + | "EAC_LR"; + + interface Options { + /** + * Force the cardboard button to display on all devices even if we don't think they support it. + * + * @default false + */ + forceCardboard?: boolean; + + /** + * Whether motion/gyro controls should be enabled. + * + * @default true on iOS and Android + */ + motionControls?: boolean; + + /** + * Defines the projection type. + * + * @default "AUTO" + */ + projection?: ProjectionType; + + /** + * This alters the number of segments in the spherical mesh onto which equirectangular videos are projected. + * The default is 32 but in some circumstances you may notice artifacts and need to increase this number. + * + * @default 32 + */ + sphereDetail?: number; + + /** + * Enable debug logging for this plugin + * + * @default false + */ + debug?: boolean; + + /** + * Use this property to pass the Omnitone library object to the plugin. Please be aware of, the Omnitone library is not included in the build files. + */ + omnitone?: object; + + /** + * Default options for the Omnitone library. Please check available options on https://github.com/GoogleChrome/omnitone + */ + omnitoneOptions?: object; + + /** + * Feature to disable the togglePlay manually. This functionality is useful in live events so that users cannot stop the live, but still have a controlBar available. + * + * @default false + */ + disableTogglePlay?: boolean; + } + + interface PlayerMediaInfo { + /** + * This should be set on a source-by-source basis to turn 360 videos on an off depending upon the video. + * Note that AUTO is the same as NONE for player.mediainfo.projection. + */ + projection?: ProjectionType; + } + + class Plugin extends videojs.Plugin { + setProjection(projection: ProjectionType): void; + init(): void; + reset(): void; + } + } + + export = videojsVR; + + declare module "video.js" { + interface VideoJsPlayer { + vr: typeof videojsVR; + mediainfo?: videojsVR.PlayerMediaInfo; + } + } +} diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 0eef94528..b4699e454 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -267,16 +267,6 @@ export const ScenePlayer: React.FC = ({ // Initialize VideoJS player useEffect(() => { - function isVrScene() { - if (!scene?.id || !vrTag) return false; - - return scene?.tags.some((tag) => { - if (vrTag == tag.name) { - return true; - } - }); - } - const options: VideoJsPlayerOptions = { id: VIDEO_PLAYER_ID, controls: true, @@ -330,9 +320,7 @@ export const ScenePlayer: React.FC = ({ }, skipButtons: {}, trackActivity: {}, - vrMenu: { - showButton: isVrScene(), - }, + vrMenu: {}, }, }; @@ -364,7 +352,8 @@ export const ScenePlayer: React.FC = ({ // reset sceneId to force reload sources sceneId.current = undefined; }; - }, [scene, vrTag]); + // empty deps - only init once + }, []); useEffect(() => { const player = getPlayer(); @@ -388,6 +377,21 @@ export const ScenePlayer: React.FC = ({ scene?.paths.funscript, ]); + useEffect(() => { + const player = getPlayer(); + if (!player) return; + + const vrMenu = player.vrMenu(); + + let showButton = false; + + if (scene && vrTag) { + showButton = scene.tags.some((tag) => vrTag === tag.name); + } + + vrMenu.setShowButton(showButton); + }, [getPlayer, scene, vrTag]); + // Player event handlers useEffect(() => { const player = getPlayer(); diff --git a/ui/v2.5/src/components/ScenePlayer/vrmode.ts b/ui/v2.5/src/components/ScenePlayer/vrmode.ts index 93459ab86..b11be3364 100644 --- a/ui/v2.5/src/components/ScenePlayer/vrmode.ts +++ b/ui/v2.5/src/components/ScenePlayer/vrmode.ts @@ -1,6 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import videojs, { VideoJsPlayer } from "video.js"; import "videojs-vr"; +// separate type import, otherwise typescript elides the above import +// and the plugin does not get initialized +import type { ProjectionType, Plugin as VideoJsVRPlugin } from "videojs-vr"; export interface VRMenuOptions { /** @@ -15,7 +18,7 @@ enum VRType { Off = "Off", } -const vrTypeProjection = { +const vrTypeProjection: Record = { [VRType.Spherical]: "360", [VRType.Off]: "NONE", }; @@ -29,7 +32,7 @@ class VRMenuItem extends videojs.getComponent("MenuItem") { public isSelected = false; constructor(parent: VRMenuButton, type: VRType) { - const options = {} as videojs.MenuItemOptions; + const options: videojs.MenuItemOptions = {}; options.selectable = true; options.multiSelectable = false; options.label = type; @@ -105,27 +108,61 @@ class VRMenuButton extends videojs.getComponent("MenuButton") { class VRMenuPlugin extends videojs.getPlugin("plugin") { private menu: VRMenuButton; + private showButton: boolean; + private vr?: VideoJsVRPlugin; constructor(player: VideoJsPlayer, options: VRMenuOptions) { super(player); this.menu = new VRMenuButton(player); + this.showButton = options.showButton ?? false; - if (isVrDevice() || !options.showButton) return; + if (isVrDevice()) return; + + this.vr = this.player.vr(); this.menu.on("typeselected", (_, type: VRType) => { - const projection = vrTypeProjection[type]; - player.vr({ projection }); - player.load(); + this.loadVR(type); }); player.on("ready", () => { - const { controlBar } = player; - const fullscreenToggle = controlBar.getChild("fullscreenToggle")!.el(); - controlBar.addChild(this.menu); - controlBar.el().insertBefore(this.menu.el(), fullscreenToggle); + if (this.showButton) { + this.addButton(); + } }); } + + private loadVR(type: VRType) { + const projection = vrTypeProjection[type]; + this.vr?.setProjection(projection); + this.vr?.init(); + } + + private addButton() { + const { controlBar } = this.player; + const fullscreenToggle = controlBar.getChild("fullscreenToggle")!.el(); + controlBar.addChild(this.menu); + controlBar.el().insertBefore(this.menu.el(), fullscreenToggle); + } + + private removeButton() { + const { controlBar } = this.player; + controlBar.removeChild(this.menu); + } + + public setShowButton(showButton: boolean) { + if (isVrDevice()) return; + + if (showButton === this.showButton) return; + + this.showButton = showButton; + if (showButton) { + this.addButton(); + } else { + this.removeButton(); + this.loadVR(VRType.Off); + } + } } // Register the plugin with video.js. @@ -136,7 +173,6 @@ videojs.registerPlugin("vrMenu", VRMenuPlugin); declare module "video.js" { interface VideoJsPlayer { vrMenu: () => VRMenuPlugin; - vr: (options: Object) => void; } interface VideoJsPlayerPluginOptions { vrMenu?: VRMenuOptions;