Skip to content

Commit

Permalink
feat(player): add orientation request events
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Nov 1, 2023
1 parent 8510bae commit 2ebd688
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 65 deletions.
4 changes: 3 additions & 1 deletion packages/vidstack/src/core/api/media-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,9 @@ export interface MediaSuspendEvent extends MediaEvent<void> {}
/**
* Fired when a screen orientation change is requested on or by the media.
*/
export interface MediaOrientationChangeEvent extends ScreenOrientationChangeEvent {}
export interface MediaOrientationChangeEvent extends ScreenOrientationChangeEvent {
request?: RE.MediaOrientationLockRequestEvent | RE.MediaOrientationUnlockRequestEvent;
}

/**
* Fired when media playback starts again after being in an `ended` state. This is fired
Expand Down
20 changes: 20 additions & 0 deletions packages/vidstack/src/core/api/media-request-events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { DOMEvent } from 'maverick.js/std';

import type { ScreenOrientationLockType } from '../..';

export interface MediaRequestEvents {
'media-audio-track-change-request': MediaAudioTrackChangeRequestEvent;
'media-enter-fullscreen-request': MediaEnterFullscreenRequestEvent;
Expand All @@ -8,6 +10,8 @@ export interface MediaRequestEvents {
'media-exit-pip-request': MediaExitPIPRequestEvent;
'media-live-edge-request': MediaLiveEdgeRequestEvent;
'media-loop-request': MediaLoopRequestEvent;
'media-orientation-lock-request': MediaOrientationLockRequestEvent;
'media-orientation-unlock-request': MediaOrientationUnlockRequestEvent;
'media-mute-request': MediaMuteRequestEvent;
'media-pause-request': MediaPauseRequestEvent;
'media-pause-controls-request': MediaPauseControlsRequestEvent;
Expand Down Expand Up @@ -223,3 +227,19 @@ export interface MediaHidePosterRequestEvent extends DOMEvent<void> {}
* @composed
*/
export interface MediaLoopRequestEvent extends DOMEvent<void> {}

/**
* Fired when requesting the screen orientation to be locked to a certain type.
*
* @bubbles
* @composed
*/
export interface MediaOrientationLockRequestEvent extends DOMEvent<ScreenOrientationLockType> {}

/**
* Fired when requesting the screen orientation to be unlocked.
*
* @bubbles
* @composed
*/
export interface MediaOrientationUnlockRequestEvent extends DOMEvent<void> {}
2 changes: 0 additions & 2 deletions packages/vidstack/src/core/api/player-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { DOMEvent } from 'maverick.js/std';

