Skip to content

Commit b7db250

Browse files
authored
Merge pull request #3559 from NoelDeMartin/MOBILE-4239
MOBILE-4239 mediaplugin: Lazy load videojs
2 parents 4cb9a66 + 8ee614a commit b7db250

File tree

9 files changed

+263
-151
lines changed

9 files changed

+263
-151
lines changed

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { CorePlatform } from '@services/platform';
1616
import { OGVPlayer, OGVCompat, OGVLoader } from 'ogv';
1717
import videojs, { PreloadOption, TechSourceObject, VideoJSOptions } from 'video.js';
1818

19-
export const Tech = videojs.getComponent('Tech');
19+
const Tech = videojs.getComponent('Tech');
2020

2121
/**
2222
* Object.defineProperty but "lazy", which means that the value is only set after
@@ -728,13 +728,6 @@ export class VideoJSOgvJS extends Tech {
728728
].forEach(([key, fn]) => {
729729
defineLazyProperty(VideoJSOgvJS.prototype, key, () => VideoJSOgvJS[fn](), true);
730730
});
731-
/**
732-
* Initialize the controller.
733-
*/
734-
export const initializeVideoJSOgvJS = (): void => {
735-
OGVLoader.base = 'assets/lib/ogv';
736-
Tech.registerTech('OgvJS', VideoJSOgvJS);
737-
};
738731

739732
type OGVPlayerEl = (HTMLAudioElement | HTMLVideoElement) & {
740733
stop: () => void;

src/addons/filter/mediaplugin/mediaplugin.module.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import { APP_INITIALIZER, NgModule } from '@angular/core';
1616

1717
import { CoreFilterDelegate } from '@features/filter/services/filter-delegate';
18-
import { initializeVideoJSOgvJS } from './classes/videojs-ogvjs';
1918
import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin';
2019

2120
@NgModule({
@@ -29,8 +28,6 @@ import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin';
2928
multi: true,
3029
useValue: () => {
3130
CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance);
32-
33-
initializeVideoJSOgvJS();
3431
},
3532
},
3633
],

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

Lines changed: 9 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,12 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import { AddonFilterMediaPluginVideoJS } from '@addons/filter/mediaplugin/services/videojs';
1516
import { Injectable } from '@angular/core';
16-
import { CoreExternalContentDirective } from '@directives/external-content';
1717

1818
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
19-
import { CoreLang } from '@services/lang';
20-
import { CoreTextUtils } from '@services/utils/text';
21-
import { CoreUrlUtils } from '@services/utils/url';
2219
import { makeSingleton } from '@singletons';
23-
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
24-
import { CoreEvents } from '@singletons/events';
2520
import { CoreMedia } from '@singletons/media';
26-
import videojs, { VideoJSOptions, VideoJSPlayer } from 'video.js';
2721

