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"