From 88179ed54e56ba627a32bdc50078b60fef8e3393 Mon Sep 17 00:00:00 2001 From: CJ <72030708+Teda1@users.noreply.github.com> Date: Tue, 30 May 2023 20:04:38 -0500 Subject: [PATCH] Adds videojs-vr support (#3636) * Add button for VR mode * fix canvas disapearing * allow user to specify vr tag --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com> --- ui/v2.5/package.json | 1 + ui/v2.5/public/vr.svg | 5 + .../components/ScenePlayer/ScenePlayer.tsx | 19 ++- .../src/components/ScenePlayer/styles.scss | 11 ++ ui/v2.5/src/components/ScenePlayer/vrmode.ts | 146 ++++++++++++++++++ .../Scenes/SceneDetails/SceneEditPanel.tsx | 6 - .../SceneDetails/SceneVideoFilterPanel.tsx | 4 +- .../SettingsInterfacePanel.tsx | 7 + ui/v2.5/src/core/config.ts | 1 + ui/v2.5/src/locales/en-GB.json | 6 +- ui/v2.5/yarn.lock | 49 +++++- 11 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 ui/v2.5/public/vr.svg create mode 100644 ui/v2.5/src/components/ScenePlayer/vrmode.ts diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index c1ce15750..47356a9d6 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -70,6 +70,7 @@ "videojs-contrib-dash": "^5.1.1", "videojs-mobile-ui": "^0.8.0", "videojs-seek-buttons": "^3.0.1", + "videojs-vr": "^2.0.0", "videojs-vtt.js": "^0.15.4", "yup": "^1.0.0" }, diff --git a/ui/v2.5/public/vr.svg b/ui/v2.5/public/vr.svg new file mode 100644 index 000000000..2c6c29773 --- /dev/null +++ b/ui/v2.5/public/vr.svg @@ -0,0 +1,5 @@ + + diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index c553fa3cf..0eef94528 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -20,6 +20,7 @@ import "./markers"; import "./vtt-thumbnails"; import "./big-buttons"; import "./track-activity"; +import "./vrmode"; import cx from "classnames"; import { useSceneSaveActivity, @@ -213,6 +214,7 @@ export const ScenePlayer: React.FC = ({ const minimumPlayPercent = uiConfig?.minimumPlayPercent ?? 0; const trackActivity = uiConfig?.trackActivity ?? false; + const vrTag = uiConfig?.vrTag ?? undefined; const file = useMemo( () => ((scene?.files.length ?? 0) > 0 ? scene?.files[0] : undefined), @@ -265,6 +267,16 @@ 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, @@ -318,11 +330,15 @@ export const ScenePlayer: React.FC = ({ }, skipButtons: {}, trackActivity: {}, + vrMenu: { + showButton: isVrScene(), + }, }, }; const videoEl = document.createElement("video-js"); videoEl.setAttribute("data-vjs-player", "true"); + videoEl.setAttribute("crossorigin", "anonymous"); videoEl.classList.add("vjs-big-play-centered"); videoRef.current!.appendChild(videoEl); @@ -348,7 +364,7 @@ export const ScenePlayer: React.FC = ({ // reset sceneId to force reload sources sceneId.current = undefined; }; - }, []); + }, [scene, vrTag]); useEffect(() => { const player = getPlayer(); @@ -662,6 +678,7 @@ export const ScenePlayer: React.FC = ({ }, [ getPlayer, scene, + vrTag, trackActivity, minimumPlayPercent, sceneIncrementPlayCount, diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index c8bee39ea..63cc0bc3c 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -189,6 +189,17 @@ $sceneTabWidth: 450px; } } + .vjs-vr-selector { + .vjs-menu li { + font-size: 0.8em; + } + + .vjs-button { + background: url("/vr.svg") center center no-repeat; + width: 50%; + } + } + .vjs-marker { background-color: rgba(33, 33, 33, 0.8); bottom: 0; diff --git a/ui/v2.5/src/components/ScenePlayer/vrmode.ts b/ui/v2.5/src/components/ScenePlayer/vrmode.ts new file mode 100644 index 000000000..93459ab86 --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/vrmode.ts @@ -0,0 +1,146 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import videojs, { VideoJsPlayer } from "video.js"; +import "videojs-vr"; + +export interface VRMenuOptions { + /** + * Whether to show the vr button. + * @default false + */ + showButton?: boolean; +} + +enum VRType { + Spherical = "360", + Off = "Off", +} + +const vrTypeProjection = { + [VRType.Spherical]: "360", + [VRType.Off]: "NONE", +}; + +function isVrDevice() { + return navigator.userAgent.match(/oculusbrowser|\svr\s/i); +} + +class VRMenuItem extends videojs.getComponent("MenuItem") { + public type: VRType; + public isSelected = false; + + constructor(parent: VRMenuButton, type: VRType) { + const options = {} as videojs.MenuItemOptions; + options.selectable = true; + options.multiSelectable = false; + options.label = type; + + super(parent.player(), options); + + this.type = type; + + this.addClass("vjs-source-menu-item"); + } + + selected(selected: boolean): void { + super.selected(selected); + this.isSelected = selected; + } + + handleClick() { + if (this.isSelected) return; + + this.trigger("selected"); + } +} + +class VRMenuButton extends videojs.getComponent("MenuButton") { + private items: VRMenuItem[] = []; + private selectedType: VRType = VRType.Off; + + constructor(player: VideoJsPlayer) { + super(player); + this.setTypes(); + } + + private onSelected(item: VRMenuItem) { + this.selectedType = item.type; + + this.items.forEach((i) => { + i.selected(i.type === this.selectedType); + }); + + this.trigger("typeselected", item.type); + } + + public setTypes() { + this.items = Object.values(VRType).map((type) => { + const item = new VRMenuItem(this, type); + + item.on("selected", () => { + this.onSelected(item); + }); + + return item; + }); + this.update(); + } + + createEl() { + return videojs.dom.createEl("div", { + className: + "vjs-vr-selector vjs-menu-button vjs-menu-button-popup vjs-control vjs-button", + }); + } + + createItems() { + if (this.items === undefined) return []; + + for (const item of this.items) { + item.selected(item.type === this.selectedType); + } + + return this.items; + } +} + +class VRMenuPlugin extends videojs.getPlugin("plugin") { + private menu: VRMenuButton; + + constructor(player: VideoJsPlayer, options: VRMenuOptions) { + super(player); + + this.menu = new VRMenuButton(player); + + if (isVrDevice() || !options.showButton) return; + + this.menu.on("typeselected", (_, type: VRType) => { + const projection = vrTypeProjection[type]; + player.vr({ projection }); + player.load(); + }); + + player.on("ready", () => { + const { controlBar } = player; + const fullscreenToggle = controlBar.getChild("fullscreenToggle")!.el(); + controlBar.addChild(this.menu); + controlBar.el().insertBefore(this.menu.el(), fullscreenToggle); + }); + } +} + +// Register the plugin with video.js. +videojs.registerComponent("VRMenuButton", VRMenuButton); +videojs.registerPlugin("vrMenu", VRMenuPlugin); + +/* eslint-disable @typescript-eslint/naming-convention */ +declare module "video.js" { + interface VideoJsPlayer { + vrMenu: () => VRMenuPlugin; + vr: (options: Object) => void; + } + interface VideoJsPlayerPluginOptions { + vrMenu?: VRMenuOptions; + } +} + +export default VRMenuPlugin; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 840621e33..4e11ef6eb 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -743,7 +743,6 @@ export const SceneEditPanel: React.FC = ({ /> - {FormUtils.renderLabel({ title: intl.formatMessage({ id: "date" }), @@ -756,7 +755,6 @@ export const SceneEditPanel: React.FC = ({ /> - {renderTextField( "director", intl.formatMessage({ id: "director" }) @@ -790,7 +788,6 @@ export const SceneEditPanel: React.FC = ({ /> - {FormUtils.renderLabel({ title: intl.formatMessage({ id: "studio" }), @@ -811,7 +808,6 @@ export const SceneEditPanel: React.FC = ({ /> - {FormUtils.renderLabel({ title: intl.formatMessage({ id: "performers" }), @@ -834,7 +830,6 @@ export const SceneEditPanel: React.FC = ({ /> - {FormUtils.renderLabel({ title: `${intl.formatMessage({ @@ -857,7 +852,6 @@ export const SceneEditPanel: React.FC = ({ {renderTableMovies()} - {FormUtils.renderLabel({ title: intl.formatMessage({ id: "tags" }), diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx index f70451d4e..5de8b045a 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx @@ -111,7 +111,9 @@ export const SceneVideoFilterPanel: React.FC = ( function updateVideoStyle() { const playerVideoContainer = document.getElementById(VIDEO_PLAYER_ID); const videoElements = - playerVideoContainer?.getElementsByTagName("video") ?? []; + playerVideoContainer?.getElementsByTagName("canvas") ?? + playerVideoContainer?.getElementsByTagName("video") ?? + []; const playerVideoElement = videoElements.length > 0 ? videoElements[0] : null; diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 97866b4d4..c44f3ab78 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -290,6 +290,13 @@ export const SettingsInterfacePanel: React.FC = () => { checked={ui.trackActivity ?? undefined} onChange={(v) => saveUI({ trackActivity: v })} /> + saveUI({ vrTag: v })} + /> id="ignore-interval" headingID="config.ui.minimum_play_percent.heading" diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts index 8ca489bf3..90e11742c 100644 --- a/ui/v2.5/src/core/config.ts +++ b/ui/v2.5/src/core/config.ts @@ -59,6 +59,7 @@ export interface IUIConfig { lastNoteSeen?: number; + vrTag?: string; pinnedFilters?: PinnedFilters; } diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index d07278676..0eafd4bca 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -658,7 +658,11 @@ "heading": "Continue playlist by default" }, "show_scrubber": "Show Scrubber", - "track_activity": "Track Activity" + "track_activity": "Track Activity", + "vr_tag": { + "description": "The VR button will only be displayed for scenes with this tag.", + "heading": "VR Tag" + } } }, "scene_wall": { diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index 8725ca14e..3d150f8d1 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -1081,7 +1081,7 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@^7.8.4": +"@babel/runtime@^7.14.5", "@babel/runtime@^7.8.4": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== @@ -3211,6 +3211,15 @@ capital-case@^1.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" +cardboard-vr-display@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/cardboard-vr-display/-/cardboard-vr-display-1.0.19.tgz#81dcde1804b329b8228b757ac00e1fd2afa9d748" + integrity sha512-+MjcnWKAkb95p68elqZLDPzoiF/dGncQilLGvPBM5ZorABp/ao3lCs7nnRcYBckmuNkg1V/5rdGDKoUaCVsHzQ== + dependencies: + gl-preserve-state "^1.0.0" + nosleep.js "^0.7.0" + webvr-polyfill-dpdb "^1.0.17" + ccount@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" @@ -4455,6 +4464,11 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +gl-preserve-state@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz#4ef710d62873f1470ed015c6546c37dacddd4198" + integrity sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -6002,6 +6016,11 @@ normalize-url@^4.5.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== +nosleep.js@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/nosleep.js/-/nosleep.js-0.7.0.tgz#cfd919c25523ca0d0f4a69fb3305c083adaee289" + integrity sha512-Z4B1HgvzR+en62ghwZf6BwAR6x4/pjezsiMcbF9KMLh7xoscpoYhaSXfY3lLkqC68AtW+/qLJ1lzvBIj0FGaTA== + nullthrows@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" @@ -7471,6 +7490,11 @@ thehandy@^1.0.3: resolved "https://registry.yarnpkg.com/thehandy/-/thehandy-1.0.3.tgz#51c5e9bae5932a6e5c563203711d78610b99d402" integrity sha512-zuuyWKBx/jqku9+MZkdkoK2oLM2mS8byWVR/vkQYq/ygAT6gPAXwiT94rfGuqv+1BLmsyJxm69nhVIzOZjfyIg== +three@0.125.2: + version "0.125.2" + resolved "https://registry.yarnpkg.com/three/-/three-0.125.2.tgz#dcba12749a2eb41522e15212b919cd3fbf729b12" + integrity sha512-7rIRO23jVKWcAPFdW/HREU2NZMGWPBZ4XwEMt0Ak0jwLUKVJhcKM55eCBWyGZq/KiQbeo1IeuAoo/9l2dzhTXA== + throttle-debounce@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-5.0.0.tgz#a17a4039e82a2ed38a5e7268e4132d6960d41933" @@ -7975,6 +7999,17 @@ videojs-seek-buttons@^3.0.1: dependencies: global "^4.4.0" +videojs-vr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/videojs-vr/-/videojs-vr-2.0.0.tgz#3d86e3fececf7373cfb89b950ed6ab77ca783d2b" + integrity sha512-ix4iN8XHaDSEe89Jqybj9DuLKYuK33EIzcSI0IEdnv1KJuH8bd0PYlQEgqIZTOmWruFpW/+rjYFCVUQ9PTypJw== + dependencies: + "@babel/runtime" "^7.14.5" + global "^4.4.0" + three "0.125.2" + video.js "^6 || ^7" + webvr-polyfill "0.10.12" + videojs-vtt.js@^0.15.4: version "0.15.4" resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz#5dc5aabcd82ba40c5595469bd855ea8230ca152c" @@ -8052,6 +8087,18 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +webvr-polyfill-dpdb@^1.0.17: + version "1.0.18" + resolved "https://registry.yarnpkg.com/webvr-polyfill-dpdb/-/webvr-polyfill-dpdb-1.0.18.tgz#258484ce06b057bf18898acc911bd173847bce11" + integrity sha512-O0S1ZGEWyPvyZEkS2VbyV7mtir/NM9MNK3EuhbHPoJ8EHTky2pTXehjIl+IiDPr+Lldgx129QGt3NGly7rwRPw== + +webvr-polyfill@0.10.12: + version "0.10.12" + resolved "https://registry.yarnpkg.com/webvr-polyfill/-/webvr-polyfill-0.10.12.tgz#47ea0b0d558f09e089bc49fa7b47a4ee7e4b8148" + integrity sha512-trDJEVUQnRIVAnmImjEQ0BlL1NfuWl8+eaEdu+bs4g59c7OtETi/5tFkgEFDRaWEYwHntXs/uFF3OXZuutNGGA== + dependencies: + cardboard-vr-display "^1.0.19" + whatwg-fetch@^3.4.1: version "3.6.2" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"