2822
/**
2923
* Handler to support the Multimedia filter.
@@ -39,15 +33,13 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
3933
/**
4034
* @inheritdoc
4135
*/
42-
filter(
43-
text: string,
44-
): string | Promise<string> {
36+
filter(text: string): string | Promise<string> {
4537
this.template.innerHTML = text;
4638

4739
const videos = Array.from(this.template.content.querySelectorAll('video'));
4840

4941
videos.forEach((video) => {
50-
this.treatYoutubeVideos(video);
42+
AddonFilterMediaPluginVideoJS.treatYoutubeVideos(video);
5143
});
5244

5345
return this.template.innerHTML;
@@ -61,104 +53,18 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
6153

6254
mediaElements.forEach((mediaElement) => {
6355
if (CoreMedia.mediaUsesJavascriptPlayer(mediaElement)) {
64-
this.useVideoJS(mediaElement);
65-
} else {
66-
// Remove the VideoJS classes and data if present.
67-
mediaElement.classList.remove('video-js');
68-
mediaElement.removeAttribute('data-setup');
69-
mediaElement.removeAttribute('data-setup-lazy');
70-
}
71-
});
72-
}
73-
74-
/**
75-
* Use video JS in a certain video or audio.
76-
*
77-
* @param mediaElement Media element.
78-
*/
79-
protected async useVideoJS(mediaElement: HTMLVideoElement | HTMLAudioElement): Promise<void> {
80-
const lang = await CoreLang.getCurrentLanguage();
56+
AddonFilterMediaPluginVideoJS.createPlayer(mediaElement);
8157

82-
// Wait for external-content to finish in the element and its sources.
83-
await Promise.all([
84-
CoreDirectivesRegistry.waitDirectivesReady(mediaElement, undefined, CoreExternalContentDirective),
85-
CoreDirectivesRegistry.waitDirectivesReady(mediaElement, 'source', CoreExternalContentDirective),
86-
]);
87-
88-
const dataSetupString = mediaElement.getAttribute('data-setup') || mediaElement.getAttribute('data-setup-lazy') || '{}';
89-
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
90-
91-
const player = videojs(mediaElement, {
92-
controls: true,
93-
techOrder: ['OgvJS'],
94-
language: lang,
95-
controlBar: {
96-
pictureInPictureToggle: false,
97-
},
98-
aspectRatio: data.aspectRatio,
99-
}, () => {
100-
if (mediaElement.tagName === 'VIDEO') {
101-
this.fixVideoJSPlayerSize(player);
58+
return;
10259
}
103-
});
10460

105-
CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, {
106-
id: mediaElement.id,
107-
element: mediaElement,
108-
player,
61+
// Remove the VideoJS classes and data if present.
62+
mediaElement.classList.remove('video-js');
63+
mediaElement.removeAttribute('data-setup');
64+
mediaElement.removeAttribute('data-setup-lazy');
10965
});
11066
}
11167

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-
}
130-
}
131-
132-
/**
133-
* Treat Video JS Youtube video links and translate them to iframes.
134-
*
135-
* @param video Video element.
136-
*/
137-
protected treatYoutubeVideos(video: HTMLElement): void {
138-
if (!video.classList.contains('video-js')) {
139-
return;
140-
}
141-
142-
const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}';
143-
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
144-
const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src);
145-
146-
if (!youtubeUrl) {
147-
return;
148-
}
149-
150-
const iframe = document.createElement('iframe');
151-
iframe.id = video.id;
152-
iframe.src = youtubeUrl;
153-
iframe.setAttribute('frameborder', '0');
154-
iframe.setAttribute('allowfullscreen', '1');
155-
iframe.width = '100%';
156-
iframe.height = '300';
157-
158-
// Replace video tag by the iframe.
159-
video.parentNode?.replaceChild(iframe, video);
160-
}
161-
16268
}
16369

