MOBILE-4166 core: Fix VideoJS in books and destroy players

main
Dani Palou 2023-01-31 14:45:31 +01:00
parent 9419db02a1
commit 9b011ba350
5 changed files with 143 additions and 14 deletions

View File

@ -22,6 +22,7 @@ import { CoreUrlUtils } from '@services/utils/url';
import { makeSingleton } from '@singletons';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreDom } from '@singletons/dom';
import { CoreEvents } from '@singletons/events';
import videojs from 'video.js';
import { VideoJSOptions } from '../../classes/videojs-ogvjs';
@ -88,7 +89,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
const dataSetupString = mediaElement.getAttribute('data-setup') || mediaElement.getAttribute('data-setup-lazy') || '{}';
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
videojs(mediaElement, {
const player = videojs(mediaElement, {
controls: true,
techOrder: ['OgvJS'],
language: lang,
@ -98,6 +99,13 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
},
aspectRatio: data.aspectRatio,
});
CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, {
id: mediaElement.id,
element: mediaElement,
player,
});
}
/**

View File

@ -18,6 +18,7 @@
export abstract class ElementController {
protected enabled: boolean;
protected destroyed = false;
constructor(enabled: boolean) {
this.enabled = enabled;
@ -49,6 +50,19 @@ export abstract class ElementController {
this.onDisabled();
}
/**
* Destroy the element.
*/
destroy(): void {
if (this.destroyed) {
return;
}
this.destroyed = true;
this.onDestroy();
}
/**
* Update underlying element to enable interactivity.
*/
@ -59,4 +73,11 @@ export abstract class ElementController {
*/
abstract onDisabled(): void;
/**
* Destroy/dispose pertinent data.
*/
onDestroy(): void {
// By default, nothing to destroy.
}
}

View File

@ -14,6 +14,10 @@
import { CoreUtils } from '@services/utils/utils';
import { ElementController } from './ElementController';
import videojs from 'video.js';
import { CoreDom } from '@singletons/dom';
import { CorePromisedValue } from '@classes/promised-value';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
* Wrapper class to control the interactivity of a media element.
@ -25,6 +29,10 @@ export class MediaElementController extends ElementController {
private playing?: boolean;
private playListener?: () => void;
private pauseListener?: () => void;
private jsPlayer = new CorePromisedValue<VideoJSPlayer | null>();
private jsPlayerListener?: CoreEventObserver;
private shouldEnable = false;
private shouldDisable = false;
constructor(media: HTMLMediaElement, enabled: boolean) {
super(enabled);
@ -34,48 +42,127 @@ export class MediaElementController extends ElementController {
media.autoplay = false;
if (CoreDom.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 as VideoJSPlayer);
}
});
}
} else {
this.jsPlayer.resolve(null);
}
enabled && this.onEnabled();
}
/**
* @inheritdoc
*/
onEnabled(): void {
async onEnabled(): Promise<void> {
this.shouldEnable = true;
this.shouldDisable = false;
const jsPlayer = await this.jsPlayer;
if (!this.shouldEnable || this.destroyed) {
return;
}
const ready = this.playing ?? this.autoplay
? this.media.play()
? (jsPlayer ?? this.media).play()
: Promise.resolve();
ready
.then(() => this.addPlaybackEventListeners())
.catch(error => CoreUtils.logUnhandledError('Error enabling media element', error));
try {
await ready;
this.addPlaybackEventListeners(jsPlayer);
} catch (error) {
CoreUtils.logUnhandledError('Error enabling media element', error);
}
}
/**
* @inheritdoc
*/
async onDisabled(): Promise<void> {
this.removePlaybackEventListeners();
this.shouldDisable = true;
this.shouldEnable = false;
this.media.pause();
const jsPlayer = await this.jsPlayer;
if (!this.shouldDisable || this.destroyed) {
return;
}
this.removePlaybackEventListeners(jsPlayer);
(jsPlayer ?? this.media).pause();
}
/**
* @inheritdoc
*/
async onDestroy(): Promise<void> {
const jsPlayer = await this.jsPlayer;
this.removePlaybackEventListeners(jsPlayer);
jsPlayer?.dispose();
}
/**
* Start listening playback events.
*
* @param jsPlayer Javascript player instance (if any).
*/
private addPlaybackEventListeners(): void {
this.media.addEventListener('play', this.playListener = () => this.playing = true);
this.media.addEventListener('pause', this.pauseListener = () => this.playing = false);
private addPlaybackEventListeners(jsPlayer: VideoJSPlayer | null): void {
if (jsPlayer) {
jsPlayer.on('play', this.playListener = () => this.playing = true);
jsPlayer.on('pause', this.pauseListener = () => this.playing = false);
} else {
this.media.addEventListener('play', this.playListener = () => this.playing = true);
this.media.addEventListener('pause', this.pauseListener = () => this.playing = false);
}
}
/**
* Stop listening playback events.
*
* @param jsPlayer Javascript player instance (if any).
*/
private removePlaybackEventListeners(): void {
this.playListener && this.media.removeEventListener('play', this.playListener);
this.pauseListener && this.media.removeEventListener('pause', this.pauseListener);
private removePlaybackEventListeners(jsPlayer: VideoJSPlayer | null): void {
if (jsPlayer) {
this.playListener && jsPlayer.off('play', this.playListener);
this.pauseListener && jsPlayer.off('pause', this.pauseListener);
} else {
this.playListener && this.media.removeEventListener('play', this.playListener);
this.pauseListener && this.media.removeEventListener('pause', this.pauseListener);
}
delete this.playListener;
delete this.pauseListener;
}
/**
* Search JS player instance.
*
* @returns Player instance if found.
*/
private searchJSPlayer(): VideoJSPlayer | undefined {
return videojs.getPlayer(this.media.id) || videojs.getPlayer(this.media.id.replace('_html5_api', ''));
}
}
type VideoJSPlayer = {
play: () => Promise<void>;
pause: () => Promise<void>;
on: (name: string, callback: (ev: Event) => void) => void;
off: (name: string, callback: (ev: Event) => void) => void;
dispose: () => void;
};

View File

@ -149,6 +149,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
ngOnDestroy(): void {
this.domElementPromise?.cancel();
this.domPromises.forEach((promise) => { promise.cancel();});
this.elementControllers.forEach(controller => controller.destroy());
}
/**
@ -365,6 +366,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
// Move the children to the current element to be able to calculate the height.
CoreDomUtils.moveChildren(result.div, this.element);
this.elementControllers.forEach(controller => controller.destroy());
this.elementControllers = result.elementControllers;
await CoreUtils.nextTick();

View File

@ -64,6 +64,7 @@ export interface CoreEventsData {
[CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
[CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed;
[CoreEvents.COMPLETE_REQUIRED_PROFILE_DATA_FINISHED]: CoreEventCompleteRequiredProfileDataFinished;
[CoreEvents.JS_PLAYER_CREATED]: CoreEventJSVideoPlayerCreated;
}
/*
@ -123,6 +124,7 @@ 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> } = {};
@ -490,3 +492,12 @@ export type CoreEventCourseModuleViewed = {
export type CoreEventCompleteRequiredProfileDataFinished = {
path: string;
};
/**
* Data passed to JS_PLAYER_CREATED event.
*/
export type CoreEventJSVideoPlayerCreated = {
id: string;
element: HTMLAudioElement | HTMLVideoElement;
player: unknown;
};