Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/ten-snakes-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@theoplayer/react-ui': minor
'@theoplayer/web-ui': minor
---

Added 360°/VR support: `<theoplayer-vr-button>` and `<theoplayer-vr-compass>`, included by default in `<theoplayer-default-ui>`.
15 changes: 15 additions & 0 deletions examples/default-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
background: #000;
}
</style>
<!-- WebXR polyfill: enables Cardboard VR on desktop and older mobile devices. -->
<script src="https://cdn.theoplayer.com/webxr/webxr-polyfill-patched.js"></script>
<script>
new WebXRPolyfill({ allowCardboardOnDesktop: true });
</script>
<!-- Modern browsers -->
<script type="importmap">
{
Expand Down Expand Up @@ -69,6 +74,7 @@ <h1>Default UI</h1>
<option value="bigBuckBunny" selected>Big Buck Bunny</option>
<option value="elephantsDream">Elephant's Dream</option>
<option value="starWarsTrailer">Star Wars Episode VII Trailer</option>
<option value="vr">National Geographic (VR)</option>
</select>
</label>
</div>
Expand Down Expand Up @@ -116,6 +122,15 @@ <h1>Default UI</h1>
title: 'Star Wars Episode VII Trailer'
},
poster: 'https://cdn.theoplayer.com/video/star_wars_episode_vii-the_force_awakens_official_comic-con_2015_reel_(2015)/poster.jpg'
},
vr: {
sources: {
src: 'https://demo.theoplayer.com/hubfs/videos/natgeo/playlist.m3u8'
},
vr: {
360: true
},
poster: 'https://demo.theoplayer.com/hubfs/videos/natgeo/poster.jpg'
}
};

Expand Down
3 changes: 3 additions & 0 deletions examples/locale/nl.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ addLocale('nl', {
seekToLiveAria: 'spring naar live',
fullscreenAria: 'volledig scherm',
fullscreenExitAria: 'volledig scherm sluiten',
vrAria: 'bekijk in VR',
vrExitAria: 'stop met bekijken in VR',
vrUnavailableAria: 'geen VR-compatibel apparaat gevonden',
airplayAria: 'start met afspelen op AirPlay',
airplayConnectedAria: 'stop met afspelen op AirPlay',
chromecastAria: 'start met afspelen op Chromecast',
Expand Down
15 changes: 15 additions & 0 deletions examples/react/default-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
line-height: 24px;
}
</style>
<!-- WebXR polyfill: enables Cardboard VR on desktop and older mobile devices. -->
<script src="https://cdn.theoplayer.com/webxr/webxr-polyfill-patched.js"></script>
<script>
new WebXRPolyfill({ allowCardboardOnDesktop: true });
</script>
<script type="importmap">
{
"imports": {
Expand Down Expand Up @@ -108,6 +113,16 @@
title: 'Star Wars Episode VII Trailer'
},
poster: 'https://cdn.theoplayer.com/video/star_wars_episode_vii-the_force_awakens_official_comic-con_2015_reel_(2015)/poster.jpg'
},
vr: {
sources: {
src: 'https://demo.theoplayer.com/hubfs/videos/natgeo/playlist.m3u8'
},
metadata: { title: 'VR' },
vr: {
360: true
},
poster: 'https://demo.theoplayer.com/hubfs/videos/natgeo/poster.jpg'
}
};
const languages = {
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-swc3": "^0.12.1",
"serve": "^14.2.6",
"theoplayer": "^11.0.0",
"theoplayer": "^11.4.0",
"tslib": "^2.8.1",
"typedoc": "^0.28.19",
"typedoc-plugin-mdn-links": "^5.1.1",
Expand Down
17 changes: 17 additions & 0 deletions react/src/components/VRButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createComponent } from '@lit/react';
import { VRButton as VRButtonElement } from '@theoplayer/web-ui';
import * as React from 'react';
import { ButtonEvents } from './Button';