16470
export const AddonFilterMediaPluginHandler = makeSingleton(AddonFilterMediaPluginHandlerService);
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// (C) Copyright 2015 Moodle Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { Injectable } from '@angular/core';
16+
import { CorePromisedValue } from '@classes/promised-value';
17+
import { CoreExternalContentDirective } from '@directives/external-content';
18+
import { CoreLang } from '@services/lang';
19+
import { CoreTextUtils } from '@services/utils/text';
20+
import { CoreUrlUtils } from '@services/utils/url';
21+
import { makeSingleton } from '@singletons';
22+
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
23+
import { CoreEvents } from '@singletons/events';
24+
import type videojs from 'video.js';
25+
26+
// eslint-disable-next-line no-duplicate-imports
27+
import type { VideoJSOptions, VideoJSPlayer } from 'video.js';
28+
29+
declare module '@singletons/events' {
30+
31+
/**
32+
* Augment CoreEventsData interface with events specific to this service.
33+
*
34+
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
35+
*/
36+
export interface CoreEventsData {
37+
[VIDEO_JS_PLAYER_CREATED]: CoreEventJSVideoPlayerCreated;
38+
}
39+
40+
}
41+
42+
export const VIDEO_JS_PLAYER_CREATED = 'video_js_player_created';
43+
44+
/**
45+
* Wrapper encapsulating videojs functionality.
46+
*/
47+
@Injectable({ providedIn: 'root' })
48+
export class AddonFilterMediaPluginVideoJSService {
49+
50+
protected videojs?: CorePromisedValue<typeof videojs>;
51+
52+
/**
53+
* Create a VideoJS player.
54+
*
55+
* @param element Media element.
56+
*/
57+
async createPlayer(element: HTMLVideoElement | HTMLAudioElement): Promise<void> {
58+
// Wait for external-content to finish in the element and its sources.
59+
await Promise.all([
60+
CoreDirectivesRegistry.waitDirectivesReady(element, undefined, CoreExternalContentDirective),
61+
CoreDirectivesRegistry.waitDirectivesReady(element, 'source', CoreExternalContentDirective),
62+
]);
63+
64+
// Create player.
65+
const videojs = await this.getVideoJS();
66+
const dataSetupString = element.getAttribute('data-setup') || element.getAttribute('data-setup-lazy') || '{}';
67+
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
68+
const player = videojs(
69+
element,
70+
{
71+
controls: true,
72+
techOrder: ['OgvJS'],
73+
language: await CoreLang.getCurrentLanguage(),
74+
controlBar: { pictureInPictureToggle: false },
75+
aspectRatio: data.aspectRatio,
76+
},
77+
() => element.tagName === 'VIDEO' && this.fixVideoJSPlayerSize(player),
78+
);
79+
80+
CoreEvents.trigger(VIDEO_JS_PLAYER_CREATED, {
81+
element,
82+
player,
83+
});
84+
}
85+
86+
/**
87+
* Find a VideoJS player by id.
88+
*
89+
* @param id Element id.
90+
* @returns VideoJS player.
91+
*/
92+
async findPlayer(id: string): Promise<VideoJSPlayer | null> {
93+
const videojs = await this.getVideoJS();
94+
95+
return videojs.getPlayer(id);
96+
}
97+
98+
/**
99+
* Treat Video JS Youtube video links and translate them to iframes.
100+
*
101+
* @param video Video element.
102+
*/
103+
treatYoutubeVideos(video: HTMLElement): void {
104+
if (!video.classList.contains('video-js')) {
105+
return;
106+
}
107+
108+
const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}';
109+
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
110+
const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src);
111+
112+
if (!youtubeUrl) {
113+
return;
114+
}
115+
116+
const iframe = document.createElement('iframe');
117+
iframe.id = video.id;
118+
iframe.src = youtubeUrl;
119+
iframe.setAttribute('frameborder', '0');
120+
iframe.setAttribute('allowfullscreen', '1');
121+
iframe.width = '100%';
122+
iframe.height = '300';
123+
124+
// Replace video tag by the iframe.
125+
video.parentNode?.replaceChild(iframe, video);
126+
}
127+
128+
/**
129+
* Gets videojs instance.
130+
*
131+
* @returns VideoJS.
132+
*/
133+
protected async getVideoJS(): Promise<typeof videojs> {
134+
if (!this.videojs) {
135+
this.videojs = new CorePromisedValue();
136+
137+
// Inject CSS.
138+
const link = document.createElement('link');
139+
140+
link.rel = 'stylesheet';
141+
link.href = 'assets/lib/video.js/video-js.min.css';
142+
143+
document.head.appendChild(link);
144+
145+
// Load library.
146+
return import('@addons/filter/mediaplugin/utils/videojs').then(({ initializeVideoJSOgvJS, videojs }) => {
147+
initializeVideoJSOgvJS();
148+
149+
this.videojs?.resolve(videojs);
150+
151+
return videojs;
152+
});
153+
}
154+
155+
return this.videojs;
156+
}
157+
158+
/**
159+
* Fix VideoJS player size.
160+
* If video width is wider than available width, video is cut off. Fix the dimensions in this case.
161+
*
162+
* @param player Player instance.
163+
*/
164+
protected fixVideoJSPlayerSize(player: VideoJSPlayer): void {
165+
const videoWidth = player.videoWidth();
166+
const videoHeight = player.videoHeight();
167+
const playerDimensions = player.currentDimensions();
168+
if (!videoWidth || !videoHeight || !playerDimensions.width || videoWidth === playerDimensions.width) {
169+
return;
170+
}
171+
172+
const candidateHeight = playerDimensions.width * videoHeight / videoWidth;
173+
if (!playerDimensions.height || Math.abs(candidateHeight - playerDimensions.height) > 1) {
174+
player.dimension('height', candidateHeight);
175+
}
176+
}
177+
178+
}
179+
180+
export const AddonFilterMediaPluginVideoJS = makeSingleton(AddonFilterMediaPluginVideoJSService);
181+
182+
/**
183+
* Data passed to VIDEO_JS_PLAYER_CREATED event.
184+
*/
185+
export type CoreEventJSVideoPlayerCreated = {
186+
element: HTMLAudioElement | HTMLVideoElement;
187+
player: VideoJSPlayer;
188+
};

0 commit comments

Comments
 (0)