import type { MediaPlayer } from '../../components';
import type { LoggerEvents } from '../../foundation/logger/events';
import type { ScreenOrientationEvents } from '../../foundation/orientation/events';
import type { HLSProviderEvents } from '../../providers/hls/events';
import type { VideoPresentationEvents } from '../../providers/video/presentation/events';
import type { MediaEvents } from './media-events';
Expand All @@ -12,7 +11,6 @@ export interface MediaPlayerEvents
extends MediaEvents,
MediaRequestEvents,
MediaUserEvents,
ScreenOrientationEvents,
LoggerEvents,
VideoPresentationEvents,
HLSProviderEvents {
Expand Down
10 changes: 9 additions & 1 deletion packages/vidstack/src/core/api/player-state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { State, tick, type Store } from 'maverick.js';

import type { LogLevel } from '../../foundation/logger/log-level';
import { canOrientScreen } from '../../utils/support';
import type { VideoQuality } from '../quality/video-quality';
import { getTimeRangesEnd, getTimeRangesStart, TimeRange } from '../time-ranges';
import type { AudioTrack } from '../tracks/audio-tracks';
Expand All @@ -24,6 +25,7 @@ export const mediaState = new State<MediaState>({
duration: 0,
canLoad: false,
canFullscreen: false,
canOrientScreen: canOrientScreen(),
canPictureInPicture: false,
canPlay: false,
controls: false,
Expand Down Expand Up @@ -242,14 +244,20 @@ export interface MediaState {
*/
duration: number;
/**
* Whether the native browser fullscreen API is available, or the current provider can
* Whether the native browser Fullscreen API is available, or the current provider can
* toggle fullscreen mode. This does not mean that the operation is guaranteed to be successful,
* only that it can be attempted.
*
* @defaultValue false
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API}
*/
canFullscreen: boolean;
/**
* Whether the native Screen Orientation API and required methods (lock/unlock) are available.
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API}
*/
canOrientScreen: boolean;
/**
* Whether picture-in-picture mode is supported by the current media provider.
*
Expand Down
90 changes: 61 additions & 29 deletions packages/vidstack/src/core/state/media-request-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { effect, peek, type ReadSignal } from 'maverick.js';
import { DOMEvent, isUndefined } from 'maverick.js/std';
import { isUndefined } from 'maverick.js/std';

import {
FullscreenController,
Expand Down Expand Up @@ -31,6 +31,7 @@ export interface MediaRequestQueueRecord {
rate: RE.MediaRateChangeRequestEvent;
volume: RE.MediaVolumeChangeRequestEvent | RE.MediaMuteRequestEvent | RE.MediaUnmuteRequestEvent;
fullscreen: RE.MediaEnterFullscreenRequestEvent | RE.MediaExitFullscreenRequestEvent;
orientation: RE.MediaOrientationLockRequestEvent | RE.MediaOrientationUnlockRequestEvent;
seeked: RE.MediaSeekRequestEvent | RE.MediaLiveEdgeRequestEvent;
seeking: RE.MediaSeekingRequestEvent;
textTrack: RE.MediaTextTrackChangeRequestEvent;
Expand Down Expand Up @@ -67,6 +68,10 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
this._orientation = new ScreenOrientationController();
}

protected override onAttach(): void {
this.listen('fullscreen-change', this._onFullscreenChange.bind(this));
}

protected override onConnect() {
const names = Object.getOwnPropertyNames(Object.getPrototypeOf(this)),
handle = this._handleRequest.bind(this);
Expand All @@ -77,8 +82,6 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
}
}

this.listen('fullscreen-change', this._onFullscreenChange.bind(this));

effect(this._onControlsDelayChange.bind(this));
effect(this._onFullscreenSupportChange.bind(this));
effect(this._onPiPSupportChange.bind(this));
Expand Down Expand Up @@ -183,12 +186,8 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
trigger?: Event,
) {
if (__SERVER__) return;
const provider = peek(this._provider);

const adapter =
(target === 'prefer-media' && this._fullscreen.supported) || target === 'media'
? this._fullscreen
: provider?.fullscreen;
const adapter = this._getFullscreenAdapter(target);

throwIfFullscreenNotSupported(target, adapter);

Expand Down Expand Up @@ -216,19 +215,13 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR

async _exitFullscreen(target: RE.MediaFullscreenRequestTarget = 'prefer-media', trigger?: Event) {
if (__SERVER__) return;
const provider = peek(this._provider);

const adapter =
(target === 'prefer-media' && this._fullscreen.supported) || target === 'media'
? this._fullscreen
: provider?.fullscreen;
const adapter = this._getFullscreenAdapter(target);

throwIfFullscreenNotSupported(target, adapter);

if (!adapter!.active) return;

if (this._orientation.locked) await this._orientation.unlock();

if (trigger?.type !== 'media-exit-fullscreen-request') {
this.dispatchEvent(
this.createEvent('media-exit-fullscreen-request', {
Expand All @@ -255,6 +248,13 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
}
}

private _getFullscreenAdapter(target: RE.MediaFullscreenRequestTarget) {
const provider = peek(this._provider);
return (target === 'prefer-media' && this._fullscreen.supported) || target === 'media'
? this._fullscreen
: provider?.fullscreen;
}

async _enterPictureInPicture(trigger?: Event) {
if (__SERVER__) return;

Expand Down Expand Up @@ -365,21 +365,21 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
}

private async _onFullscreenChange(event: MediaFullscreenChangeEvent) {
if (!event.detail) return;
try {
const lockType = peek(this.$props.fullscreenOrientation);
const lockType = peek(this.$props.fullscreenOrientation),
isFullscreen = event.detail;

if (this._orientation.supported && !isUndefined(lockType)) {
await this._orientation.lock(lockType);
}
} catch (error) {
if (__DEV__) {
this._media.logger
?.errorGroup('fullscreen orientation change failed')
.labelledLog('Event', event)
.labelledLog('Error', error)
.dispatch();
}
if (isUndefined(lockType) || !this._orientation.supported) return;

if (isFullscreen) {
if (this._orientation.locked) return;
this.dispatch('media-orientation-lock-request', {
detail: lockType,
trigger: event,
});
} else if (this._orientation.locked) {
this.dispatch('media-orientation-unlock-request', {
trigger: event,
});
}
}

Expand All @@ -399,6 +399,38 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
);
}

async ['media-orientation-lock-request'](event: RE.MediaOrientationLockRequestEvent) {
try {
this._request._queue._enqueue('orientation', event);
await this._orientation.lock(event.detail);
} catch (error) {
this._request._queue._delete('orientation');
if (__DEV__) {
this._media.logger
?.errorGroup('failed to lock screen orientation')
.labelledLog('Request Event', event)
.labelledLog('Error', error)
.dispatch();
}
}
}

async ['media-orientation-unlock-request'](event: RE.MediaOrientationUnlockRequestEvent) {
try {
this._request._queue._enqueue('orientation', event);
await this._orientation.unlock();
} catch (error) {
this._request._queue._delete('orientation');
if (__DEV__) {
this._media.logger
?.errorGroup('failed to unlock screen orientation')
.labelledLog('Request Event', event)
.labelledLog('Error', error)
.dispatch();
}
}
}

async ['media-enter-pip-request'](event: RE.MediaEnterPIPRequestEvent) {
try {
await this._enterPictureInPicture(event);
Expand Down
9 changes: 7 additions & 2 deletions packages/vidstack/src/core/state/media-state-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ export class MediaStateManager extends MediaPlayerController {

protected override onAttach(el: HTMLElement): void {
el.setAttribute('aria-busy', 'true');
this.listen('fullscreen-change', this['fullscreen-change'].bind(this));
this.listen('fullscreen-error', this['fullscreen-error'].bind(this));
this.listen('orientation-change', this['orientation-change'].bind(this));
}

protected override onConnect(el: HTMLElement) {
this._addTextTrackListeners();
this._addQualityListeners();
this._addAudioTrackListeners();
this.listen('fullscreen-change', this['fullscreen-change'].bind(this));
this.listen('fullscreen-error', this['fullscreen-error'].bind(this));
this._resumePlaybackOnConnect();
onDispose(this._pausePlaybackOnDisconnect.bind(this));
}
Expand Down Expand Up @@ -622,6 +623,10 @@ export class MediaStateManager extends MediaPlayerController {
this._satisfyRequest('fullscreen', event);
}

['orientation-change'](event: ME.MediaOrientationChangeEvent) {
this._satisfyRequest('orientation', event);
}

['picture-in-picture-change'](event: ME.MediaPIPChangeEvent) {
this.$state.pictureInPicture.set(event.detail);
this._satisfyRequest('pip', event);
Expand Down
33 changes: 26 additions & 7 deletions packages/vidstack/src/core/state/remote-control.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component } from 'maverick.js';
import { DOMEvent } from 'maverick.js/std';

import type { ScreenOrientationLockType } from '../..';
import type { MediaPlayer } from '../../components/player';
import { Logger } from '../../foundation/logger/controller';
import type { MediaFullscreenRequestTarget, MediaRequestEvents } from '../api/media-request-events';
Expand All @@ -9,7 +10,7 @@ import { isTrackCaptionKind } from '../tracks/text/text-track';
/**
* A simple facade for dispatching media requests to the nearest media player element.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/state#media-remote}
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/state#remote-control}
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/state#updating}
*
*/
Expand Down Expand Up @@ -107,7 +108,7 @@ export class MediaRemoteControl {
/**
* Dispatch a request to enter fullscreen.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/fullscreen#media-remote}
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/fullscreen#remote-control}
*/
enterFullscreen(target?: MediaFullscreenRequestTarget, trigger?: Event) {
this._dispatchRequest('media-enter-fullscreen-request', trigger, target);
Expand All @@ -116,16 +117,34 @@ export class MediaRemoteControl {
/**
* Dispatch a request to exit fullscreen.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/fullscreen#media-remote}
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/fullscreen#remote-control}
*/
exitFullscreen(target?: MediaFullscreenRequestTarget, trigger?: Event) {
this._dispatchRequest('media-exit-fullscreen-request', trigger, target);
}

/**
* Dispatch a request to lock the screen orientation.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/screen-orientation#remote-control}
*/
lockScreenOrientation(lockType: ScreenOrientationLockType, trigger?: Event) {
this._dispatchRequest('media-orientation-lock-request', trigger, lockType);
}

/**
* Dispatch a request to unlock the screen orientation.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/screen-orientation#remote-control}
*/
unlockScreenOrientation(trigger?: Event) {
this._dispatchRequest('media-orientation-unlock-request', trigger);
}

/**
* Dispatch a request to enter picture-in-picture mode.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/picture-in-picture#media-remote}
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/picture-in-picture#remote-control}
*/
enterPictureInPicture(trigger?: Event) {
this._dispatchRequest('media-enter-pip-request', trigger);
Expand All @@ -134,7 +153,7 @@ export class MediaRemoteControl {
/**
* Dispatch a request to exit picture-in-picture mode.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/picture-in-picture#media-remote}
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/picture-in-picture#remote-control}
*/
exitPictureInPicture(trigger?: Event) {
this._dispatchRequest('media-exit-pip-request', trigger);
Expand Down Expand Up @@ -319,7 +338,7 @@ export class MediaRemoteControl {
/**
* Dispatch a request to toggle the media fullscreen state.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/fullscreen#media-remote}
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/fullscreen#remote-control}
*/
toggleFullscreen(target?: MediaFullscreenRequestTarget, trigger?: Event) {
const player = this.getPlayer(trigger?.target);
Expand All @@ -336,7 +355,7 @@ export class MediaRemoteControl {
/**
* Dispatch a request to toggle the media picture-in-picture mode.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/picture-in-picture#media-remote}
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/picture-in-picture#remote-control}
*/
togglePictureInPicture(trigger?: Event) {
const player = this.getPlayer(trigger?.target);
Expand Down
Loading

0 comments on commit 2ebd688

Please sign in to comment.