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>
This commit is contained in:
CJ 2023-05-30 20:04:38 -05:00 committed by GitHub
parent d0847d1ebf
commit 88179ed54e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 245 additions and 10 deletions

View file

@ -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"
},

5
ui/v2.5/public/vr.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
<!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.
Modified from https://github.com/FortAwesome/Font-Awesome/blob/6.x/svgs/solid/vr-cardboard.svg
Changed fill style
--><path d="M576 64H64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H184.4c24.2 0 46.4-13.7 57.2-35.4l32-64c8.8-17.5 26.7-28.6 46.3-28.6s37.5 11.1 46.3 28.6l32 64c10.8 21.7 33 35.4 57.2 35.4H576c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64zM96 240a64 64 0 1 1 128 0A64 64 0 1 1 96 240zm384-64a64 64 0 1 1 0 128 64 64 0 1 1 0-128z" style="fill:#ffffff;fill-opacity:1"/></svg>

After

Width:  |  Height:  |  Size: 762 B

View file

@ -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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
// 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<IScenePlayerProps> = ({
},
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<IScenePlayerProps> = ({
// reset sceneId to force reload sources
sceneId.current = undefined;
};
}, []);
}, [scene, vrTag]);
useEffect(() => {
const player = getPlayer();
@ -662,6 +678,7 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
}, [
getPlayer,
scene,
vrTag,
trackActivity,
minimumPlayPercent,
sceneIncrementPlayCount,

View file

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

View file

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

View file

@ -743,7 +743,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
/>
</Col>
</Form.Group>
<Form.Group controlId="date" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "date" }),
@ -756,7 +755,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
/>
</Col>
</Form.Group>
{renderTextField(
"director",
intl.formatMessage({ id: "director" })
@ -790,7 +788,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
/>
</Col>
</Form.Group>
<Form.Group controlId="studio" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "studio" }),
@ -811,7 +808,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
/>
</Col>
</Form.Group>
<Form.Group controlId="performers" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "performers" }),
@ -834,7 +830,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
/>
</Col>
</Form.Group>
<Form.Group controlId="moviesScenes" as={Row}>
{FormUtils.renderLabel({
title: `${intl.formatMessage({
@ -857,7 +852,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
{renderTableMovies()}
</Col>
</Form.Group>
<Form.Group controlId="tags" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "tags" }),

View file

@ -111,7 +111,9 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
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;

View file

@ -290,6 +290,13 @@ export const SettingsInterfacePanel: React.FC = () => {
checked={ui.trackActivity ?? undefined}
onChange={(v) => saveUI({ trackActivity: v })}
/>
<StringSetting
id="vr-tag"
headingID="config.ui.scene_player.options.vr_tag.heading"
subHeadingID="config.ui.scene_player.options.vr_tag.description"
value={ui.vrTag ?? undefined}
onChange={(v) => saveUI({ vrTag: v })}
/>
<ModalSetting<number>
id="ignore-interval"
headingID="config.ui.minimum_play_percent.heading"

View file

@ -59,6 +59,7 @@ export interface IUIConfig {
lastNoteSeen?: number;
vrTag?: string;
pinnedFilters?: PinnedFilters;
}

View file

@ -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": {

View file

@ -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"