/**
* See {@link @theoplayer/web-ui!VRButton | VRButton in @theoplayer/web-ui}.
*
* @group Components
*/
export const VRButton = createComponent({
tagName: 'theoplayer-vr-button',
displayName: 'VRButton',
elementClass: VRButtonElement,
react: React,
events: ButtonEvents
});
15 changes: 15 additions & 0 deletions react/src/components/VRCompass.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createComponent } from '@lit/react';
import { VRCompass as VRCompassElement } from '@theoplayer/web-ui';
import * as React from 'react';

/**
* See {@link @theoplayer/web-ui!VRCompass | VRCompass in @theoplayer/web-ui}.
*
* @group Components
*/
export const VRCompass = createComponent({
tagName: 'theoplayer-vr-compass',
displayName: 'VRCompass',
elementClass: VRCompassElement,
react: React
});
2 changes: 2 additions & 0 deletions react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export * from './TextTrackStyleMenu';
export * from './SettingsMenu';
export * from './SettingsMenuButton';
export * from './FullscreenButton';
export * from './VRButton';
export * from './VRCompass';
export * from './Range';
export * from './TimeRange';
export * from './VolumeRange';
Expand Down
9 changes: 9 additions & 0 deletions src/DefaultUI.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ theoplayer-ui {
pointer-events: none;
}

/* Place the VR compass in the top-right corner by default (can be overridden). */
[part='middle-chrome'] theoplayer-vr-compass {
position: absolute;
top: 0;
right: 0;
z-index: 1;
margin: 0.5em;
}

