MOBILE-4239 mediaplugin: Lazy load videojs
parent
4cb9a6640c
commit
8ee614a60a
|
@ -16,7 +16,7 @@ import { CorePlatform } from '@services/platform';
|
|||
import { OGVPlayer, OGVCompat, OGVLoader } from 'ogv';
|
||||
import videojs, { PreloadOption, TechSourceObject, VideoJSOptions } from 'video.js';
|
||||
|
||||
export const Tech = videojs.getComponent('Tech');
|
||||
const Tech = videojs.getComponent('Tech');
|
||||
|
||||
/**
|
||||
* Object.defineProperty but "lazy", which means that the value is only set after
|
||||
|
@ -728,13 +728,6 @@ export class VideoJSOgvJS extends Tech {
|
|||
].forEach(([key, fn]) => {
|
||||
defineLazyProperty(VideoJSOgvJS.prototype, key, () => VideoJSOgvJS[fn](), true);
|
||||
});
|
||||
/**
|
||||
* Initialize the controller.
|
||||
*/
|
||||
export const initializeVideoJSOgvJS = (): void => {
|
||||
OGVLoader.base = 'assets/lib/ogv';
|
||||
Tech.registerTech('OgvJS', VideoJSOgvJS);
|
||||
};
|
||||
|
||||
type OGVPlayerEl = (HTMLAudioElement | HTMLVideoElement) & {
|
||||
stop: () => void;
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
|
||||
import { CoreFilterDelegate } from '@features/filter/services/filter-delegate';
|
||||
import { initializeVideoJSOgvJS } from './classes/videojs-ogvjs';
|
||||
import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin';
|
||||
|
||||
@NgModule({
|
||||
|
@ -29,8 +28,6 @@ import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin';
|
|||
multi: true,
|
||||
useValue: () => {
|
||||
CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance);
|
||||
|
||||
initializeVideoJSOgvJS();
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -12,18 +12,12 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { AddonFilterMediaPluginVideoJS } from '@addons/filter/mediaplugin/services/videojs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreExternalContentDirective } from '@directives/external-content';
|
||||
|
||||
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
|
||||
import { CoreLang } from '@services/lang';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreMedia } from '@singletons/media';
|
||||
import videojs, { VideoJSOptions, VideoJSPlayer } from 'video.js';
|
||||
|
||||
/**
|
||||
* Handler to support the Multimedia filter.
|
||||
|
@ -39,15 +33,13 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
filter(
|
||||
text: string,
|
||||
): string | Promise<string> {
|
||||
filter(text: string): string | Promise<string> {
|
||||
this.template.innerHTML = text;
|
||||
|
||||
const videos = Array.from(this.template.content.querySelectorAll('video'));
|
||||
|
||||
videos.forEach((video) => {
|
||||
this.treatYoutubeVideos(video);
|
||||
AddonFilterMediaPluginVideoJS.treatYoutubeVideos(video);
|
||||
});
|
||||
|
||||
return this.template.innerHTML;
|
||||
|
@ -61,104 +53,18 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
|||
|
||||
mediaElements.forEach((mediaElement) => {
|
||||
if (CoreMedia.mediaUsesJavascriptPlayer(mediaElement)) {
|
||||
this.useVideoJS(mediaElement);
|
||||
} else {
|
||||
// Remove the VideoJS classes and data if present.
|
||||
mediaElement.classList.remove('video-js');
|
||||
mediaElement.removeAttribute('data-setup');
|
||||
mediaElement.removeAttribute('data-setup-lazy');
|
||||
AddonFilterMediaPluginVideoJS.createPlayer(mediaElement);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the VideoJS classes and data if present.
|
||||
mediaElement.classList.remove('video-js');
|
||||
mediaElement.removeAttribute('data-setup');
|
||||
mediaElement.removeAttribute('data-setup-lazy');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Use video JS in a certain video or audio.
|
||||
*
|
||||
* @param mediaElement Media element.
|
||||
*/
|
||||
protected async useVideoJS(mediaElement: HTMLVideoElement | HTMLAudioElement): Promise<void> {
|
||||
const lang = await CoreLang.getCurrentLanguage();
|
||||
|
||||
// Wait for external-content to finish in the element and its sources.
|
||||
await Promise.all([
|
||||
CoreDirectivesRegistry.waitDirectivesReady(mediaElement, undefined, CoreExternalContentDirective),
|
||||
CoreDirectivesRegistry.waitDirectivesReady(mediaElement, 'source', CoreExternalContentDirective),
|
||||
]);
|
||||
|
||||
const dataSetupString = mediaElement.getAttribute('data-setup') || mediaElement.getAttribute('data-setup-lazy') || '{}';
|
||||
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
|
||||
|
||||
const player = videojs(mediaElement, {
|
||||
controls: true,
|
||||
techOrder: ['OgvJS'],
|
||||
language: lang,
|
||||
controlBar: {
|
||||
pictureInPictureToggle: false,
|
||||
},
|
||||
aspectRatio: data.aspectRatio,
|
||||
}, () => {
|
||||
if (mediaElement.tagName === 'VIDEO') {
|
||||
this.fixVideoJSPlayerSize(player);
|
||||
}
|
||||
});
|
||||
|
||||
CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, {
|
||||
id: mediaElement.id,
|
||||
element: mediaElement,
|
||||
player,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix VideoJS player size.
|
||||
* If video width is wider than available width, video is cut off. Fix the dimensions in this case.
|
||||
*
|
||||
* @param player Player instance.
|
||||
*/
|
||||
protected fixVideoJSPlayerSize(player: VideoJSPlayer): void {
|
||||
const videoWidth = player.videoWidth();
|
||||
const videoHeight = player.videoHeight();
|
||||
const playerDimensions = player.currentDimensions();
|
||||
if (!videoWidth || !videoHeight || !playerDimensions.width || videoWidth === playerDimensions.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candidateHeight = playerDimensions.width * videoHeight / videoWidth;
|
||||
if (!playerDimensions.height || Math.abs(candidateHeight - playerDimensions.height) > 1) {
|
||||
player.dimension('height', candidateHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat Video JS Youtube video links and translate them to iframes.
|
||||
*
|
||||
* @param video Video element.
|
||||
*/
|
||||
protected treatYoutubeVideos(video: HTMLElement): void {
|
||||
if (!video.classList.contains('video-js')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}';
|
||||
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
|
||||
const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src);
|
||||
|
||||
if (!youtubeUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = video.id;
|
||||
iframe.src = youtubeUrl;
|
||||
iframe.setAttribute('frameborder', '0');
|
||||
iframe.setAttribute('allowfullscreen', '1');
|
||||
iframe.width = '100%';
|
||||
iframe.height = '300';
|
||||
|
||||
// Replace video tag by the iframe.
|
||||
video.parentNode?.replaceChild(iframe, video);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const AddonFilterMediaPluginHandler = makeSingleton(AddonFilterMediaPluginHandlerService);
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
import { CoreExternalContentDirective } from '@directives/external-content';
|
||||
import { CoreLang } from '@services/lang';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import type videojs from 'video.js';
|
||||
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import type { VideoJSOptions, VideoJSPlayer } from 'video.js';
|
||||
|
||||
declare module '@singletons/events' {
|
||||
|
||||
/**
|
||||
* Augment CoreEventsData interface with events specific to this service.
|
||||
*
|
||||
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||
*/
|
||||
export interface CoreEventsData {
|
||||
[VIDEO_JS_PLAYER_CREATED]: CoreEventJSVideoPlayerCreated;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const VIDEO_JS_PLAYER_CREATED = 'video_js_player_created';
|
||||
|
||||
/**
|
||||
* Wrapper encapsulating videojs functionality.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonFilterMediaPluginVideoJSService {
|
||||
|
||||
protected videojs?: CorePromisedValue<typeof videojs>;
|
||||
|
||||
/**
|
||||
* Create a VideoJS player.
|
||||
*
|
||||
* @param element Media element.
|
||||
*/
|
||||
async createPlayer(element: HTMLVideoElement | HTMLAudioElement): Promise<void> {
|
||||
// Wait for external-content to finish in the element and its sources.
|
||||
await Promise.all([
|
||||
CoreDirectivesRegistry.waitDirectivesReady(element, undefined, CoreExternalContentDirective),
|
||||
CoreDirectivesRegistry.waitDirectivesReady(element, 'source', CoreExternalContentDirective),
|
||||
]);
|
||||
|
||||
// Create player.
|
||||
const videojs = await this.getVideoJS();
|
||||
const dataSetupString = element.getAttribute('data-setup') || element.getAttribute('data-setup-lazy') || '{}';
|
||||
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
|
||||
const player = videojs(
|
||||
element,
|
||||
{
|
||||
controls: true,
|
||||
techOrder: ['OgvJS'],
|
||||
language: await CoreLang.getCurrentLanguage(),
|
||||
controlBar: { pictureInPictureToggle: false },
|
||||
aspectRatio: data.aspectRatio,
|
||||
},
|
||||
() => element.tagName === 'VIDEO' && this.fixVideoJSPlayerSize(player),
|
||||
);
|
||||
|
||||
CoreEvents.trigger(VIDEO_JS_PLAYER_CREATED, {
|
||||
element,
|
||||
player,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a VideoJS player by id.
|
||||
*
|
||||
* @param id Element id.
|
||||
* @returns VideoJS player.
|
||||
*/
|
||||
async findPlayer(id: string): Promise<VideoJSPlayer | null> {
|
||||
const videojs = await this.getVideoJS();
|
||||
|
||||
return videojs.getPlayer(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat Video JS Youtube video links and translate them to iframes.
|
||||
*
|
||||
* @param video Video element.
|
||||
*/
|
||||
treatYoutubeVideos(video: HTMLElement): void {
|
||||
if (!video.classList.contains('video-js')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}';
|
||||
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
|
||||
const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src);
|
||||
|
||||
if (!youtubeUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = video.id;
|
||||
iframe.src = youtubeUrl;
|
||||
iframe.setAttribute('frameborder', '0');
|
||||
iframe.setAttribute('allowfullscreen', '1');
|
||||
iframe.width = '100%';
|
||||
iframe.height = '300';
|
||||
|
||||
// Replace video tag by the iframe.
|
||||
video.parentNode?.replaceChild(iframe, video);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets videojs instance.
|
||||
*
|
||||
* @returns VideoJS.
|
||||
*/
|
||||
protected async getVideoJS(): Promise<typeof videojs> {
|
||||
if (!this.videojs) {
|
||||
this.videojs = new CorePromisedValue();
|
||||
|
||||
// Inject CSS.
|
||||
const link = document.createElement('link');
|
||||
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'assets/lib/video.js/video-js.min.css';
|
||||
|
||||
document.head.appendChild(link);
|
||||
|
||||
// Load library.
|
||||
return import('@addons/filter/mediaplugin/utils/videojs').then(({ initializeVideoJSOgvJS, videojs }) => {
|
||||
initializeVideoJSOgvJS();
|
||||
|
||||
this.videojs?.resolve(videojs);
|
||||
|
||||
return videojs;
|
||||
});
|
||||
}
|
||||
|
||||
return this.videojs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix VideoJS player size.
|
||||
* If video width is wider than available width, video is cut off. Fix the dimensions in this case.
|
||||
*
|
||||
* @param player Player instance.
|
||||
*/
|
||||
protected fixVideoJSPlayerSize(player: VideoJSPlayer): void {
|
||||
const videoWidth = player.videoWidth();
|
||||
const videoHeight = player.videoHeight();
|
||||
const playerDimensions = player.currentDimensions();
|
||||
if (!videoWidth || !videoHeight || !playerDimensions.width || videoWidth === playerDimensions.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candidateHeight = playerDimensions.width * videoHeight / videoWidth;
|
||||
if (!playerDimensions.height || Math.abs(candidateHeight - playerDimensions.height) > 1) {
|
||||
player.dimension('height', candidateHeight);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const AddonFilterMediaPluginVideoJS = makeSingleton(AddonFilterMediaPluginVideoJSService);
|
||||
|
||||
/**
|
||||
* Data passed to VIDEO_JS_PLAYER_CREATED event.
|
||||
*/
|
||||
export type CoreEventJSVideoPlayerCreated = {
|
||||
element: HTMLAudioElement | HTMLVideoElement;
|
||||
player: VideoJSPlayer;
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { VideoJSOgvJS } from '@addons/filter/mediaplugin/classes/videojs-ogvjs';
|
||||
import { OGVLoader } from 'ogv';
|
||||
import videojs from 'video.js';
|
||||
|
||||
export { videojs };
|
||||
|
||||
/**
|
||||
* Initialize the controller.
|
||||
*/
|
||||
export function initializeVideoJSOgvJS(): void {
|
||||
OGVLoader.base = 'assets/lib/ogv';
|
||||
|
||||
videojs.getComponent('Tech').registerTech('OgvJS', VideoJSOgvJS);
|
||||
}
|
|
@ -14,10 +14,11 @@
|
|||
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ElementController } from './ElementController';
|
||||
import videojs, { VideoJSPlayer } from 'video.js';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreMedia } from '@singletons/media';
|
||||
import { AddonFilterMediaPluginVideoJS, VIDEO_JS_PLAYER_CREATED } from '@addons/filter/mediaplugin/services/videojs';
|
||||
import type { VideoJSPlayer } from 'video.js';
|
||||
|
||||
/**
|
||||
* Wrapper class to control the interactivity of a media element.
|
||||
|
@ -42,21 +43,7 @@ export class MediaElementController extends ElementController {
|
|||
|
||||
media.autoplay = false;
|
||||
|
||||
if (CoreMedia.mediaUsesJavascriptPlayer(media)) {
|
||||
const player = this.searchJSPlayer();
|
||||
if (player) {
|
||||
this.jsPlayer.resolve(player);
|
||||
} else {
|
||||
this.jsPlayerListener = CoreEvents.on(CoreEvents.JS_PLAYER_CREATED, data => {
|
||||
if (data.element === media) {
|
||||
this.jsPlayerListener?.off();
|
||||
this.jsPlayer.resolve(data.player);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.jsPlayer.resolve(null);
|
||||
}
|
||||
this.initJSPlayer(media);
|
||||
|
||||
enabled && this.onEnabled();
|
||||
}
|
||||
|
@ -115,6 +102,36 @@ export class MediaElementController extends ElementController {
|
|||
jsPlayer?.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init JS Player instance.
|
||||
*
|
||||
* @param media Media element.
|
||||
*/
|
||||
private async initJSPlayer(media: HTMLMediaElement): Promise<void> {
|
||||
if (!CoreMedia.mediaUsesJavascriptPlayer(media)) {
|
||||
this.jsPlayer.resolve(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = await this.searchJSPlayer();
|
||||
|
||||
if (!player) {
|
||||
this.jsPlayerListener = CoreEvents.on(VIDEO_JS_PLAYER_CREATED, ({ element, player }) => {
|
||||
if (element !== media) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jsPlayerListener?.off();
|
||||
this.jsPlayer.resolve(player);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.jsPlayer.resolve(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening playback events.
|
||||
*
|
||||
|
@ -153,8 +170,9 @@ export class MediaElementController extends ElementController {
|
|||
*
|
||||
* @returns Player instance if found.
|
||||
*/
|
||||
private searchJSPlayer(): VideoJSPlayer | null {
|
||||
return videojs.getPlayer(this.media.id) || videojs.getPlayer(this.media.id.replace('_html5_api', ''));
|
||||
private async searchJSPlayer(): Promise<VideoJSPlayer | null> {
|
||||
return AddonFilterMediaPluginVideoJS.findPlayer(this.media.id)
|
||||
?? AddonFilterMediaPluginVideoJS.findPlayer(this.media.id.replace('_html5_api', ''));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool';
|
|||
import { CoreRedirectPayload } from '@services/navigator';
|
||||
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
import { CoreScreenOrientation } from '@services/screen';
|
||||
import { VideoJSPlayer } from 'video.js';
|
||||
|
||||
/**
|
||||
* Observer instance to stop listening to an event.
|
||||
|
@ -65,7 +64,6 @@ export interface CoreEventsData {
|
|||
[CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
|
||||
[CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed;
|
||||
[CoreEvents.COMPLETE_REQUIRED_PROFILE_DATA_FINISHED]: CoreEventCompleteRequiredProfileDataFinished;
|
||||
[CoreEvents.JS_PLAYER_CREATED]: CoreEventJSVideoPlayerCreated;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -125,7 +123,6 @@ export class CoreEvents {
|
|||
static readonly COMPLETE_REQUIRED_PROFILE_DATA_FINISHED = 'complete_required_profile_data_finished';
|
||||
static readonly MAIN_HOME_LOADED = 'main_home_loaded';
|
||||
static readonly FULL_SCREEN_CHANGED = 'full_screen_changed';
|
||||
static readonly JS_PLAYER_CREATED = 'js_player_created';
|
||||
|
||||
protected static logger = CoreLogger.getInstance('CoreEvents');
|
||||
protected static observables: { [eventName: string]: Subject<unknown> } = {};
|
||||
|
@ -493,12 +490,3 @@ export type CoreEventCourseModuleViewed = {
|
|||
export type CoreEventCompleteRequiredProfileDataFinished = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data passed to JS_PLAYER_CREATED event.
|
||||
*/
|
||||
export type CoreEventJSVideoPlayerCreated = {
|
||||
id: string;
|
||||
element: HTMLAudioElement | HTMLVideoElement;
|
||||
player: VideoJSPlayer;
|
||||
};
|
||||
|
|
|
@ -16,19 +16,12 @@
|
|||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<link rel="icon" type="image/png" href="assets/icon/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/lib/video.js/video-js.min.css">
|
||||
|
||||
<!-- add to homescreen for ios -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
|
||||
<script src="assets/lib/mathjax/MathJax.js?delayStartupUntil=configured"></script>
|
||||
<script type="text/javascript">
|
||||
if (globalThis === undefined) {
|
||||
// Define globalThis in environments where it's not supported to avoid errors with ogv.js.
|
||||
var globalThis = window;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'zone.js/dist/zone';
|
|||
|
||||
// Platform polyfills
|
||||
import 'core-js/es/array/includes';
|
||||
import 'core-js/es/global-this';
|
||||
import 'core-js/es/promise/finally';
|
||||
import 'core-js/es/string/match-all';
|
||||
import 'core-js/es/string/trim-right';
|
||||
|
|
Loading…
Reference in New Issue