Skip to content

Commit 884827a

Browse files
committed
MOBILE-4166 videojs: Support fullscreen and improve types
1 parent 9b011ba commit 884827a

File tree

11 files changed

+414
-160
lines changed

11 files changed

+414
-160
lines changed

src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts

Lines changed: 59 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import { CorePlatform } from '@services/platform';
1616
import { OGVPlayer, OGVCompat, OGVLoader } from 'ogv';
17-
import videojs from 'video.js';
17+
import videojs, { PreloadOption, TechSourceObject, VideoJSOptions } from 'video.js';
1818

1919
export const Tech = videojs.getComponent('Tech');
2020

@@ -95,6 +95,10 @@ export class VideoJSOgvJS extends Tech {
9595
'volumechange',
9696
];
9797

98+
protected playerId?: string;
99+
protected parentElement: HTMLElement | null = null;
100+
protected placeholderElement = document.createElement('div');
101+
98102
// Variables/functions defined in parent classes.
99103
protected el_!: OGVPlayerEl; // eslint-disable-line @typescript-eslint/naming-convention
100104
protected options_!: VideoJSOptions; // eslint-disable-line @typescript-eslint/naming-convention
@@ -108,14 +112,15 @@ export class VideoJSOgvJS extends Tech {
108112
* @param options The key/value store of player options.
109113
* @param ready Callback function to call when the `OgvJS` Tech is ready.
110114
*/
111-
constructor(options: VideoJSOptions, ready: () => void) {
115+
constructor(options: VideoJSTechOptions, ready: () => void) {
112116
super(options, ready);
113117

114118
this.el_.src = options.src || options.source?.src || options.sources?.[0]?.src || this.el_.src;
115119
VideoJSOgvJS.setIfAvailable(this.el_, 'autoplay', options.autoplay);
116120
VideoJSOgvJS.setIfAvailable(this.el_, 'loop', options.loop);
117121
VideoJSOgvJS.setIfAvailable(this.el_, 'poster', options.poster);
118122
VideoJSOgvJS.setIfAvailable(this.el_, 'preload', options.preload);
123+
this.playerId = options.playerId;
119124

120125
this.on('loadedmetadata', () => {
121126
if (CoreApp.isIPhone()) {
@@ -654,8 +659,7 @@ export class VideoJSOgvJS extends Tech {
654659
* @returns Whether it supports full screen.
655660
*/
656661
supportsFullScreen(): boolean {
657-
// iOS devices have some problem with HTML5 fullscreen api so we need to fallback to fullWindow mode.
658-
return !CoreApp.isIOS();
662+
return !!this.playerId;
659663
}
660664

661665
/**
@@ -667,6 +671,50 @@ export class VideoJSOgvJS extends Tech {
667671
return this.el_.error;
668672
}
669673

674+
/**
675+
* Enter full screen mode.
676+
*/
677+
enterFullScreen(): void {
678+
// Use a "fake" full screen mode, moving the player to a different place in DOM to be able to use full screen size.
679+
const player = videojs.getPlayer(this.playerId ?? '');
680+
if (!player) {
681+
return;
682+
}
683+
684+
const container = player.el();
685+
this.parentElement = container.parentElement;
686+
if (!this.parentElement) {
687+
// Shouldn't happen, it means the element is not in DOM. Do not support full screen in this case.
688+
return;
689+
}
690+
691+
this.parentElement.replaceChild(this.placeholderElement, container);
692+
document.body.appendChild(container);
693+
container.classList.add('vjs-ios-moodleapp-fs');
694+
695+
player.isFullscreen(true);
696+
}
697+
698+
/**
699+
* Exit full screen mode.
700+
*/
701+
exitFullScreen(): void {
702+
if (!this.parentElement) {
703+
return;
704+
}
705+
706+
const player = videojs.getPlayer(this.playerId ?? '');
707+
if (!player) {
708+
return;
709+
}
710+
711+
const container = player.el();
712+
this.parentElement.replaceChild(container, this.placeholderElement);
713+
container.classList.remove('vjs-ios-moodleapp-fs');
714+
715+
player.isFullscreen(false);
716+
}
717+
670718
}
671719

672720
[
@@ -688,75 +736,13 @@ export const initializeVideoJSOgvJS = (): void => {
688736
Tech.registerTech('OgvJS', VideoJSOgvJS);
689737
};
690738

691-
export type VideoJSOptions = {
692-
aspectRatio?: string;
693-
audioOnlyMode?: boolean;
694-
audioPosterMode?: boolean;
695-
autoplay?: boolean | string;
696-
autoSetup?: boolean;
697-
base?: string;
698-
breakpoints?: Record<string, number>;
699-
children?: string[] | Record<string, Record<string, unknown>>;
700-
controlBar?: {
701-
remainingTimeDisplay?: {
702-
displayNegative?: boolean;
703-
};
704-
};
705-
controls?: boolean;
706-
fluid?: boolean;
707-
fullscreen?: {
708-
options?: Record<string, unknown>;
709-
};
710-
height?: string | number;
711-
id?: string;
712-
inactivityTimeout?: number;
713-
language?: string;
714-
languages?: Record<string, Record<string, string>>;
715-
liveui?: boolean;
716-
liveTracker?: {
717-
trackingThreshold?: number;
718-
liveTolerance?: number;
719-
};
720-
loop?: boolean;
721-
muted?: boolean;
722-
nativeControlsForTouch?: boolean;
723-
normalizeAutoplay?: boolean;
724-
notSupportedMessage?: string;
725-
noUITitleAttributes?: boolean;
726-
playbackRates?: number[];
727-
plugins?: Record<string, Record<string, unknown>>;
728-
poster?: string;
729-
preferFullWindow?: boolean;
730-
preload?: PreloadOption;
731-
responsive?: boolean;
732-
restoreEl?: boolean | HTMLElement;
733-
source?: TechSourceObject;
734-
sources?: TechSourceObject[];
735-
src?: string;
736-
suppressNotSupportedError?: boolean;
737-
tag?: HTMLElement;
738-
techCanOverridePoster?: boolean;
739-
techOrder?: string[];
740-
userActions?: {
741-
click?: boolean | ((ev: MouseEvent) => void);
742-
doubleClick?: boolean | ((ev: MouseEvent) => void);
743-
hotkeys?: boolean | ((ev: KeyboardEvent) => void) | {
744-
fullscreenKey?: (ev: KeyboardEvent) => void;
745-
muteKey?: (ev: KeyboardEvent) => void;
746-
playPauseKey?: (ev: KeyboardEvent) => void;
747-
};
748-
};
749-
'vtt.js'?: string;
750-
width?: string | number;
751-
};
752-
753-
type TechSourceObject = {
754-
src: string; // Source URL.
755-
type: string; // Mimetype.
756-
};
757-
758-
type PreloadOption = '' | 'none' | 'metadata' | 'auto';
759-
760739
type OGVPlayerEl = (HTMLAudioElement | HTMLVideoElement) & {
761740
stop: () => void;
762741
};
742+
743+
/**
744+
* VideoJS Tech options. It includes some options added by VideoJS internally.
745+
*/
746+
type VideoJSTechOptions = VideoJSOptions & {
747+
playerId?: string;
748+
};

src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ import { CoreTextUtils } from '@services/utils/text';
2121
import { CoreUrlUtils } from '@services/utils/url';
2222
import { makeSingleton } from '@singletons';
2323
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
24-
import { CoreDom } from '@singletons/dom';
2524
import { CoreEvents } from '@singletons/events';
26-
import videojs from 'video.js';
27-
import { VideoJSOptions } from '../../classes/videojs-ogvjs';
25+
import { CoreMedia } from '@singletons/media';
26+
import videojs, { VideoJSOptions, VideoJSPlayer } from 'video.js';
2827

2928
/**
3029
* Handler to support the Multimedia filter.
@@ -48,7 +47,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
4847
const videos = Array.from(this.template.content.querySelectorAll('video'));
4948

5049
videos.forEach((video) => {
51-
this.treatVideoFilters(video);
50+
this.treatYoutubeVideos(video);
5251
});
5352

5453
return this.template.innerHTML;
@@ -61,7 +60,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
6160
const mediaElements = Array.from(container.querySelectorAll<HTMLVideoElement | HTMLAudioElement>('video, audio'));
6261

6362
mediaElements.forEach((mediaElement) => {
64-
if (CoreDom.mediaUsesJavascriptPlayer(mediaElement)) {
63+
if (CoreMedia.mediaUsesJavascriptPlayer(mediaElement)) {
6564
this.useVideoJS(mediaElement);
6665
} else {
6766
// Remove the VideoJS classes and data if present.
@@ -93,28 +92,49 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
9392
controls: true,
9493
techOrder: ['OgvJS'],
9594
language: lang,
96-
fluid: true,
9795
controlBar: {
98-
fullscreenToggle: false,
96+
pictureInPictureToggle: false,
9997
},
10098
aspectRatio: data.aspectRatio,
99+
}, () => {
100+
if (mediaElement.tagName === 'VIDEO') {
101+
this.fixVideoJSPlayerSize(player);
102+
}
101103
});
102104

103105
CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, {
104106
id: mediaElement.id,
105107
element: mediaElement,
106108
player,
107109
});
110+
}
108111

112+
/**
113+
* Fix VideoJS player size.
114+
* If video width is wider than available width, video is cut off. Fix the dimensions in this case.
115+
*
116+
* @param player Player instance.
117+
*/
118+
protected fixVideoJSPlayerSize(player: VideoJSPlayer): void {
119+
const videoWidth = player.videoWidth();
120+
const videoHeight = player.videoHeight();
121+
const playerDimensions = player.currentDimensions();
122+
if (!videoWidth || !videoHeight || !playerDimensions.width || videoWidth === playerDimensions.width) {
123+
return;
124+
}
125+
126+
const candidateHeight = playerDimensions.width * videoHeight / videoWidth;
127+
if (!playerDimensions.height || Math.abs(candidateHeight - playerDimensions.height) > 1) {
128+
player.dimension('height', candidateHeight);
129+
}
109130
}
110131

111132
/**
112-
* Treat video filters. Currently only treating youtube video using video JS.
133+
* Treat Video JS Youtube video links and translate them to iframes.
113134
*
114135
* @param video Video element.
115136
*/
116-
protected treatVideoFilters(video: HTMLElement): void {
117-
// Treat Video JS Youtube video links and translate them to iframes.
137+
protected treatYoutubeVideos(video: HTMLElement): void {
118138
if (!video.classList.contains('video-js')) {
119139
return;
120140
}

src/core/classes/element-controllers/MediaElementController.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414

1515
import { CoreUtils } from '@services/utils/utils';
1616
import { ElementController } from './ElementController';
17-
import videojs from 'video.js';
18-
import { CoreDom } from '@singletons/dom';
17+
import videojs, { VideoJSPlayer } from 'video.js';
1918
import { CorePromisedValue } from '@classes/promised-value';
2019
import { CoreEventObserver, CoreEvents } from '@singletons/events';
20+
import { CoreMedia } from '@singletons/media';
2121

2222
/**
2323
* Wrapper class to control the interactivity of a media element.
@@ -42,15 +42,15 @@ export class MediaElementController extends ElementController {
4242

4343
media.autoplay = false;
4444

45-
if (CoreDom.mediaUsesJavascriptPlayer(media)) {
45+
if (CoreMedia.mediaUsesJavascriptPlayer(media)) {
4646
const player = this.searchJSPlayer();
4747
if (player) {
4848
this.jsPlayer.resolve(player);
4949
} else {
5050
this.jsPlayerListener = CoreEvents.on(CoreEvents.JS_PLAYER_CREATED, data => {
5151
if (data.element === media) {
5252
this.jsPlayerListener?.off();
53-
this.jsPlayer.resolve(data.player as VideoJSPlayer);
53+
this.jsPlayer.resolve(data.player);
5454
}
5555
});
5656
}
@@ -153,16 +153,8 @@ export class MediaElementController extends ElementController {
153153
*
154154
* @returns Player instance if found.
155155
*/
156-
private searchJSPlayer(): VideoJSPlayer | undefined {
156+
private searchJSPlayer(): VideoJSPlayer | null {
157157
return videojs.getPlayer(this.media.id) || videojs.getPlayer(this.media.id.replace('_html5_api', ''));
158158
}
159159

160160
}
161-
162-
type VideoJSPlayer = {
163-
play: () => Promise<void>;
164-
pause: () => Promise<void>;
165-
on: (name: string, callback: (ev: Event) => void) => void;
166-
off: (name: string, callback: (ev: Event) => void) => void;
167-
dispose: () => void;
168-
};

src/core/services/utils/url.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { CoreUrl } from '@singletons/url';
2222
import { CoreSites } from '@services/sites';
2323
import { CorePath } from '@singletons/path';
2424
import { CorePlatform } from '@services/platform';
25-
import { CoreDom } from '@singletons/dom';
25+
import { CoreMedia } from '@singletons/media';
2626

2727
/*
2828
* "Utils" service with helper functions for URLs.
@@ -122,7 +122,7 @@ export class CoreUrlUtilsProvider {
122122
return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && (
123123
url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 ||
124124
url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) &&
125-
!CoreDom.sourceUsesJavascriptPlayer({ src: url });
125+
!CoreMedia.sourceUsesJavascriptPlayer({ src: url });
126126
}
127127

128128
/**

src/core/singletons/dom.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import { CoreCancellablePromise } from '@classes/cancellable-promise';
1616
import { CoreApp } from '@services/app';
1717
import { CoreDomUtils } from '@services/utils/dom';
18-
import { CoreMimetypeUtils } from '@services/utils/mimetype';
1918
import { CoreUtils } from '@services/utils/utils';
2019
import { CoreEventObserver } from '@singletons/events';
2120

@@ -569,6 +568,7 @@ export class CoreDom {
569568
}
570569
}
571570

571+
<<<<<<< HEAD
572572
/**
573573
* Get all source URLs and types for a video or audio.
574574
*
@@ -637,6 +637,8 @@ export class CoreDom {
637637
return sources.some(source => CoreDom.sourceUsesJavascriptPlayer(source));
638638
}
639639

640+
=======
641+
>>>>>>> f42ea632ca (a)
640642
}
641643

642644
/**

src/core/singletons/events.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool';
2020
import { CoreRedirectPayload } from '@services/navigator';
2121
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
2222
import { CoreScreenOrientation } from '@services/screen';
23+
import { VideoJSPlayer } from 'video.js';
2324

2425
/**
2526
* Observer instance to stop listening to an event.
@@ -499,5 +500,5 @@ export type CoreEventCompleteRequiredProfileDataFinished = {
499500
export type CoreEventJSVideoPlayerCreated = {
500501
id: string;
501502
element: HTMLAudioElement | HTMLVideoElement;
502-
player: unknown;
503+
player: VideoJSPlayer;
503504
};

0 commit comments

Comments
 (0)