[part='bottom-chrome'] {
position: relative;
display: flex;
Expand Down
2 changes: 2 additions & 0 deletions src/DefaultUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ export class DefaultUI extends LitElement {
</div>
<div slot="middle-chrome" part="middle-chrome">
<theoplayer-chromecast-display></theoplayer-chromecast-display>
<theoplayer-vr-compass></theoplayer-vr-compass>
</div>
<div part="bottom-chrome">
<theoplayer-control-bar part="ad-chrome" ad-only>
Expand Down Expand Up @@ -446,6 +447,7 @@ export class DefaultUI extends LitElement {
<theoplayer-airplay-button tv-hidden mobile-hidden ad-hidden></theoplayer-airplay-button>
<theoplayer-chromecast-button tv-hidden mobile-hidden ad-hidden></theoplayer-chromecast-button>
<slot name="bottom-control-bar"></slot>
<theoplayer-vr-button tv-hidden ad-hidden></theoplayer-vr-button>
<theoplayer-settings-menu-button menu="settings-menu" mobile-hidden ad-hidden></theoplayer-settings-menu-button>
<theoplayer-fullscreen-button part="fullscreen-button" tv-hidden></theoplayer-fullscreen-button>
</theoplayer-control-bar>
Expand Down
42 changes: 37 additions & 5 deletions src/UIContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { ENTER_FULLSCREEN_EVENT, type EnterFullscreenEvent } from './events/Ente
import { EXIT_FULLSCREEN_EVENT, type ExitFullscreenEvent } from './events/ExitFullscreenEvent';
import { fullscreenAPI } from './util/FullscreenUtils';
import { Attribute } from './util/Attribute';
import { isMobile, isTv } from './util/Environment';
import { isIOS, isMobile, isTv } from './util/Environment';
import { Rectangle } from './util/GeometryUtils';
import { PREVIEW_TIME_CHANGE_EVENT, type PreviewTimeChangeEvent } from './events/PreviewTimeChangeEvent';
import type { StreamType } from './util/StreamType';
Expand Down Expand Up @@ -840,6 +840,10 @@ export class UIContainer extends LitElement {
private readonly _onEnterFullscreen = (rawEvent: Event): void => {
const event = rawEvent as EnterFullscreenEvent;
event.stopPropagation();
this._enterFullscreen();
};

private _enterFullscreen(): void {
if (fullscreenAPI && document[fullscreenAPI.fullscreenEnabled_] && this[fullscreenAPI.requestFullscreen_]) {
const promise = this[fullscreenAPI.requestFullscreen_]({
navigationUI: 'hide',
Expand All @@ -856,11 +860,15 @@ export class UIContainer extends LitElement {
window.addEventListener('keydown', this._exitFullscreenOnEsc);
this._onFullscreenChange();
}
};
}

private readonly _onExitFullscreen = (rawEvent: Event): void => {
const event = rawEvent as ExitFullscreenEvent;
event.stopPropagation();
this._exitFullscreen();
};

private _exitFullscreen(): void {
if (fullscreenAPI) {
const promise = document[fullscreenAPI.exitFullscreen_]();
if (promise && promise.then) {
Expand All @@ -876,6 +884,31 @@ export class UIContainer extends LitElement {
window.removeEventListener('keydown', this._exitFullscreenOnEsc);
this._onFullscreenChange();
}
}

/**
* On iOS, presenting stereoscopic VR requires the player to fill the screen.
*/
private _vrFullscreen: boolean = false;
private _vrWasFullscreen: boolean = false;

private readonly _onVrStateChange = (): void => {
if (!isIOS()) {
return;
}
const presenting = this._player?.vr?.state === 'presenting';
if (presenting && !this._vrFullscreen) {
this._vrFullscreen = true;
this._vrWasFullscreen = this.fullscreen;
if (!this.fullscreen) {
this._enterFullscreen();
}
} else if (!presenting && this._vrFullscreen) {
this._vrFullscreen = false;
if (!this._vrWasFullscreen) {
this._exitFullscreen();
}
}
};

private readonly _onFullscreenChange = (): void => {
Expand Down Expand Up @@ -969,9 +1002,6 @@ export class UIContainer extends LitElement {
if (streamType) {
return streamType;
}
if (source?.dvr) {
return 'dvr';
}
// Assume VOD.
return 'vod';
} else if (duration === Infinity) {
Expand Down Expand Up @@ -1251,6 +1281,7 @@ export class UIContainer extends LitElement {
player.videoTracks.addEventListener(['addtrack', 'removetrack', 'change'], this._updateActiveVideoTrack);
player.cast?.addEventListener('castingchange', this._updateCasting);
player.ads?.addEventListener(['adbreakbegin', 'adbreakend', 'adbegin', 'adend', 'adskip'], this._updatePlayingAd);
player.vr?.addEventListener('statechange', this._onVrStateChange);
}

private removePlayerListeners_(player: ChromelessPlayer): void {
Expand All @@ -1271,6 +1302,7 @@ export class UIContainer extends LitElement {
player.videoTracks.removeEventListener(['addtrack', 'removetrack', 'change'], this._updateActiveVideoTrack);
player.cast?.removeEventListener('castingchange', this._updateCasting);
player.ads?.removeEventListener(['adbreakbegin', 'adbreakend', 'adbegin', 'adend', 'adskip'], this._updatePlayingAd);
player.vr?.removeEventListener('statechange', this._onVrStateChange);
} catch {
// Ignore errors from accessing player.ads when the player is already destroyed.
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/GestureReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export class GestureReceiver extends LitElement {
// Clicking during a linear ad should open the ad's clickthrough URL instead.
return;
}
if (this._player.vr && this._player.vr.state !== 'unavailable') {
// While VR is active, mouse drags are used to look around the 360° video,
// so a click should not toggle play/pause.
return;
}
if (this._player.paused) {
this._player.play();
} else {
Expand Down
4 changes: 4 additions & 0 deletions src/components/PlayButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ export class PlayButton extends Button {
} else {
this._player.pause();
}
const vr = this._player.vr;
if (vr !== undefined && vr.state !== 'unavailable') {
vr.useDeviceMotionControls = true;
}
Comment on lines +111 to +114

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a bit out of place... 😕

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I suppose our video.js UI is also doing this when the BigPlayButton is clicked. Maybe we should move this inside ChromelessPlayer.prepareWithUserAction() instead... 🤔

}
}

Expand Down
10 changes: 10 additions & 0 deletions src/components/VRButton.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* When no VR capable device is available, the button is disabled/greyed out.
*/
:host([disabled]) svg,
:host([disabled]) img,
:host([disabled]) ::slotted(svg),
:host([disabled]) ::slotted(img) {
color: var(--theoplayer-button-disabled-text-color, #ccc);
opacity: var(--theoplayer-vr-button-disabled-icon-opacity, 0.5);
}
Comment on lines +1 to +10

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move these styles to Button.css, so all disabled buttons will look like this? 🤔

Loading