From aebc660d2a34ad4407d1f230fcabcfb4a0275180 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Fri, 8 Apr 2022 13:51:05 +0200 Subject: [PATCH 01/25] Refactor: Expose seekBar from DlfMediaPlayer - Remove methods in DlfMediaPlayer that just forward to seek bar. - Prepare moving seek bar to a new frontend class. --- .../DlfMediaPlayer/DlfMediaPlayer.js | 42 +++++-------------- .../SlubMediaPlayer/SlubMediaPlayer.js | 19 +++++---- 2 files changed, 20 insertions(+), 41 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index b321311..251e7f8 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -98,7 +98,7 @@ export default class DlfMediaPlayer { this.controlEventQueue = []; /** @private @type {FlatSeekBar | null} */ - this.seekBar = null; + this.seekBar_ = null; /** @private @type {VideoFrame | null} */ this.vifa = null; @@ -129,7 +129,7 @@ export default class DlfMediaPlayer { // TODO: Figure out a good flow of events this.controls.addEventListener('dlf-media-seek-bar', (e) => { const detail = /** @type {dlf.media.SeekBarEvent} */(e).detail; - this.seekBar = detail.seekBar; + this.seekBar_ = detail.seekBar; }); this.controls.addEventListener('dlf-media-manual-seek', this.handlers.onManualSeek); @@ -169,6 +169,14 @@ export default class DlfMediaPlayer { Object.assign(this.constants, constants); } + /** + * + * @returns {FlatSeekBar | null} + */ + get seekBar() { + return this.seekBar_; + } + /** * * @param {string} posterUrl @@ -377,36 +385,6 @@ export default class DlfMediaPlayer { } } - /** - * @returns {boolean} - */ - isThumbnailPreviewOpen() { - return this.seekBar?.isThumbnailPreviewOpen() ?? false; - } - - /** - * Stop any active seeking/scrubbing and close thumbnail preview. - */ - endSeek() { - this.seekBar?.endSeek(); - } - - /** - * - * @param {boolean} value - */ - setThumbnailSnap(value) { - this.seekBar?.setThumbnailSnap(value); - } - - /** - * - * @param {number} clientX - */ - beginRelativeSeek(clientX) { - this.seekBar?.thumbnailPreview?.beginChange(clientX); - } - /** * * @returns {boolean} diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index 17e3730..e62960c 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -103,8 +103,8 @@ export default class SlubMediaPlayer { 'cancel': () => { if (this.modals.hasOpen()) { this.modals.closeNext(); - } else if (this.dlfPlayer.isThumbnailPreviewOpen()) { - this.dlfPlayer.endSeek(); + } else if (this.dlfPlayer.seekBar?.isThumbnailPreviewOpen() ?? false) { + this.dlfPlayer.seekBar?.endSeek(); } else if (this.dlfPlayer.anySettingsMenusAreOpen()) { this.dlfPlayer.hideSettingsMenus(); } @@ -113,7 +113,7 @@ export default class SlubMediaPlayer { this.openModal(this.modals.help); }, 'modal.help.toggle': () => { - this.dlfPlayer.endSeek(); + this.dlfPlayer.seekBar?.endSeek(); this.modals.toggleExclusive(this.modals.help); }, 'modal.bookmark.open': () => { @@ -126,11 +126,11 @@ export default class SlubMediaPlayer { this.snapScreenshot(); }, 'fullscreen.toggle': () => { - this.dlfPlayer.endSeek(); + this.dlfPlayer.seekBar?.endSeek(); this.toggleFullScreen(); }, 'theater.toggle': () => { - this.dlfPlayer.endSeek(); + this.dlfPlayer.seekBar?.endSeek(); // @see DigitalcollectionsScripts.js // TODO: Make sure the theater mode isn't activated on startup; then stop persisting @@ -204,7 +204,7 @@ export default class SlubMediaPlayer { /** @type {number} */ _keyIndex, /** @type {KeyEventMode} */ mode ) => { - this.dlfPlayer.setThumbnailSnap(mode === 'down'); + this.dlfPlayer.seekBar?.setThumbnailSnap(mode === 'down'); }, }; @@ -400,7 +400,8 @@ export default class SlubMediaPlayer { case 'hold': if (e.tapCount === 1) { - this.dlfPlayer.beginRelativeSeek(e.event.clientX); + // TODO: Somehow extract an action "navigate.relative-seek"? How to pass clientX? + this.dlfPlayer.seekBar?.thumbnailPreview?.beginChange(e.event.clientX); } else if (e.tapCount >= 2) { if (e.position.x < 1 / 3) { this.actions['navigate.continuous-rewind'](); @@ -422,7 +423,7 @@ export default class SlubMediaPlayer { }); g.on('release', () => { - this.dlfPlayer.endSeek(); + this.dlfPlayer.seekBar?.endSeek(); this.dlfPlayer.cancelTrickPlay(); }); } @@ -612,7 +613,7 @@ export default class SlubMediaPlayer { this.dlfPlayer.pauseOn(modal); } - this.dlfPlayer.endSeek(); + this.dlfPlayer.seekBar?.endSeek(); modal.open(); } } From 85c0073d5330e469553d432a0a6d2f4cc8876c3c Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Fri, 8 Apr 2022 16:02:08 +0200 Subject: [PATCH 02/25] Refactor: Move video loading to DlfMediaPlayer --- .../DlfMediaPlayer/DlfMediaPlayer.js | 49 +++++++++++++++++-- .../SlubMediaPlayer/SlubMediaPlayer.js | 28 +++-------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 251e7f8..44b3d5e 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -73,6 +73,12 @@ export default class DlfMediaPlayer { */ this.videoPausedOn = null; + /** @private @type {dlf.media.Source[]} */ + this.sources_ = []; + + /** @private @type {number | null} */ + this.startTime = null; + /** @private @type {string[]} */ this.controlPanelButtons = []; @@ -143,6 +149,7 @@ export default class DlfMediaPlayer { * Determines whether or not the player supports playback of videos in the * given mime type. * + * @private * @param {string} mimeType * @returns {boolean} */ @@ -301,13 +308,27 @@ export default class DlfMediaPlayer { return new DOMRect(bounding.x, bounding.y, bounding.width, bounding.height - controlsHeight - 20); } + async load() { + // Try loading video until one of the sources works. + for (const source of this.sources_) { + try { + await this.loadManifest(source); + return true; + } catch (e) { + console.error(e); + } + } + + return false; + } + /** * + * @private * @param {dlf.media.Source} videoSource - * @param {number | null} startTime */ - async loadManifest(videoSource, startTime = null) { - await this.player.load(videoSource.url, startTime, videoSource.mimeType); + async loadManifest(videoSource) { + await this.player.load(videoSource.url, this.startTime, videoSource.mimeType); this.variantGroups = new VariantGroups(this.player); @@ -418,6 +439,28 @@ export default class DlfMediaPlayer { this.emitControlEvent('dlf-media-chapters', { chapters }); } + /** + * + * @param {number | null} startTime + */ + setStartTime(startTime) { + this.startTime = startTime; + } + + get sources() { + return this.sources_; + } + + /** + * + * @param {dlf.media.Source[]} sources + */ + setSources(sources) { + this.sources_ = sources.filter( + source => this.supportsMimeType(source.mimeType) + ); + } + /** * * @returns {dlf.media.Chapter | undefined} diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index e62960c..64cf69c 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -279,16 +279,6 @@ export default class SlubMediaPlayer { * @private */ async load() { - // Find sources for supported manifest/video formats - const videoSources = this.videoInfo.sources.filter( - source => this.dlfPlayer.supportsMimeType(source.mimeType) - ); - - if (videoSources.length === 0) { - this.failWithError('error.playback-not-supported'); - return; - } - document.querySelectorAll("a[data-timecode], .tx-dlf-tableofcontents a").forEach(el => { const link = /** @type {HTMLAnchorElement} */(el); const timecode = this.getLinkTimecode(link); @@ -335,21 +325,17 @@ export default class SlubMediaPlayer { this.dlfPlayer.setPoster(this.videoInfo.url.poster); } this.dlfPlayer.setChapters(chapters); + this.dlfPlayer.setStartTime(startTime ?? null); + this.dlfPlayer.setSources(this.videoInfo.sources); this.dlfPlayer.mount(this.playerMount); - // Try loading video until one of the sources works. - let loadedSource; - for (const source of videoSources) { - try { - await this.dlfPlayer.loadManifest(source, startTime); - loadedSource = source; - break; - } catch (e) { - console.error(e); - } + if (this.dlfPlayer.sources.length === 0) { + this.failWithError('error.playback-not-supported'); + return; } - if (loadedSource === undefined) { + const hasLoadedVideo = await this.dlfPlayer.load(); + if (!hasLoadedVideo) { this.failWithError('error.load-failed'); return; } From 4722df48f6851e1936cdd0be2106c5bea45a6f27 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Tue, 12 Apr 2022 13:17:31 +0200 Subject: [PATCH 03/25] Move load error handling to DlfMediaPlayer --- .../DlfMediaPlayer/DlfMediaPlayer.js | 32 +++++++++++++++---- .../SlubMediaPlayer/SlubMediaPlayer.js | 23 ------------- .../Less/DlfMediaPlayer/DlfMediaPlayer.less | 21 ++++++++++++ .../Less/SlubMediaPlayer/SlubMediaPlayer.less | 9 ++---- 4 files changed, 50 insertions(+), 35 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 44b3d5e..464c1d9 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -5,7 +5,7 @@ import 'shaka-player/ui/controls.less'; import VideoFrame from './vendor/VideoFrame'; -import { clamp, e } from '../lib/util'; +import { clamp, e, setElementClass } from '../lib/util'; import Chapters from './Chapters'; import { FlatSeekBar, @@ -61,7 +61,9 @@ export default class DlfMediaPlayer { this.hidePoster(); }, }); - this.container.append(this.video, this.poster); + this.videoBox = e('div', { className: "dlf-media-shaka-box" }, [this.video, this.poster]); + this.errorBox = e('div', { className: "dlf-media-shaka-box dlf-media-error" }); + this.container.append(this.videoBox, this.errorBox); /** * The object that has caused current pause state, if any. @@ -89,7 +91,7 @@ export default class DlfMediaPlayer { this.player = new shaka.Player(this.video); /** @private @type {shaka.ui.Overlay} */ - this.ui = new shaka.ui.Overlay(this.player, this.container, this.video); + this.ui = new shaka.ui.Overlay(this.player, this.videoBox, this.video); /** @private @type {shaka.ui.Controls} */ this.controls = /** @type {shaka.ui.Controls} */(this.ui.getControls()); @@ -260,7 +262,7 @@ export default class DlfMediaPlayer { // Set again after `ui.configure()` this.shakaBottomControls = - this.container.querySelector('.shaka-bottom-controls'); + this.videoBox.querySelector('.shaka-bottom-controls'); mount.replaceWith(this.container); @@ -283,6 +285,18 @@ export default class DlfMediaPlayer { } } + /** + * + * @param {string | null} langKey + */ + showError(langKey) { + if (langKey !== null) { + this.errorBox.innerText = this.env.t(langKey); + } + + setElementClass(this.errorBox, 'dlf-visible', langKey !== null); + } + getContainer() { return this.container; } @@ -294,7 +308,7 @@ export default class DlfMediaPlayer { * @param {PointerEvent} e */ isUserAreaEvent(e) { - return e.target === this.container.querySelector('.shaka-play-button-container'); + return e.target === this.videoBox.querySelector('.shaka-play-button-container'); } /** @@ -303,12 +317,17 @@ export default class DlfMediaPlayer { * @type {DOMRect} */ get userArea() { - const bounding = this.container.getBoundingClientRect(); + const bounding = this.videoBox.getBoundingClientRect(); const controlsHeight = this.shakaBottomControls?.getBoundingClientRect().height ?? 0; return new DOMRect(bounding.x, bounding.y, bounding.width, bounding.height - controlsHeight - 20); } async load() { + if (this.sources_.length === 0) { + this.showError('error.playback-not-supported'); + return false; + } + // Try loading video until one of the sources works. for (const source of this.sources_) { try { @@ -319,6 +338,7 @@ export default class DlfMediaPlayer { } } + this.showError('error.load-failed'); return false; } diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index 64cf69c..fe419ae 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -213,23 +213,6 @@ export default class SlubMediaPlayer { this.load(); } - /** - * Prints global error message into {@link container} and quits. - * - * @private - * @param {string} langKey - */ - failWithError(langKey) { - this.dlfPlayer.unmount(); - - const errorBox = e('div', { - className: "sxnd-player-fatal-error", - }, [this.env.t(langKey)]); - - this.container.innerHTML = ""; - this.container.append(errorBox); - } - /** * @private * @param {Chapters} chapters @@ -329,14 +312,8 @@ export default class SlubMediaPlayer { this.dlfPlayer.setSources(this.videoInfo.sources); this.dlfPlayer.mount(this.playerMount); - if (this.dlfPlayer.sources.length === 0) { - this.failWithError('error.playback-not-supported'); - return; - } - const hasLoadedVideo = await this.dlfPlayer.load(); if (!hasLoadedVideo) { - this.failWithError('error.load-failed'); return; } diff --git a/Resources/Private/Less/DlfMediaPlayer/DlfMediaPlayer.less b/Resources/Private/Less/DlfMediaPlayer/DlfMediaPlayer.less index 5760f5e..7ee224d 100644 --- a/Resources/Private/Less/DlfMediaPlayer/DlfMediaPlayer.less +++ b/Resources/Private/Less/DlfMediaPlayer/DlfMediaPlayer.less @@ -25,6 +25,27 @@ } } +.dlf-media-shaka-box { + background-color: black; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.dlf-media-error { + display: none; + color: white; + z-index: 1; + + &.dlf-visible { + display: flex; + justify-content: center; + align-items: center; + } +} + .shaka-video-container { height: 100%; } diff --git a/Resources/Private/Less/SlubMediaPlayer/SlubMediaPlayer.less b/Resources/Private/Less/SlubMediaPlayer/SlubMediaPlayer.less index f833265..9021ee4 100644 --- a/Resources/Private/Less/SlubMediaPlayer/SlubMediaPlayer.less +++ b/Resources/Private/Less/SlubMediaPlayer/SlubMediaPlayer.less @@ -36,12 +36,9 @@ } } -.sxnd-player-fatal-error { - position: absolute; - text-align: left; - padding-left: 1em; - padding-top: 1em; - color: white; +.dlf-media-player { + width: 100%; + height: 100%; } .inline-icon { From 226e9413e6d52ef3a784994c7e5d9a692b438f92 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Mon, 2 May 2022 21:06:13 +0200 Subject: [PATCH 04/25] Refactor: Pull modals out of SlubMediaPlayer constructor This prepares various changes: - Make modal parent configurable, so modals may not be created if the given parent is invalid. - Later, extract custom element where modals are created in `connectedCallback()`. --- .../SlubMediaPlayer/SlubMediaPlayer.js | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index fe419ae..4e4fba3 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -79,29 +79,12 @@ export default class SlubMediaPlayer { this.chapterLinks = []; /** @private */ - this.modals = Modals({ - help: new HelpModal(this.container, this.env, { - constants: { - ...this.constants, - // TODO: Refactor - forceLandscapeOnFullscreen: Number(this.constants.forceLandscapeOnFullscreen), - }, - keybindings: this.keybindings, - }), - bookmark: new BookmarkModal(this.container, this.env, { - shareButtons: this.config.shareButtons, - }), - screenshot: new ScreenshotModal(this.container, this.env, { - keybindings: this.keybindings, - screnshotCaptions: this.config.screenshotCaptions ?? [], - constants: this.constants, - }), - }); + this.modals = null; /** @private */ this.actions = { 'cancel': () => { - if (this.modals.hasOpen()) { + if (this.modals?.hasOpen()) { this.modals.closeNext(); } else if (this.dlfPlayer.seekBar?.isThumbnailPreviewOpen() ?? false) { this.dlfPlayer.seekBar?.endSeek(); @@ -110,11 +93,13 @@ export default class SlubMediaPlayer { } }, 'modal.help.open': () => { - this.openModal(this.modals.help); + this.openModal(this.modals?.help); }, 'modal.help.toggle': () => { - this.dlfPlayer.seekBar?.endSeek(); - this.modals.toggleExclusive(this.modals.help); + if (this.modals !== null) { + this.dlfPlayer.seekBar?.endSeek(); + this.modals.toggleExclusive(this.modals.help); + } }, 'modal.bookmark.open': () => { this.showBookmarkUrl(); @@ -208,11 +193,34 @@ export default class SlubMediaPlayer { }, }; - this.modals.on('closed', this.handlers.onCloseModal); - + this.createModals(); this.load(); } + createModals() { + /** @private */ + this.modals = Modals({ + help: new HelpModal(this.container, this.env, { + constants: { + ...this.constants, + // TODO: Refactor + forceLandscapeOnFullscreen: Number(this.constants.forceLandscapeOnFullscreen), + }, + keybindings: this.keybindings, + }), + bookmark: new BookmarkModal(this.container, this.env, { + shareButtons: this.config.shareButtons, + }), + screenshot: new ScreenshotModal(this.container, this.env, { + keybindings: this.keybindings, + screnshotCaptions: this.config.screenshotCaptions ?? [], + constants: this.constants, + }), + }); + + this.modals.on('closed', this.handlers.onCloseModal); + } + /** * @private * @param {Chapters} chapters @@ -317,7 +325,7 @@ export default class SlubMediaPlayer { return; } - this.modals.resize(); + this.modals?.resize(); this.registerEventHandlers(); } @@ -396,7 +404,7 @@ export default class SlubMediaPlayer { * @returns {KeyboardScope} */ getKeyboardScope() { - if (this.modals.hasOpen()) { + if (this.modals?.hasOpen()) { return 'modal'; } @@ -525,7 +533,7 @@ export default class SlubMediaPlayer { return; } - const modal = this.modals.bookmark + const modal = this.modals?.bookmark .setTimecode(this.dlfPlayer.displayTime) .setFps(this.dlfPlayer.getFps() ?? 0); @@ -533,16 +541,16 @@ export default class SlubMediaPlayer { } /** - * @returns {ScreenshotModal | null} + * @returns {ScreenshotModal | undefined} */ prepareScreenshot() { // Don't do screenshot if there isn't yet an image to be displayed if (!this.dlfPlayer.hasCurrentData) { - return null; + return; } return ( - this.modals.screenshot + this.modals?.screenshot .setVideo(this.dlfPlayer.getVideo()) .setMetadata(this.videoInfo.metadata) .setFps(this.dlfPlayer.getFps()) @@ -552,26 +560,24 @@ export default class SlubMediaPlayer { showScreenshot() { const modal = this.prepareScreenshot(); - - if (modal !== null) { - this.openModal(modal, /* pause= */ true); - } + this.openModal(modal, /* pause= */ true); } snapScreenshot() { const modal = this.prepareScreenshot(); - - if (modal !== null) { - modal.snap(); - } + modal?.snap(); } /** * @private - * @param {ValueOf} modal + * @param {ValueOf=} modal * @param {boolean} pause */ openModal(modal, pause = false) { + if (modal == null) { + return; + } + if (pause) { this.dlfPlayer.pauseOn(modal); } From 0e01f6fcc069bf7d3d337a9bc67a21ff9dee9028 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Thu, 5 May 2022 12:37:11 +0200 Subject: [PATCH 05/25] Fix & refactor modals in fullscreen This fixes the issue that when using the fullscreen button on the player instead of a keybinding to enter fullscreen, the modals couldn't be opened. Here we extract our own fullscreen button, and use the correct container as fullscreenElement for both keybinding and button. Also, move `toggleFullScreen` to `Environment`. --- .../DlfMediaPlayer/DlfMediaPlayer.js | 4 -- .../controls/ControlPanelButton.js | 20 ++++-- .../controls/ControlPanelButton.test.js | 6 +- .../controls/FullScreenButton.js | 62 +++++++++++++++++++ .../DlfMediaPlayer/controls/index.js | 1 + .../JavaScript/DlfMediaPlayer/index.js | 2 +- .../JavaScript/SlubMediaPlayer/Environment.js | 37 +++++++++++ .../SlubMediaPlayer/SlubMediaPlayer.js | 53 +++++----------- .../lib/generateTimecodeUrl.test.js | 1 + .../JavaScript/SlubMediaPlayer/types.d.ts | 7 ++- Resources/Private/JavaScript/lib/lib.d.ts | 6 ++ .../Private/Language/de.locallang_video.xlf | 8 +++ .../Private/Language/locallang_video.xlf | 6 ++ .../Plugins/Media/Partials/Player.html | 3 +- 14 files changed, 163 insertions(+), 53 deletions(-) create mode 100644 Resources/Private/JavaScript/DlfMediaPlayer/controls/FullScreenButton.js diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 464c1d9..f8a3a58 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -438,10 +438,6 @@ export default class DlfMediaPlayer { this.controls.hideSettingsMenus(); } - toggleFullScreen() { - this.controls.toggleFullScreen(); - } - /** * * @param {string} locale diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.js index 396320a..dc4b2f8 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.js @@ -20,7 +20,7 @@ export default class ControlPanelButton extends shaka.ui.Element { * Registers a factory with specified configuration. The returned key may * be added to `controlPanelElements` in shaka-player config. * - * @param {Identifier} env + * @param {Translator & Identifier} env * @param {Partial} config * @returns {string} Key of the registered element factory */ @@ -28,8 +28,9 @@ export default class ControlPanelButton extends shaka.ui.Element { const key = env.mkid(); shaka.ui.Controls.registerElement(key, { - create(rootElement, controls) { - return new ControlPanelButton(rootElement, controls, config); + create: (rootElement, controls) => { + // "new this": Allow registering instance of derived classes + return new this(rootElement, controls, env, config); }, }); @@ -39,11 +40,15 @@ export default class ControlPanelButton extends shaka.ui.Element { /** * @param {HTMLElement} parent * @param {shaka.ui.Controls} controls + * @param {Translator} env * @param {Partial} config */ - constructor(parent, controls, config = {}) { + constructor(parent, controls, env, config = {}) { super(parent, controls); + /** @protected */ + this.env = env; + const button = e("button", { className: `material-icons-round ${config.className ?? ""}`, }, [config.material_icon]); @@ -57,10 +62,13 @@ export default class ControlPanelButton extends shaka.ui.Element { this.eventManager.listen(button, 'click', config.onClick); } - this.updateStrings(); + this.updateControlPanelButton(); } - updateStrings() { + /** + * @protected + */ + updateControlPanelButton() { let tooltip = this.dlf.config.title ?? ""; this.dlf.button.ariaLabel = tooltip; setElementClass(this.dlf.button, 'shaka-tooltip', tooltip !== ""); diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.test.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.test.js index 8edcc06..9167086 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.test.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.test.js @@ -5,16 +5,18 @@ // @ts-check import { describe, expect, test } from '@jest/globals'; +import Environment from '../../SlubMediaPlayer/Environment'; import ControlPanelButton from './ControlPanelButton'; import { createShakaPlayer } from './test-util'; describe('ControlPanelButton', () => { const shk = createShakaPlayer(); + const env = new Environment(); test('basic', () => { let clicked = 0; const buttonContainer = document.createElement('div'); - const button = new ControlPanelButton(buttonContainer, shk.controls, { + const button = new ControlPanelButton(buttonContainer, shk.controls, env, { material_icon: 'info', title: "Do it now", onClick: () => { @@ -29,7 +31,7 @@ describe('ControlPanelButton', () => { test('allows to omit title', () => { const buttonContainer = document.createElement('div'); - const button = new ControlPanelButton(buttonContainer, shk.controls); + const button = new ControlPanelButton(buttonContainer, shk.controls, env); const domButton = buttonContainer.querySelector('button'); expect(domButton?.ariaLabel).toBe(""); }); diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/FullScreenButton.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FullScreenButton.js new file mode 100644 index 0000000..5e586cb --- /dev/null +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FullScreenButton.js @@ -0,0 +1,62 @@ +// @ts-check + +import shaka from 'shaka-player/dist/shaka-player.ui'; +import ControlPanelButton from './ControlPanelButton'; + +/** + * @typedef Config + * @property {() => void} onClick + */ + +/** + * Adopted from Shaka's fullscreen_button. + * + * A separate control is used to allow overriding the fullscreen action. + */ +export default class FullScreenButton extends ControlPanelButton { + /** + * @param {!HTMLElement} parent + * @param {!shaka.ui.Controls} controls + * @param {Translator} env + * @param {Partial} config + */ + constructor(parent, controls, env, config = {}) { + super(parent, controls, env, { + ...config, + className: `shaka-fullscreen-button shaka-tooltip` + }); + + if (this.eventManager) { + this.eventManager.listen(document, 'fullscreenchange', + this.updateFullScreenButton.bind(this)); + } + + this.updateFullScreenButton(); + } + + /** + * @override + */ + updateControlPanelButton() { + // We do all updates ourselves + } + + /** + * @protected + */ + updateFullScreenButton() { + if (document.fullscreenEnabled) { + // Update Material Icon + this.dlf.button.textContent = document.fullscreenElement + ? 'fullscreen_exit' + : 'fullscreen'; + + this.dlf.button.ariaLabel = document.fullscreenElement + ? this.env.t('control.fullscreen_exit.tooltip') + : this.env.t('control.fullscreen.tooltip'); + } else { + // Don't show the button if fullscreen is not supported + this.dlf.button.classList.add('shaka-hidden'); + } + } +}; diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/index.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/index.js index 83964fb..729dd88 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/index.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/index.js @@ -2,6 +2,7 @@ export { default as ControlPanelButton } from './ControlPanelButton'; export { default as FlatSeekBar } from './FlatSeekBar'; +export { default as FullScreenButton } from './FullScreenButton'; export { default as OverflowMenuButton } from './OverflowMenuButton'; export { default as PresentationTimeTracker } from './PresentationTimeTracker'; export { default as VideoTrackSelection } from './VideoTrackSelection'; diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/index.js b/Resources/Private/JavaScript/DlfMediaPlayer/index.js index f21fdac..2b75820 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/index.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/index.js @@ -1,6 +1,6 @@ // @ts-check export { default as Chapters } from './Chapters'; -export { ControlPanelButton, OverflowMenuButton } from './controls'; +export { ControlPanelButton, FullScreenButton, OverflowMenuButton } from './controls'; export { default as buildTimeString, timeStringFromTemplate } from './lib/buildTimeString'; export { default as DlfMediaPlayer } from './DlfMediaPlayer'; diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/Environment.js b/Resources/Private/JavaScript/SlubMediaPlayer/Environment.js index 6d81670..2089bc5 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/Environment.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/Environment.js @@ -96,6 +96,43 @@ export default class Environment { return document.fullscreenElement !== null; } + /** + * Mostly taken from Shaka player (shaka.ui.Controls). + * + * @inheritdoc + * @param {HTMLElement} fullscreenElement + * @param {boolean} forceLandscape + */ + async toggleFullScreen(fullscreenElement, forceLandscape) { + if (document.fullscreenElement) { + if (screen.orientation) { + screen.orientation.unlock(); + } + await document.exitFullscreen(); + } else { + // If we are in PiP mode, leave PiP mode first. + try { + if (document.pictureInPictureElement) { + await document.exitPictureInPicture(); + } + await fullscreenElement.requestFullscreen({ navigationUI: 'hide' }); + if (forceLandscape && screen.orientation) { + try { + // Locking to 'landscape' should let it be either + // 'landscape-primary' or 'landscape-secondary' as appropriate. + await screen.orientation.lock('landscape'); + } catch (error) { + // If screen.orientation.lock does not work on a device, it will + // be rejected with an error. Suppress that error. + } + } + } catch (e) { + // TODO: Error handling + console.log(e); + } + } + } + /** * @inheritdoc * @returns {string} diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index 4e4fba3..b5b2b18 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -8,6 +8,7 @@ import { Chapters, ControlPanelButton, DlfMediaPlayer, + FullScreenButton, } from '../DlfMediaPlayer'; import Modals from './lib/Modals'; @@ -33,13 +34,16 @@ export default class SlubMediaPlayer { /** * * @param {HTMLElement} container + * @param {HTMLElement} fullscreenElement * @param {VideoInfo} videoInfo * @param {AppConfig} config */ - constructor(container, videoInfo, config) { + constructor(container, fullscreenElement, videoInfo, config) { /** @private */ this.container = container; /** @private */ + this.fullscreenElement = fullscreenElement; + /** @private */ this.playerMount = e('div'); this.container.append(this.playerMount); /** @private */ @@ -200,7 +204,7 @@ export default class SlubMediaPlayer { createModals() { /** @private */ this.modals = Modals({ - help: new HelpModal(this.container, this.env, { + help: new HelpModal(this.fullscreenElement, this.env, { constants: { ...this.constants, // TODO: Refactor @@ -208,10 +212,10 @@ export default class SlubMediaPlayer { }, keybindings: this.keybindings, }), - bookmark: new BookmarkModal(this.container, this.env, { + bookmark: new BookmarkModal(this.fullscreenElement, this.env, { shareButtons: this.config.shareButtons, }), - screenshot: new ScreenshotModal(this.container, this.env, { + screenshot: new ScreenshotModal(this.fullscreenElement, this.env, { keybindings: this.keybindings, screnshotCaptions: this.config.screenshotCaptions ?? [], constants: this.constants, @@ -302,7 +306,9 @@ export default class SlubMediaPlayer { title: this.env.t('control.bookmark.tooltip'), onClick: this.actions['modal.bookmark.open'], }), - 'fullscreen', + FullScreenButton.register(this.env, { + onClick: this.actions['fullscreen.toggle'], + }), ControlPanelButton.register(this.env, { className: "sxnd-help-button", material_icon: 'info_outline', @@ -490,40 +496,11 @@ export default class SlubMediaPlayer { this.dlfPlayer.resumeOn(modal); } - /** - * Mostly taken from Shaka player (shaka.ui.Controls). - * - * We put this here so that we don't need to append the app elements (modals) - * to the player container. - */ async toggleFullScreen() { - if (document.fullscreenElement) { - if (screen.orientation) { - screen.orientation.unlock(); - } - await document.exitFullscreen(); - } else { - // If we are in PiP mode, leave PiP mode first. - try { - if (document.pictureInPictureElement) { - await document.exitPictureInPicture(); - } - await this.container.requestFullscreen({ navigationUI: 'hide' }); - if (this.constants.forceLandscapeOnFullscreen && screen.orientation) { - try { - // Locking to 'landscape' should let it be either - // 'landscape-primary' or 'landscape-secondary' as appropriate. - await screen.orientation.lock('landscape'); - } catch (error) { - // If screen.orientation.lock does not work on a device, it will - // be rejected with an error. Suppress that error. - } - } - } catch (e) { - // TODO: Error handling - console.log(e); - } - } + // We use this instead of Shaka's toggleFullScreen so that we don't need to + // append the app elements (modals) to the player container. + this.env.toggleFullScreen(this.fullscreenElement, + this.constants.forceLandscapeOnFullscreen); } showBookmarkUrl() { diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/lib/generateTimecodeUrl.test.js b/Resources/Private/JavaScript/SlubMediaPlayer/lib/generateTimecodeUrl.test.js index 2b01160..5a29c07 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/lib/generateTimecodeUrl.test.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/lib/generateTimecodeUrl.test.js @@ -25,6 +25,7 @@ describe('generateTimecodeUrl', () => { supportsCanvasExport: () => false, supportsVideoMime: () => false, isInFullScreen: () => false, + toggleFullScreen: () => { }, }; } diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts b/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts index cbe1acc..5795134 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts @@ -3,7 +3,12 @@ interface Window { SlubMediaPlayer: { - new (container: HTMLElement, videoInfo: VideoInfo, config: AppConfig); + new ( + container: HTMLElement, + fullscreenElement: HTMLElement, + videoInfo: VideoInfo, + config: AppConfig + ); }; } diff --git a/Resources/Private/JavaScript/lib/lib.d.ts b/Resources/Private/JavaScript/lib/lib.d.ts index 128abef..74fbaa7 100644 --- a/Resources/Private/JavaScript/lib/lib.d.ts +++ b/Resources/Private/JavaScript/lib/lib.d.ts @@ -67,6 +67,12 @@ interface Browser { * Checks whether the browser is in full screen. */ isInFullScreen(): boolean; + + /** + * Toggle full screen, using {@link fullscreenElement} if switching to full + * screen. + */ + toggleFullScreen(fullscreenElement: HTMLElement, forceLandscape: boolean); } /** diff --git a/Resources/Private/Language/de.locallang_video.xlf b/Resources/Private/Language/de.locallang_video.xlf index 4926fab..6fa939e 100644 --- a/Resources/Private/Language/de.locallang_video.xlf +++ b/Resources/Private/Language/de.locallang_video.xlf @@ -113,6 +113,14 @@ + + + + + + + + diff --git a/Resources/Private/Language/locallang_video.xlf b/Resources/Private/Language/locallang_video.xlf index 93ba93c..1c841b6 100644 --- a/Resources/Private/Language/locallang_video.xlf +++ b/Resources/Private/Language/locallang_video.xlf @@ -86,6 +86,12 @@ + + + + + + diff --git a/Resources/Private/Plugins/Media/Partials/Player.html b/Resources/Private/Plugins/Media/Partials/Player.html index dacc7c6..5b8578a 100644 --- a/Resources/Private/Plugins/Media/Partials/Player.html +++ b/Resources/Private/Plugins/Media/Partials/Player.html @@ -6,11 +6,12 @@ From c120fc8efe859150eb802dc26f795783a0b05487 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Mon, 9 May 2022 15:49:46 +0200 Subject: [PATCH 06/25] Refactor constants handling (app/player/modals) --- .../DlfMediaPlayer/DlfMediaPlayer.js | 27 ++++++++++------- .../JavaScript/DlfMediaPlayer/types.d.ts | 25 ++++++++++++++++ .../SlubMediaPlayer/SlubMediaPlayer.js | 25 +++++++--------- .../SlubMediaPlayer/modals/ScreenshotModal.js | 7 +---- .../JavaScript/SlubMediaPlayer/types.d.ts | 29 ++++--------------- 5 files changed, 59 insertions(+), 54 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index f8a3a58..ddf2a74 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -5,6 +5,7 @@ import 'shaka-player/ui/controls.less'; import VideoFrame from './vendor/VideoFrame'; +import typoConstants from '../lib/typoConstants'; import { clamp, e, setElementClass } from '../lib/util'; import Chapters from './Chapters'; import { @@ -14,13 +15,6 @@ import { } from './controls'; import VariantGroups from './VariantGroups'; -/** - * @typedef {{ - * prevChapterTolerance: number; - * minBottomControlsReadyState: number; - * }} Constants - */ - export default class DlfMediaPlayer { /** @private */ static hasInstalledPolyfills = false; @@ -38,9 +32,12 @@ export default class DlfMediaPlayer { /** @private */ this.env = env; - /** @private @type {Constants} @see {setConstants} */ + /** @private @type {dlf.media.PlayerConstants} @see {parseConstants} */ this.constants = { prevChapterTolerance: 5, + volumeStep: 0.05, + seekStep: 5, + trickPlayFactor: 4, minBottomControlsReadyState: 2, // Enough data for current position }; @@ -172,10 +169,18 @@ export default class DlfMediaPlayer { /** * - * @param {Partial} constants + * @returns {Readonly} + */ + getConstants() { + return this.constants; + } + + /** + * + * @param {import('../lib/typoConstants').TypoConstants} constants */ - setConstants(constants) { - Object.assign(this.constants, constants); + parseConstants(constants) { + this.constants = typoConstants(constants, this.constants); } /** diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index 88e3339..ed9509f 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -23,6 +23,31 @@ namespace dlf { url: string; }; + type PlayerConstants = { + /** + * Number of seconds in which to still rewind to previous chapter. + */ + prevChapterTolerance: number; + + /** + * Volume increase/decrease in relevant keybinding. + */ + volumeStep: number; + + /** + * Number of seconds to seek or rewind in relevant keybinding. + */ + seekStep: number; + + /** + * Trick play factor for continuous rewind/seek. + * TODO: Check if this should be input as setting or retrieved from current manifest + */ + trickPlayFactor: number; + + minBottomControlsReadyState: number; + }; + /** * Signals chapters available in current video. * diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index b5b2b18..67a057e 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -55,12 +55,6 @@ export default class SlubMediaPlayer { /** @private @type {AppConstants} */ this.constants = typoConstants(config.constants ?? {}, { - screenshotFilenameTemplate: 'Screenshot', - screenshotCommentTemplate: '', - prevChapterTolerance: 5, - volumeStep: 0.05, - seekStep: 5, - trickPlayFactor: 4, forceLandscapeOnFullscreen: true, }); @@ -78,6 +72,9 @@ export default class SlubMediaPlayer { /** @private */ this.dlfPlayer = new DlfMediaPlayer(this.env); + this.dlfPlayer.parseConstants(config.constants ?? {}); + + const playerConstants = this.dlfPlayer.getConstants(); /** @private @type {ChapterLink[]} */ this.chapterLinks = []; @@ -143,25 +140,25 @@ export default class SlubMediaPlayer { this.dlfPlayer.muted = !this.dlfPlayer.muted; }, 'playback.volume.inc': () => { - this.dlfPlayer.volume = this.dlfPlayer.volume + this.constants.volumeStep; + this.dlfPlayer.volume = this.dlfPlayer.volume + playerConstants.volumeStep; }, 'playback.volume.dec': () => { - this.dlfPlayer.volume = this.dlfPlayer.volume - this.constants.volumeStep; + this.dlfPlayer.volume = this.dlfPlayer.volume - playerConstants.volumeStep; }, 'playback.captions.toggle': () => { this.dlfPlayer.showCaptions = !this.dlfPlayer.showCaptions; }, 'navigate.rewind': () => { - this.dlfPlayer.skipSeconds(-this.constants.seekStep); + this.dlfPlayer.skipSeconds(-playerConstants.seekStep); }, 'navigate.seek': () => { - this.dlfPlayer.skipSeconds(+this.constants.seekStep); + this.dlfPlayer.skipSeconds(+playerConstants.seekStep); }, 'navigate.continuous-rewind': () => { - this.dlfPlayer.ensureTrickPlay(-this.constants.trickPlayFactor); + this.dlfPlayer.ensureTrickPlay(-playerConstants.trickPlayFactor); }, 'navigate.continuous-seek': () => { - this.dlfPlayer.ensureTrickPlay(this.constants.trickPlayFactor); + this.dlfPlayer.ensureTrickPlay(playerConstants.trickPlayFactor); }, 'navigate.chapter.prev': () => { this.dlfPlayer.prevChapter(); @@ -207,6 +204,7 @@ export default class SlubMediaPlayer { help: new HelpModal(this.fullscreenElement, this.env, { constants: { ...this.constants, + ...this.dlfPlayer.getConstants(), // TODO: Refactor forceLandscapeOnFullscreen: Number(this.constants.forceLandscapeOnFullscreen), }, @@ -218,7 +216,7 @@ export default class SlubMediaPlayer { screenshot: new ScreenshotModal(this.fullscreenElement, this.env, { keybindings: this.keybindings, screnshotCaptions: this.config.screenshotCaptions ?? [], - constants: this.constants, + constants: this.config.constants ?? {}, }), }); @@ -316,7 +314,6 @@ export default class SlubMediaPlayer { onClick: this.actions['modal.help.open'], }) ); - this.dlfPlayer.setConstants(this.constants); this.dlfPlayer.setLocale(this.config.lang.twoLetterIsoCode); if (this.videoInfo.url.poster !== undefined) { this.dlfPlayer.setPoster(this.videoInfo.url.poster); diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/modals/ScreenshotModal.js b/Resources/Private/JavaScript/SlubMediaPlayer/modals/ScreenshotModal.js index 0427f13..bd16421 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/modals/ScreenshotModal.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/modals/ScreenshotModal.js @@ -29,14 +29,9 @@ import typoConstants from '../../lib/typoConstants'; * }} State * * @typedef {{ - * screenshotFilenameTemplate: string; - * screenshotCommentTemplate: string; - * }} Constants - * - * @typedef {{ * keybindings: Keybinding[]; * screnshotCaptions: import('../Screenshot').ScreenshotCaption[]; - * constants: import('../../lib/typoConstants').TypoConstants; + * constants: import('../../lib/typoConstants').TypoConstants; * }} Config */ diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts b/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts index 5795134..f0b1c27 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts @@ -48,7 +48,7 @@ type LangDef = { phrases: PhrasesDict; }; -type AppConstants = { +type ScreenshotModalConstants = { /** * Template for filename when downloading screenshot (without extension). */ @@ -58,35 +58,18 @@ type AppConstants = { * Template for comment added to metadata of screenshot image file. */ screenshotCommentTemplate: string; +}; - /** - * Number of seconds in which to still rewind to previous chapter. - */ - prevChapterTolerance: number; - - /** - * Volume increase/decrease in relevant keybinding. - */ - volumeStep: number; - - /** - * Number of seconds to seek or rewind in relevant keybinding. - */ - seekStep: number; - - /** - * Trick play factor for continuous rewind/seek. - * TODO: Check if this should be input as setting or retrieved from current manifest - */ - trickPlayFactor: number, - +type AppConstants = { /** * Whether or not to switch to landscape in fullscreen mode. */ forceLandscapeOnFullscreen: boolean; }; -type AppConstantsConfig = import('../lib/typoConstants').TypoConstants; +type AppConstantsConfig = import("../lib/typoConstants").TypoConstants< + dlf.media.PlayerConstants & ScreenshotModalConstants & AppConstants +>; type AppConfig = { shareButtons: import("./modals/BookmarkModal").ShareButtonInfo[]; From 9fcf1ff895bf93aec228355b44324b13f8cfb275 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Fri, 8 Apr 2022 11:38:58 +0200 Subject: [PATCH 07/25] Move actions and gestures to DlfMediaPlayer --- .../DlfMediaPlayer/DlfMediaPlayer.js | 151 +++++++++++++++++- .../SlubMediaPlayer/SlubMediaPlayer.js | 142 +--------------- 2 files changed, 153 insertions(+), 140 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index ddf2a74..ccf1a53 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -5,6 +5,7 @@ import 'shaka-player/ui/controls.less'; import VideoFrame from './vendor/VideoFrame'; +import Gestures from '../lib/Gestures'; import typoConstants from '../lib/typoConstants'; import { clamp, e, setElementClass } from '../lib/util'; import Chapters from './Chapters'; @@ -125,6 +126,83 @@ export default class DlfMediaPlayer { onManualSeek: this.onManualSeek.bind(this), }; + this.registerEventHandlers(); + + /** @readonly */ + this.actions = { + 'fullscreen.toggle': () => { + // Override in application + }, + 'playback.toggle': () => { + if (this.paused) { + this.play(); + } else { + this.pause(); + } + }, + 'playback.volume.mute.toggle': () => { + this.muted = !this.muted; + }, + 'playback.volume.inc': () => { + this.volume = this.volume + this.constants.volumeStep; + }, + 'playback.volume.dec': () => { + this.volume = this.volume - this.constants.volumeStep; + }, + 'playback.captions.toggle': () => { + this.showCaptions = !this.showCaptions; + }, + 'navigate.rewind': () => { + this.skipSeconds(-this.constants.seekStep); + }, + 'navigate.seek': () => { + this.skipSeconds(+this.constants.seekStep); + }, + 'navigate.continuous-rewind': () => { + this.ensureTrickPlay(-this.constants.trickPlayFactor); + }, + 'navigate.continuous-seek': () => { + this.ensureTrickPlay(this.constants.trickPlayFactor); + }, + 'navigate.chapter.prev': () => { + this.prevChapter(); + }, + 'navigate.chapter.next': () => { + this.nextChapter(); + }, + 'navigate.frame.prev': () => { + this.getVifa()?.seekBackward(1); + }, + 'navigate.frame.next': () => { + this.getVifa()?.seekForward(1); + }, + 'navigate.position.percental': ( + /** @type {Keybinding} */ kb, + /** @type {number} */ keyIndex + ) => { + if (0 <= keyIndex && keyIndex < kb.keys.length) { + // Implies kb.keys.length > 0 + + const relative = keyIndex / kb.keys.length; + const absolute = relative * this.video.duration; + + this.seekTo(absolute); + } + }, + 'navigate.thumbnails.snap': ( + /** @type {Keybinding} */ _kb, + /** @type {number} */ _keyIndex, + /** @type {KeyEventMode} */ mode + ) => { + this.seekBar?.setThumbnailSnap(mode === 'down'); + }, + } + } + + /** + * @private + */ + registerEventHandlers() { this.player.addEventListener('error', this.handlers.onErrorEvent); this.controls.addEventListener('error', this.handlers.onErrorEvent); @@ -142,6 +220,75 @@ export default class DlfMediaPlayer { this.controls.addEventListener('timeandseekrangeupdated', this.handlers.onTimeUpdate); this.video.addEventListener('play', this.handlers.onPlay); + + this.registerGestures(); + } + + /** + * @private + */ + registerGestures() { + const g = new Gestures(); + g.register(this.container); + + g.on('gesture', (e) => { + if (e.event.clientY >= this.userArea.bottom) { + return; + } + + if (!this.isUserAreaEvent(e.event)) { + return; + } + + switch (e.type) { + case 'tapup': + if (e.event.pointerType === 'mouse') { + if (e.tapCount <= 2) { + this.actions['playback.toggle'](); + } + + if (e.tapCount === 2) { + this.actions['fullscreen.toggle'](); + } + } else if (e.tapCount >= 2) { + if (e.position.x < 1 / 3) { + this.actions['navigate.rewind'](); + } else if (e.position.x > 2 / 3) { + this.actions['navigate.seek'](); + } else if (e.tapCount === 2 && !this.env.isInFullScreen()) { + this.actions['fullscreen.toggle'](); + } + } + break; + + case 'hold': + if (e.tapCount === 1) { + // TODO: Somehow extract an action "navigate.relative-seek"? How to pass clientX? + this.seekBar?.thumbnailPreview?.beginChange(e.event.clientX); + } else if (e.tapCount >= 2) { + if (e.position.x < 1 / 3) { + this.actions['navigate.continuous-rewind'](); + } else if (e.position.x > 2 / 3) { + this.actions['navigate.continuous-seek'](); + } + } + break; + + case 'swipe': + // "Natural" swiping + if (e.direction === 'east') { + this.actions['navigate.rewind'](); + } else if (e.direction === 'west') { + this.actions['navigate.seek'](); + } + break; + } + }); + + g.on('release', () => { + this.seekBar?.endSeek(); + this.cancelTrickPlay(); + }); } /** @@ -302,10 +449,6 @@ export default class DlfMediaPlayer { setElementClass(this.errorBox, 'dlf-visible', langKey !== null); } - getContainer() { - return this.container; - } - /** * Check if the event {@link e} interacts with user area (e.g., isn't clicking * the big play button). diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index 67a057e..e17a44a 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -1,6 +1,5 @@ // @ts-check -import Gestures from '../lib/Gestures'; import { e } from '../lib/util'; import { Keybindings$find } from '../lib/Keyboard'; import typoConstants from '../lib/typoConstants'; @@ -74,8 +73,6 @@ export default class SlubMediaPlayer { this.dlfPlayer = new DlfMediaPlayer(this.env); this.dlfPlayer.parseConstants(config.constants ?? {}); - const playerConstants = this.dlfPlayer.getConstants(); - /** @private @type {ChapterLink[]} */ this.chapterLinks = []; @@ -83,7 +80,11 @@ export default class SlubMediaPlayer { this.modals = null; /** @private */ - this.actions = { + this.dlfPlayer.actions['fullscreen.toggle'] = () => { + this.dlfPlayer.seekBar?.endSeek(); + this.toggleFullScreen(); + }; + this.actions = Object.assign({}, this.dlfPlayer.actions, { 'cancel': () => { if (this.modals?.hasOpen()) { this.modals.closeNext(); @@ -111,10 +112,6 @@ export default class SlubMediaPlayer { 'modal.screenshot.snap': () => { this.snapScreenshot(); }, - 'fullscreen.toggle': () => { - this.dlfPlayer.seekBar?.endSeek(); - this.toggleFullScreen(); - }, 'theater.toggle': () => { this.dlfPlayer.seekBar?.endSeek(); @@ -129,70 +126,7 @@ export default class SlubMediaPlayer { }); window.dispatchEvent(ev); }, - 'playback.toggle': () => { - if (this.dlfPlayer.paused) { - this.dlfPlayer.play(); - } else { - this.dlfPlayer.pause(); - } - }, - 'playback.volume.mute.toggle': () => { - this.dlfPlayer.muted = !this.dlfPlayer.muted; - }, - 'playback.volume.inc': () => { - this.dlfPlayer.volume = this.dlfPlayer.volume + playerConstants.volumeStep; - }, - 'playback.volume.dec': () => { - this.dlfPlayer.volume = this.dlfPlayer.volume - playerConstants.volumeStep; - }, - 'playback.captions.toggle': () => { - this.dlfPlayer.showCaptions = !this.dlfPlayer.showCaptions; - }, - 'navigate.rewind': () => { - this.dlfPlayer.skipSeconds(-playerConstants.seekStep); - }, - 'navigate.seek': () => { - this.dlfPlayer.skipSeconds(+playerConstants.seekStep); - }, - 'navigate.continuous-rewind': () => { - this.dlfPlayer.ensureTrickPlay(-playerConstants.trickPlayFactor); - }, - 'navigate.continuous-seek': () => { - this.dlfPlayer.ensureTrickPlay(playerConstants.trickPlayFactor); - }, - 'navigate.chapter.prev': () => { - this.dlfPlayer.prevChapter(); - }, - 'navigate.chapter.next': () => { - this.dlfPlayer.nextChapter(); - }, - 'navigate.frame.prev': () => { - this.dlfPlayer.getVifa()?.seekBackward(1); - }, - 'navigate.frame.next': () => { - this.dlfPlayer.getVifa()?.seekForward(1); - }, - 'navigate.position.percental': ( - /** @type {Keybinding} */ kb, - /** @type {number} */ keyIndex - ) => { - if (0 <= keyIndex && keyIndex < kb.keys.length) { - // Implies kb.keys.length > 0 - - const relative = keyIndex / kb.keys.length; - const absolute = relative * this.dlfPlayer.getVideo().duration; - - this.dlfPlayer.seekTo(absolute); - } - }, - 'navigate.thumbnails.snap': ( - /** @type {Keybinding} */ _kb, - /** @type {number} */ _keyIndex, - /** @type {KeyEventMode} */ mode - ) => { - this.dlfPlayer.seekBar?.setThumbnailSnap(mode === 'down'); - }, - }; + }); this.createModals(); this.load(); @@ -336,70 +270,6 @@ export default class SlubMediaPlayer { registerEventHandlers() { document.addEventListener('keydown', this.handlers.onKeyDown); document.addEventListener('keyup', this.handlers.onKeyUp, { capture: true }); - - // TODO: Move actions to DlfMediaPlayer, then also move gesture detection there - - const g = new Gestures(); - g.register(this.dlfPlayer.getContainer()); - - g.on('gesture', (e) => { - if (e.event.clientY >= this.dlfPlayer.userArea.bottom) { - return; - } - - if (!this.dlfPlayer.isUserAreaEvent(e.event)) { - return; - } - - switch (e.type) { - case 'tapup': - if (e.event.pointerType === 'mouse') { - if (e.tapCount <= 2) { - this.actions['playback.toggle'](); - } - - if (e.tapCount === 2) { - this.actions['fullscreen.toggle'](); - } - } else if (e.tapCount >= 2) { - if (e.position.x < 1 / 3) { - this.actions['navigate.rewind'](); - } else if (e.position.x > 2 / 3) { - this.actions['navigate.seek'](); - } else if (e.tapCount === 2 && !this.env.isInFullScreen()) { - this.actions['fullscreen.toggle'](); - } - } - break; - - case 'hold': - if (e.tapCount === 1) { - // TODO: Somehow extract an action "navigate.relative-seek"? How to pass clientX? - this.dlfPlayer.seekBar?.thumbnailPreview?.beginChange(e.event.clientX); - } else if (e.tapCount >= 2) { - if (e.position.x < 1 / 3) { - this.actions['navigate.continuous-rewind'](); - } else if (e.position.x > 2 / 3) { - this.actions['navigate.continuous-seek'](); - } - } - break; - - case 'swipe': - // "Natural" swiping - if (e.direction === 'east') { - this.actions['navigate.rewind'](); - } else if (e.direction === 'west') { - this.actions['navigate.seek'](); - } - break; - } - }); - - g.on('release', () => { - this.dlfPlayer.seekBar?.endSeek(); - this.dlfPlayer.cancelTrickPlay(); - }); } /** From 75b7e6fcff43d70c3f68f869457d65d74a973edf Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Sat, 14 May 2022 13:01:38 +0200 Subject: [PATCH 08/25] Fix: Hide poster when seeking frame --- Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index ccf1a53..769fc34 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -172,9 +172,11 @@ export default class DlfMediaPlayer { }, 'navigate.frame.prev': () => { this.getVifa()?.seekBackward(1); + this.emitControlEvent('dlf-media-manual-seek', {}); }, 'navigate.frame.next': () => { this.getVifa()?.seekForward(1); + this.emitControlEvent('dlf-media-manual-seek', {}); }, 'navigate.position.percental': ( /** @type {Keybinding} */ kb, From 0c558d38cf4af138e806977b6dd0108e1047977f Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Sat, 14 May 2022 15:45:35 +0200 Subject: [PATCH 09/25] Refactor: Merge `vifa`/`fps` into `Fps` object --- .../DlfMediaPlayer/DlfMediaPlayer.js | 36 +++++++------------ .../DlfMediaPlayer/controls/FlatSeekBar.js | 4 ++- .../controls/PresentationTimeTracker.js | 19 +++++----- .../JavaScript/DlfMediaPlayer/types.d.ts | 8 +++-- 4 files changed, 30 insertions(+), 37 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 769fc34..a8b6dda 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -106,10 +106,7 @@ export default class DlfMediaPlayer { /** @private @type {FlatSeekBar | null} */ this.seekBar_ = null; - /** @private @type {VideoFrame | null} */ - this.vifa = null; - - /** @private @type {number | null} */ + /** @private @type {dlf.media.Fps | null} */ this.fps = null; /** @private @type {VariantGroups | null} */ @@ -171,11 +168,11 @@ export default class DlfMediaPlayer { this.nextChapter(); }, 'navigate.frame.prev': () => { - this.getVifa()?.seekBackward(1); + this.fps?.vifa.seekBackward(1); this.emitControlEvent('dlf-media-manual-seek', {}); }, 'navigate.frame.next': () => { - this.getVifa()?.seekForward(1); + this.fps?.vifa.seekForward(1); this.emitControlEvent('dlf-media-manual-seek', {}); }, 'navigate.position.percental': ( @@ -522,16 +519,17 @@ export default class DlfMediaPlayer { if (fps === null) { this.fps = null; - this.vifa = null; - } else if (fps !== this.fps) { - this.fps = fps; - this.vifa = new VideoFrame({ - id: this.video.id, - frameRate: fps, - }); + } else if (this.fps === null || fps !== this.fps.rate) { + this.fps = { + rate: fps, + vifa: new VideoFrame({ + id: this.video.id, + frameRate: fps, + }), + }; } - this.emitControlEvent('dlf-media-fps', { vifa: this.vifa, fps: this.fps }); + this.emitControlEvent('dlf-media-fps', { fps: this.fps }); } onTimeUpdate() { @@ -766,15 +764,7 @@ export default class DlfMediaPlayer { * @returns {number | null} */ getFps() { - return this.fps; - } - - /** - * - * @returns {VideoFrame | null} - */ - getVifa() { - return this.vifa; + return this.fps?.rate ?? null; } /** diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js index 71081d5..6523705 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js @@ -138,7 +138,9 @@ export default class FlatSeekBar extends shaka.ui.Element { this.eventManager.listen(this.controls, 'dlf-media-fps', (e) => { const detail = /** @type {dlf.media.FpsEvent} */(e).detail; - this.dlf.thumbnailPreview?.setFps(detail.fps); + if (detail.fps) { + this.dlf.thumbnailPreview?.setFps(detail.fps?.rate); + } }); this.controls?.dispatchEvent(/** @type {dlf.media.SeekBarEvent} */( diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/PresentationTimeTracker.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/PresentationTimeTracker.js index ee41e95..38d0d68 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/PresentationTimeTracker.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/PresentationTimeTracker.js @@ -28,8 +28,7 @@ const TimeMode = { * activeMode: number; * duration: number; * totalSeconds: number; - * vifa: VideoFrame | null; - * fps: number | null; + * fps: dlf.media.Fps | null; * chapters: Chapters | null; * }} State */ @@ -88,7 +87,6 @@ export default class PresentationTimeTracker extends shaka.ui.Element { activeMode: TimeMode.CurrentTime, totalSeconds: 0, duration: 0, - vifa: null, fps: null, chapters: null, }; @@ -113,7 +111,6 @@ export default class PresentationTimeTracker extends shaka.ui.Element { this.eventManager.listen(this.controls, 'dlf-media-fps', (e) => { const detail = /** @type {dlf.media.FpsEvent} */(e).detail; this.render({ - vifa: detail.vifa, fps: detail.fps, }); }); @@ -165,32 +162,32 @@ export default class PresentationTimeTracker extends shaka.ui.Element { /** * * @param {TimeModeKey} tKey - * @param {Pick} state + * @param {Pick} state * @returns {string} */ - getTimecodeText(tKey, { isReady, totalSeconds, duration, vifa, fps, chapters }) { + getTimecodeText(tKey, { isReady, totalSeconds, duration, fps, chapters }) { // Don't show incomplete info when duration is not yet available if (!isReady || duration === 0) { return this.dlf.env.t('player.loading'); } else { const showHour = duration >= 3600; + const fpsRate = fps?.rate ?? null; const textValues = { get chapterTitle() { return chapters?.timeToChapter(totalSeconds)?.title ?? "_"; }, get currentTime() { - return buildTimeString(totalSeconds, showHour, fps); + return buildTimeString(totalSeconds, showHour, fpsRate); }, get totalTime() { - return buildTimeString(duration, showHour, fps); + return buildTimeString(duration, showHour, fpsRate); }, get remainingTime() { - return buildTimeString(duration - totalSeconds, showHour, fps); + return buildTimeString(duration - totalSeconds, showHour, fpsRate); }, get currentFrame() { - return vifa?.get() ?? -1; + return fps?.vifa.get() ?? -1; }, }; diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index ed9509f..9f8f6c2 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -48,6 +48,11 @@ namespace dlf { minBottomControlsReadyState: number; }; + type Fps = { + rate: number; + vifa: import("./vendor/VideoFrame").default; + }; + /** * Signals chapters available in current video. * @@ -92,8 +97,7 @@ namespace dlf { chapters: import("./Chapters").default; }; "dlf-media-fps": { - vifa: import("./vendor/VideoFrame").default | null; - fps: number | null; + fps: Fps | null; }; "dlf-media-seek-bar": { seekBar: import("./controls/FlatSeekBar").default; From 47805b55257492e393c8665ee78990efc5599915 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Sat, 14 May 2022 16:04:07 +0200 Subject: [PATCH 10/25] Refactor: Extract `allowGesture()` callback --- .../DlfMediaPlayer/DlfMediaPlayer.js | 54 +++++++++---------- Resources/Private/JavaScript/lib/Gestures.js | 7 ++- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index a8b6dda..bfe6871 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -227,18 +227,12 @@ export default class DlfMediaPlayer { * @private */ registerGestures() { - const g = new Gestures(); + const g = new Gestures({ + allowGesture: this.allowGesture.bind(this), + }); g.register(this.container); g.on('gesture', (e) => { - if (e.event.clientY >= this.userArea.bottom) { - return; - } - - if (!this.isUserAreaEvent(e.event)) { - return; - } - switch (e.type) { case 'tapup': if (e.event.pointerType === 'mouse') { @@ -290,6 +284,27 @@ export default class DlfMediaPlayer { }); } + /** + * + * @param {PointerEvent} event + */ + allowGesture(event) { + // Don't allow gestures over Shaka bottom controls + const bounding = this.videoBox.getBoundingClientRect(); + const controlsHeight = this.shakaBottomControls?.getBoundingClientRect().height ?? 0; + const userAreaBottom = bounding.bottom - controlsHeight - 20; + if (event.clientY >= userAreaBottom) { + return false; + } + + // Check that the pointer interacts with the container, so isn't over the button + if (event.target !== this.videoBox.querySelector('.shaka-play-button-container')) { + return false; + } + + return true; + } + /** * Determines whether or not the player supports playback of videos in the * given mime type. @@ -448,27 +463,6 @@ export default class DlfMediaPlayer { setElementClass(this.errorBox, 'dlf-visible', langKey !== null); } - /** - * Check if the event {@link e} interacts with user area (e.g., isn't clicking - * the big play button). - * - * @param {PointerEvent} e - */ - isUserAreaEvent(e) { - return e.target === this.videoBox.querySelector('.shaka-play-button-container'); - } - - /** - * Area of the player that may be used for user interaction. - * - * @type {DOMRect} - */ - get userArea() { - const bounding = this.videoBox.getBoundingClientRect(); - const controlsHeight = this.shakaBottomControls?.getBoundingClientRect().height ?? 0; - return new DOMRect(bounding.x, bounding.y, bounding.width, bounding.height - controlsHeight - 20); - } - async load() { if (this.sources_.length === 0) { this.showError('error.playback-not-supported'); diff --git a/Resources/Private/JavaScript/lib/Gestures.js b/Resources/Private/JavaScript/lib/Gestures.js index c9ab471..d432c36 100644 --- a/Resources/Private/JavaScript/lib/Gestures.js +++ b/Resources/Private/JavaScript/lib/Gestures.js @@ -52,6 +52,7 @@ import EventEmitter from 'events'; * tapMaxDistance: number; * swipeMinDistance: number; * holdMinDelay: number; + * allowGesture: (event: PointerEvent) => boolean; * }} Config */ @@ -69,6 +70,7 @@ export default class Gestures { tapMaxDistance: 20, swipeMinDistance: 100, holdMinDelay: 200, // TODO: Use something more dynamic, such as difference to double click? + allowGesture: () => true, ...config }; @@ -144,7 +146,8 @@ export default class Gestures { */ handlePointerDown(e) { // Release if non-left mouse button is clicked - if (e.button !== 0) { + // (or gesture is not allowed for this event) + if (e.button !== 0 || !this.config.allowGesture(e)) { this.release(); return; } @@ -178,7 +181,7 @@ export default class Gestures { * @param {PointerEvent} e */ handlePointerUp(e) { - if (e.button !== 0) { + if (e.button !== 0 || !this.config.allowGesture(e)) { return; } From f2c98388d3391a11c47992bb30e88645cd3b6b42 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Sat, 14 May 2022 17:08:55 +0200 Subject: [PATCH 11/25] Refactor: Start extracting `ShakaFrontend` to decouple UI --- .../DlfMediaPlayer/DlfMediaPlayer.js | 35 ++++++--------- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 43 +++++++++++++++++++ .../JavaScript/DlfMediaPlayer/types.d.ts | 7 +++ .../Less/DlfMediaPlayer/DlfMediaPlayer.less | 23 +--------- .../Less/DlfMediaPlayer/ShakaFrontend.less | 22 ++++++++++ 5 files changed, 87 insertions(+), 43 deletions(-) create mode 100644 Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js create mode 100644 Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index bfe6871..1bdfa26 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -8,6 +8,7 @@ import VideoFrame from './vendor/VideoFrame'; import Gestures from '../lib/Gestures'; import typoConstants from '../lib/typoConstants'; import { clamp, e, setElementClass } from '../lib/util'; +import ShakaFrontend from './frontend/ShakaFrontend'; import Chapters from './Chapters'; import { FlatSeekBar, @@ -45,23 +46,17 @@ export default class DlfMediaPlayer { /** @private @type {HTMLElement | null} */ this.mountPoint = null; - /** @private @type {HTMLElement} */ - this.container = e('div', { className: "dlf-media-player" }); - /** @private @type {HTMLVideoElement} */ this.video = e('video', { id: this.env.mkid(), className: "dlf-media", }); - this.poster = e('img', { - className: "dlf-media-poster dlf-visible", - $error: () => { - this.hidePoster(); - }, - }); - this.videoBox = e('div', { className: "dlf-media-shaka-box" }, [this.video, this.poster]); - this.errorBox = e('div', { className: "dlf-media-shaka-box dlf-media-error" }); - this.container.append(this.videoBox, this.errorBox); + + /** @private */ + this.frontend = new ShakaFrontend(this.video); + this.poster = this.frontend.$poster; + this.videoBox = this.frontend.$videoBox; + this.errorBox = this.frontend.$errorBox; /** * The object that has caused current pause state, if any. @@ -230,7 +225,7 @@ export default class DlfMediaPlayer { const g = new Gestures({ allowGesture: this.allowGesture.bind(this), }); - g.register(this.container); + g.register(this.frontend.domElement); g.on('gesture', (e) => { switch (e.type) { @@ -430,7 +425,7 @@ export default class DlfMediaPlayer { this.shakaBottomControls = this.videoBox.querySelector('.shaka-bottom-controls'); - mount.replaceWith(this.container); + mount.replaceWith(this.frontend.domElement); this.mountPoint = mount; @@ -446,7 +441,7 @@ export default class DlfMediaPlayer { unmount() { if (this.mountPoint !== null) { - this.container.replaceWith(this.mountPoint); + this.frontend.domElement.replaceWith(this.mountPoint); this.mountPoint = null; } } @@ -539,17 +534,13 @@ export default class DlfMediaPlayer { // Hide poster once playback has started the first time // This is necessary because "onTimeUpdate" may be fired with a delay - this.hidePoster(); + this.frontend.hidePoster(); } onManualSeek() { // Hide poster when seeking in pause mode before playback has started // We don't want to hide the poster when initial timecode is used - this.hidePoster(); - } - - hidePoster() { - this.poster.classList.remove('dlf-visible'); + this.frontend.hidePoster(); } /** @@ -772,7 +763,7 @@ export default class DlfMediaPlayer { this.video.currentTime = position.timecode; } - this.hidePoster(); + this.frontend.hidePoster(); } /** diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js new file mode 100644 index 0000000..ce52057 --- /dev/null +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -0,0 +1,43 @@ +// @ts-check + +import { e } from '../../lib/util'; + +/** + * @implements {dlf.media.PlayerFrontend} + */ +export default class ShakaFrontend { + /** + * + * @param {HTMLMediaElement} media + */ + constructor(media) { + /** @private */ + this.media = media; + + /** @private */ + this.$container = e('div', { + className: "dlf-media-player dlf-media-frontend-shaka" + }, [ + this.$videoBox = e('div', { className: "dlf-media-shaka-box" }, [ + this.$video = media, + this.$poster = e('img', { + className: "dlf-media-poster dlf-visible", + $error: () => { + this.hidePoster(); + }, + }), + ]), + this.$errorBox = e('div', { + className: "dlf-media-shaka-box dlf-media-error" + }), + ]); + } + + get domElement() { + return this.$container; + } + + hidePoster() { + this.$poster.classList.remove('dlf-visible'); + } +} diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index 9f8f6c2..699d321 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -53,6 +53,13 @@ namespace dlf { vifa: import("./vendor/VideoFrame").default; }; + interface PlayerFrontend { + /** + * Main DOM element / container of the frontend. + */ + get domElement(): HTMLElement; + } + /** * Signals chapters available in current video. * diff --git a/Resources/Private/Less/DlfMediaPlayer/DlfMediaPlayer.less b/Resources/Private/Less/DlfMediaPlayer/DlfMediaPlayer.less index 7ee224d..f6ff8c8 100644 --- a/Resources/Private/Less/DlfMediaPlayer/DlfMediaPlayer.less +++ b/Resources/Private/Less/DlfMediaPlayer/DlfMediaPlayer.less @@ -1,5 +1,7 @@ @import (reference) "shaka-player/ui/controls.less"; +@import "ShakaFrontend.less"; + .dlf-media-player { .noselect(); } @@ -25,27 +27,6 @@ } } -.dlf-media-shaka-box { - background-color: black; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -.dlf-media-error { - display: none; - color: white; - z-index: 1; - - &.dlf-visible { - display: flex; - justify-content: center; - align-items: center; - } -} - .shaka-video-container { height: 100%; } diff --git a/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less b/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less new file mode 100644 index 0000000..c54e8a1 --- /dev/null +++ b/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less @@ -0,0 +1,22 @@ +.dlf-media-frontend-shaka { + .dlf-media-shaka-box { + background-color: black; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + .dlf-media-error { + display: none; + color: white; + z-index: 1; + + &.dlf-visible { + display: flex; + justify-content: center; + align-items: center; + } + } +} From 03b912e78b790cc826b0d833966137184a98b072 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Sat, 14 May 2022 17:35:33 +0200 Subject: [PATCH 12/25] Move Shaka UI to `ShakaFrontend` --- .../DlfMediaPlayer/DlfMediaPlayer.js | 107 ++++-------------- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 91 ++++++++++++++- .../SlubMediaPlayer/SlubMediaPlayer.js | 52 +++++---- 3 files changed, 141 insertions(+), 109 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 1bdfa26..7936dab 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -1,7 +1,6 @@ // @ts-check import shaka from 'shaka-player/dist/shaka-player.ui'; -import 'shaka-player/ui/controls.less'; import VideoFrame from './vendor/VideoFrame'; @@ -10,11 +9,7 @@ import typoConstants from '../lib/typoConstants'; import { clamp, e, setElementClass } from '../lib/util'; import ShakaFrontend from './frontend/ShakaFrontend'; import Chapters from './Chapters'; -import { - FlatSeekBar, - PresentationTimeTracker, - VideoTrackSelection, -} from './controls'; +import { FlatSeekBar } from './controls'; import VariantGroups from './VariantGroups'; export default class DlfMediaPlayer { @@ -52,12 +47,6 @@ export default class DlfMediaPlayer { className: "dlf-media", }); - /** @private */ - this.frontend = new ShakaFrontend(this.video); - this.poster = this.frontend.$poster; - this.videoBox = this.frontend.$videoBox; - this.errorBox = this.frontend.$errorBox; - /** * The object that has caused current pause state, if any. * @@ -74,27 +63,12 @@ export default class DlfMediaPlayer { /** @private @type {number | null} */ this.startTime = null; - /** @private @type {string[]} */ - this.controlPanelButtons = []; - - /** @private @type {string[]} */ - this.overflowMenuButtons = []; - /** @private @type {shaka.Player} */ this.player = new shaka.Player(this.video); - /** @private @type {shaka.ui.Overlay} */ - this.ui = new shaka.ui.Overlay(this.player, this.videoBox, this.video); - - /** @private @type {shaka.ui.Controls} */ - this.controls = /** @type {shaka.ui.Controls} */(this.ui.getControls()); - /** @private */ this.lastReadyState = 0; - /** @private @type {HTMLElement | null} */ - this.shakaBottomControls = null; - /** @private @type {Event[]} */ this.controlEventQueue = []; @@ -110,6 +84,12 @@ export default class DlfMediaPlayer { /** @private @type {Chapters} */ this.chapters = new Chapters([]); + /** @private */ + this.frontend = new ShakaFrontend(this.env, this.player, this.video); + this.poster = this.frontend.$poster; + this.videoBox = this.frontend.$videoBox; + this.errorBox = this.frontend.$errorBox; + this.handlers = { onErrorEvent: this.onErrorEvent.bind(this), onTrackChange: this.onTrackChange.bind(this), @@ -193,6 +173,14 @@ export default class DlfMediaPlayer { } } + get controls() { + return this.frontend.controls; + } + + get shakaBottomControls() { + return this.frontend.shakaBottomControls; + } + /** * @private */ @@ -300,6 +288,13 @@ export default class DlfMediaPlayer { return true; } + /** + * @returns {dlf.media.PlayerFrontend} + */ + get ui() { + return this.frontend; + } + /** * Determines whether or not the player supports playback of videos in the * given mime type. @@ -355,22 +350,6 @@ export default class DlfMediaPlayer { this.poster.src = posterUrl; } - /** - * - * @param {string[]} elementKey - */ - addControlElement(...elementKey) { - this.controlPanelButtons.push(...elementKey); - } - - /** - * - * @param {string[]} elementKey - */ - addOverflowButton(...elementKey) { - this.overflowMenuButtons.push(...elementKey); - } - /** * Configures the Shaka player UI and mounts it into {@link mount}. The mount * point is being replaced with the player until {@link unmount} is called. @@ -383,48 +362,6 @@ export default class DlfMediaPlayer { return false; } - // TODO: Somehow avoid overriding the SeekBar globally? - FlatSeekBar.register(); - - // TODO: Refactor insertion at custom position (left or right of fullscreen) - this.ui.configure({ - addSeekBar: true, - enableTooltips: true, - controlPanelElements: [ - 'play_pause', - PresentationTimeTracker.register(this.env), - 'spacer', - 'volume', - 'mute', - ...this.controlPanelButtons, - 'overflow_menu', - ], - overflowMenuButtons: [ - 'language', - VideoTrackSelection.register(this.env), - 'playback_rate', - 'loop', - 'quality', - 'picture_in_picture', - 'captions', - ...this.overflowMenuButtons, - ], - addBigPlayButton: true, - seekBarColors: { - base: 'rgba(255, 255, 255, 0.3)', - buffered: 'rgba(255, 255, 255, 0.54)', - played: 'rgb(255, 255, 255)', - adBreaks: 'rgb(255, 204, 0)', - }, - enableKeyboardPlaybackControls: false, - doubleClickForFullscreen: false, - singleClickForPlayAndPause: false, - }); - - // Set again after `ui.configure()` - this.shakaBottomControls = - this.videoBox.querySelector('.shaka-bottom-controls'); - mount.replaceWith(this.frontend.domElement); this.mountPoint = mount; diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index ce52057..ea483ca 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -1,6 +1,14 @@ // @ts-check +import shaka from 'shaka-player/dist/shaka-player.ui'; +import 'shaka-player/ui/controls.less'; + import { e } from '../../lib/util'; +import { + FlatSeekBar, + PresentationTimeTracker, + VideoTrackSelection +} from '../controls'; /** * @implements {dlf.media.PlayerFrontend} @@ -8,12 +16,29 @@ import { e } from '../../lib/util'; export default class ShakaFrontend { /** * + * @param {Translator & Identifier} env + * @param {shaka.Player} player * @param {HTMLMediaElement} media */ - constructor(media) { + constructor(env, player, media) { + /** @private */ + this.env = env; + + /** @private */ + this.player = player; + /** @private */ this.media = media; + /** @private @type {string[]} */ + this.controlPanelButtons = []; + + /** @private @type {string[]} */ + this.overflowMenuButtons = []; + + /** @type {HTMLElement | null} */ + this.shakaBottomControls = null; + /** @private */ this.$container = e('div', { className: "dlf-media-player dlf-media-frontend-shaka" @@ -31,12 +56,76 @@ export default class ShakaFrontend { className: "dlf-media-shaka-box dlf-media-error" }), ]); + + /** @private */ + this.ui = new shaka.ui.Overlay(this.player, this.$videoBox, this.media); + + this.controls = /** @type {shaka.ui.Controls} */(this.ui.getControls()); } get domElement() { return this.$container; } + /** + * + * @param {string[]} elementKey + */ + addControlElement(...elementKey) { + this.controlPanelButtons.push(...elementKey); + } + + /** + * + * @param {string[]} elementKey + */ + addOverflowButton(...elementKey) { + this.overflowMenuButtons.push(...elementKey); + } + + configure() { + // TODO: Somehow avoid overriding the SeekBar globally? + FlatSeekBar.register(); + + this.ui.configure({ + addSeekBar: true, + enableTooltips: true, + controlPanelElements: [ + 'play_pause', + PresentationTimeTracker.register(this.env), + 'spacer', + 'volume', + 'mute', + ...this.controlPanelButtons, + 'overflow_menu', + ], + overflowMenuButtons: [ + 'language', + VideoTrackSelection.register(this.env), + 'playback_rate', + 'loop', + 'quality', + 'picture_in_picture', + 'captions', + ...this.overflowMenuButtons, + ], + addBigPlayButton: true, + seekBarColors: { + base: 'rgba(255, 255, 255, 0.3)', + buffered: 'rgba(255, 255, 255, 0.54)', + played: 'rgb(255, 255, 255)', + adBreaks: 'rgb(255, 204, 0)', + }, + enableKeyboardPlaybackControls: false, + doubleClickForFullscreen: false, + singleClickForPlayAndPause: false, + }); + + // DOM is (re-)created in `ui.configure()`, so query container afterwards + this.shakaBottomControls = + this.$videoBox.querySelector('.shaka-bottom-controls'); + } + hidePoster() { this.$poster.classList.remove('dlf-visible'); } diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index e17a44a..d3a825a 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -9,6 +9,7 @@ import { DlfMediaPlayer, FullScreenButton, } from '../DlfMediaPlayer'; +import ShakaFrontend from '../DlfMediaPlayer/frontend/ShakaFrontend'; import Modals from './lib/Modals'; import { BookmarkModal, HelpModal, ScreenshotModal } from './modals'; @@ -225,29 +226,34 @@ export default class SlubMediaPlayer { const startTime = this.getStartTime(chapters); - this.dlfPlayer.addControlElement( - ControlPanelButton.register(this.env, { - className: "sxnd-screenshot-button", - material_icon: 'photo_camera', - title: this.env.t('control.screenshot.tooltip'), - onClick: this.actions['modal.screenshot.open'], - }), - ControlPanelButton.register(this.env, { - className: "sxnd-bookmark-button", - material_icon: 'bookmark_border', - title: this.env.t('control.bookmark.tooltip'), - onClick: this.actions['modal.bookmark.open'], - }), - FullScreenButton.register(this.env, { - onClick: this.actions['fullscreen.toggle'], - }), - ControlPanelButton.register(this.env, { - className: "sxnd-help-button", - material_icon: 'info_outline', - title: this.env.t('control.help.tooltip'), - onClick: this.actions['modal.help.open'], - }) - ); + // TODO: How to deal with this check? + if (this.dlfPlayer.ui instanceof ShakaFrontend) { + this.dlfPlayer.ui.addControlElement( + ControlPanelButton.register(this.env, { + className: "sxnd-screenshot-button", + material_icon: 'photo_camera', + title: this.env.t('control.screenshot.tooltip'), + onClick: this.actions['modal.screenshot.open'], + }), + ControlPanelButton.register(this.env, { + className: "sxnd-bookmark-button", + material_icon: 'bookmark_border', + title: this.env.t('control.bookmark.tooltip'), + onClick: this.actions['modal.bookmark.open'], + }), + FullScreenButton.register(this.env, { + onClick: this.actions['fullscreen.toggle'], + }), + ControlPanelButton.register(this.env, { + className: "sxnd-help-button", + material_icon: 'info_outline', + title: this.env.t('control.help.tooltip'), + onClick: this.actions['modal.help.open'], + }) + ); + + this.dlfPlayer.ui.configure(); + } this.dlfPlayer.setLocale(this.config.lang.twoLetterIsoCode); if (this.videoInfo.url.poster !== undefined) { this.dlfPlayer.setPoster(this.videoInfo.url.poster); From 03587e0c0c370e3fccd82ebbfbe296a6253b669e Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Sat, 14 May 2022 17:48:24 +0200 Subject: [PATCH 13/25] Move seek bar handling to `ShakaFrontend` --- .../DlfMediaPlayer/DlfMediaPlayer.js | 24 +++---------------- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 23 ++++++++++++++++++ .../JavaScript/DlfMediaPlayer/types.d.ts | 4 ++++ .../SlubMediaPlayer/SlubMediaPlayer.js | 12 +++++----- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 7936dab..7da433a 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -9,7 +9,6 @@ import typoConstants from '../lib/typoConstants'; import { clamp, e, setElementClass } from '../lib/util'; import ShakaFrontend from './frontend/ShakaFrontend'; import Chapters from './Chapters'; -import { FlatSeekBar } from './controls'; import VariantGroups from './VariantGroups'; export default class DlfMediaPlayer { @@ -72,9 +71,6 @@ export default class DlfMediaPlayer { /** @private @type {Event[]} */ this.controlEventQueue = []; - /** @private @type {FlatSeekBar | null} */ - this.seekBar_ = null; - /** @private @type {dlf.media.Fps | null} */ this.fps = null; @@ -168,7 +164,7 @@ export default class DlfMediaPlayer { /** @type {number} */ _keyIndex, /** @type {KeyEventMode} */ mode ) => { - this.seekBar?.setThumbnailSnap(mode === 'down'); + this.frontend.seekBar?.setThumbnailSnap(mode === 'down'); }, } } @@ -191,12 +187,6 @@ export default class DlfMediaPlayer { this.player.addEventListener('adaptation', this.handlers.onTrackChange); this.player.addEventListener('variantchanged', this.handlers.onTrackChange); - // TODO: Figure out a good flow of events - this.controls.addEventListener('dlf-media-seek-bar', (e) => { - const detail = /** @type {dlf.media.SeekBarEvent} */(e).detail; - this.seekBar_ = detail.seekBar; - }); - this.controls.addEventListener('dlf-media-manual-seek', this.handlers.onManualSeek); this.controls.addEventListener('timeandseekrangeupdated', this.handlers.onTimeUpdate); @@ -240,7 +230,7 @@ export default class DlfMediaPlayer { case 'hold': if (e.tapCount === 1) { // TODO: Somehow extract an action "navigate.relative-seek"? How to pass clientX? - this.seekBar?.thumbnailPreview?.beginChange(e.event.clientX); + this.frontend.seekBar?.thumbnailPreview?.beginChange(e.event.clientX); } else if (e.tapCount >= 2) { if (e.position.x < 1 / 3) { this.actions['navigate.continuous-rewind'](); @@ -262,7 +252,7 @@ export default class DlfMediaPlayer { }); g.on('release', () => { - this.seekBar?.endSeek(); + this.frontend.seekBar?.endSeek(); this.cancelTrickPlay(); }); } @@ -334,14 +324,6 @@ export default class DlfMediaPlayer { this.constants = typoConstants(constants, this.constants); } - /** - * - * @returns {FlatSeekBar | null} - */ - get seekBar() { - return this.seekBar_; - } - /** * * @param {string} posterUrl diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index ea483ca..c90e05c 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -11,6 +11,9 @@ import { } from '../controls'; /** + * Listens to the following custom events: + * - {@link dlf.media.SeekBarEvent} + * * @implements {dlf.media.PlayerFrontend} */ export default class ShakaFrontend { @@ -39,6 +42,9 @@ export default class ShakaFrontend { /** @type {HTMLElement | null} */ this.shakaBottomControls = null; + /** @private @type {FlatSeekBar | null} */ + this.seekBar_ = null; + /** @private */ this.$container = e('div', { className: "dlf-media-player dlf-media-frontend-shaka" @@ -61,12 +67,29 @@ export default class ShakaFrontend { this.ui = new shaka.ui.Overlay(this.player, this.$videoBox, this.media); this.controls = /** @type {shaka.ui.Controls} */(this.ui.getControls()); + + this.registerEventHandlers(); + } + + /** + * @private + */ + registerEventHandlers() { + // TODO: Figure out a good flow of events + this.controls.addEventListener('dlf-media-seek-bar', (e) => { + const detail = /** @type {dlf.media.SeekBarEvent} */(e).detail; + this.seekBar_ = detail.seekBar; + }); } get domElement() { return this.$container; } + get seekBar() { + return this.seekBar_; + } + /** * * @param {string[]} elementKey diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index 699d321..f8e6656 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -58,6 +58,10 @@ namespace dlf { * Main DOM element / container of the frontend. */ get domElement(): HTMLElement; + + get seekBar(): + | import("../VideoPlayer/controls/FlatSeekBar").default + | null; } /** diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index d3a825a..d7c7ea9 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -82,15 +82,15 @@ export default class SlubMediaPlayer { /** @private */ this.dlfPlayer.actions['fullscreen.toggle'] = () => { - this.dlfPlayer.seekBar?.endSeek(); + this.dlfPlayer.ui.seekBar?.endSeek(); this.toggleFullScreen(); }; this.actions = Object.assign({}, this.dlfPlayer.actions, { 'cancel': () => { if (this.modals?.hasOpen()) { this.modals.closeNext(); - } else if (this.dlfPlayer.seekBar?.isThumbnailPreviewOpen() ?? false) { - this.dlfPlayer.seekBar?.endSeek(); + } else if (this.dlfPlayer.ui.seekBar?.isThumbnailPreviewOpen() ?? false) { + this.dlfPlayer.ui.seekBar?.endSeek(); } else if (this.dlfPlayer.anySettingsMenusAreOpen()) { this.dlfPlayer.hideSettingsMenus(); } @@ -100,7 +100,7 @@ export default class SlubMediaPlayer { }, 'modal.help.toggle': () => { if (this.modals !== null) { - this.dlfPlayer.seekBar?.endSeek(); + this.dlfPlayer.ui.seekBar?.endSeek(); this.modals.toggleExclusive(this.modals.help); } }, @@ -114,7 +114,7 @@ export default class SlubMediaPlayer { this.snapScreenshot(); }, 'theater.toggle': () => { - this.dlfPlayer.seekBar?.endSeek(); + this.dlfPlayer.ui.seekBar?.endSeek(); // @see DigitalcollectionsScripts.js // TODO: Make sure the theater mode isn't activated on startup; then stop persisting @@ -432,7 +432,7 @@ export default class SlubMediaPlayer { this.dlfPlayer.pauseOn(modal); } - this.dlfPlayer.seekBar?.endSeek(); + this.dlfPlayer.ui.seekBar?.endSeek(); modal.open(); } } From f8b35228163b21d92dfa17192d9bfb754a3cf1a2 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Sat, 14 May 2022 18:37:08 +0200 Subject: [PATCH 14/25] Move some UI interactivity to `ShakaFrontend` - `afterManualSeek()` - `handleEscape()` - Gesture initialization --- .../DlfMediaPlayer/DlfMediaPlayer.js | 57 +++-------------- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 61 +++++++++++++++++++ .../JavaScript/DlfMediaPlayer/types.d.ts | 21 +++++++ .../SlubMediaPlayer/SlubMediaPlayer.js | 6 +- 4 files changed, 91 insertions(+), 54 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 7da433a..90a34c0 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -4,7 +4,6 @@ import shaka from 'shaka-player/dist/shaka-player.ui'; import VideoFrame from './vendor/VideoFrame'; -import Gestures from '../lib/Gestures'; import typoConstants from '../lib/typoConstants'; import { clamp, e, setElementClass } from '../lib/util'; import ShakaFrontend from './frontend/ShakaFrontend'; @@ -91,7 +90,6 @@ export default class DlfMediaPlayer { onTrackChange: this.onTrackChange.bind(this), onTimeUpdate: this.onTimeUpdate.bind(this), onPlay: this.onPlay.bind(this), - onManualSeek: this.onManualSeek.bind(this), }; this.registerEventHandlers(); @@ -140,11 +138,11 @@ export default class DlfMediaPlayer { }, 'navigate.frame.prev': () => { this.fps?.vifa.seekBackward(1); - this.emitControlEvent('dlf-media-manual-seek', {}); + this.frontend.afterManualSeek(); }, 'navigate.frame.next': () => { this.fps?.vifa.seekForward(1); - this.emitControlEvent('dlf-media-manual-seek', {}); + this.frontend.afterManualSeek(); }, 'navigate.position.percental': ( /** @type {Keybinding} */ kb, @@ -187,8 +185,6 @@ export default class DlfMediaPlayer { this.player.addEventListener('adaptation', this.handlers.onTrackChange); this.player.addEventListener('variantchanged', this.handlers.onTrackChange); - this.controls.addEventListener('dlf-media-manual-seek', this.handlers.onManualSeek); - this.controls.addEventListener('timeandseekrangeupdated', this.handlers.onTimeUpdate); this.video.addEventListener('play', this.handlers.onPlay); @@ -200,10 +196,10 @@ export default class DlfMediaPlayer { * @private */ registerGestures() { - const g = new Gestures({ - allowGesture: this.allowGesture.bind(this), - }); - g.register(this.frontend.domElement); + const g = this.frontend.gestures; + if (g === null) { + return; + } g.on('gesture', (e) => { switch (e.type) { @@ -257,27 +253,6 @@ export default class DlfMediaPlayer { }); } - /** - * - * @param {PointerEvent} event - */ - allowGesture(event) { - // Don't allow gestures over Shaka bottom controls - const bounding = this.videoBox.getBoundingClientRect(); - const controlsHeight = this.shakaBottomControls?.getBoundingClientRect().height ?? 0; - const userAreaBottom = bounding.bottom - controlsHeight - 20; - if (event.clientY >= userAreaBottom) { - return false; - } - - // Check that the pointer interacts with the container, so isn't over the button - if (event.target !== this.videoBox.querySelector('.shaka-play-button-container')) { - return false; - } - - return true; - } - /** * @returns {dlf.media.PlayerFrontend} */ @@ -456,12 +431,6 @@ export default class DlfMediaPlayer { this.frontend.hidePoster(); } - onManualSeek() { - // Hide poster when seeking in pause mode before playback has started - // We don't want to hide the poster when initial timecode is used - this.frontend.hidePoster(); - } - /** * @private * @param {number} readyState @@ -478,18 +447,6 @@ export default class DlfMediaPlayer { } } - /** - * - * @returns {boolean} - */ - anySettingsMenusAreOpen() { - return this.controls.anySettingsMenusAreOpen(); - } - - hideSettingsMenus() { - this.controls.hideSettingsMenus(); - } - /** * * @param {string} locale @@ -682,7 +639,7 @@ export default class DlfMediaPlayer { this.video.currentTime = position.timecode; } - this.frontend.hidePoster(); + this.frontend.afterManualSeek(); } /** diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index c90e05c..314cb68 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -3,6 +3,7 @@ import shaka from 'shaka-player/dist/shaka-player.ui'; import 'shaka-player/ui/controls.less'; +import Gestures from '../../lib/Gestures'; import { e } from '../../lib/util'; import { FlatSeekBar, @@ -13,6 +14,7 @@ import { /** * Listens to the following custom events: * - {@link dlf.media.SeekBarEvent} + * - {@link dlf.media.ManualSeekEvent} * * @implements {dlf.media.PlayerFrontend} */ @@ -68,6 +70,16 @@ export default class ShakaFrontend { this.controls = /** @type {shaka.ui.Controls} */(this.ui.getControls()); + /** @private */ + this.gestures_ = new Gestures({ + allowGesture: this.allowGesture.bind(this), + }); + + /** @private */ + this.handlers = { + afterManualSeek: this.afterManualSeek.bind(this), + }; + this.registerEventHandlers(); } @@ -80,6 +92,9 @@ export default class ShakaFrontend { const detail = /** @type {dlf.media.SeekBarEvent} */(e).detail; this.seekBar_ = detail.seekBar; }); + this.controls.addEventListener('dlf-media-manual-seek', this.handlers.afterManualSeek); + + this.gestures_.register(this.$videoBox); } get domElement() { @@ -90,6 +105,31 @@ export default class ShakaFrontend { return this.seekBar_; } + get gestures() { + return this.gestures_; + } + + handleEscape() { + if (this.seekBar?.isThumbnailPreviewOpen()) { + this.seekBar?.endSeek(); + return true; + } + + if (this.controls.anySettingsMenusAreOpen()) { + this.controls.hideSettingsMenus(); + return true; + } + + return false; + } + + afterManualSeek() { + // Hide poster when seeking in pause mode before playback has started + // We don't want to hide the poster when initial timecode is used + // TODO: Move this back to DlfMediaPlayer? + this.hidePoster(); + } + /** * * @param {string[]} elementKey @@ -152,4 +192,25 @@ export default class ShakaFrontend { hidePoster() { this.$poster.classList.remove('dlf-visible'); } + + /** + * @private + * @param {PointerEvent} event + */ + allowGesture(event) { + // Don't allow gestures over Shaka bottom controls + const bounding = this.$videoBox.getBoundingClientRect(); + const controlsHeight = this.shakaBottomControls?.getBoundingClientRect().height ?? 0; + const userAreaBottom = bounding.bottom - controlsHeight - 20; + if (event.clientY >= userAreaBottom) { + return false; + } + + // Check that the pointer interacts with the container, so isn't over the button + if (event.target !== this.$videoBox.querySelector('.shaka-play-button-container')) { + return false; + } + + return true; + } } diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index f8e6656..a029ac3 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -62,6 +62,27 @@ namespace dlf { get seekBar(): | import("../VideoPlayer/controls/FlatSeekBar").default | null; + + /** + * ``Gestures`` object that is configured to only dispatch gestures that + * are admissible on the player. + */ + get gestures(): import("../lib/Gestures").default | null; + + /** + * Handle `Esc` key press, e.g., to close open tooltips or popups. + * + * @returns Whether or not the UI has changed. This can be used to execute + * only one `Esc` action at a time. + */ + handleEscape(): boolean; + + /** + * React to a manual seek by the user (e.g., by using a keybinding), as + * opposed to automatic seeks such as seeking to the initial timecode. + * This may be used, for example, to hide the poster after a user action. + */ + afterManualSeek(); } /** diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index d7c7ea9..869aea1 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -89,10 +89,8 @@ export default class SlubMediaPlayer { 'cancel': () => { if (this.modals?.hasOpen()) { this.modals.closeNext(); - } else if (this.dlfPlayer.ui.seekBar?.isThumbnailPreviewOpen() ?? false) { - this.dlfPlayer.ui.seekBar?.endSeek(); - } else if (this.dlfPlayer.anySettingsMenusAreOpen()) { - this.dlfPlayer.hideSettingsMenus(); + } else { + this.dlfPlayer.ui.handleEscape(); } }, 'modal.help.open': () => { From b9a2bca157c35462573114ccf6fe3e056bf1c95d Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Fri, 13 May 2022 11:40:44 +0200 Subject: [PATCH 15/25] Refactor: Extract `MediaProperties` - Move to `ShakaFrontend` - Merge fps/chapter/variantGroups events - Use for poster --- .../DlfMediaPlayer/DlfMediaPlayer.js | 45 ++---------- .../DlfMediaPlayer/ThumbnailPreview.js | 2 +- .../DlfMediaPlayer/controls/FlatSeekBar.js | 58 ++++++++------- .../controls/PresentationTimeTracker.js | 32 ++++----- .../controls/VideoTrackSelection.js | 71 ++++++++++++------- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 56 ++++++++++++++- .../JavaScript/DlfMediaPlayer/types.d.ts | 41 ++++------- .../SlubMediaPlayer/SlubMediaPlayer.js | 6 +- 8 files changed, 163 insertions(+), 148 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 90a34c0..35b7873 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -67,9 +67,6 @@ export default class DlfMediaPlayer { /** @private */ this.lastReadyState = 0; - /** @private @type {Event[]} */ - this.controlEventQueue = []; - /** @private @type {dlf.media.Fps | null} */ this.fps = null; @@ -81,7 +78,6 @@ export default class DlfMediaPlayer { /** @private */ this.frontend = new ShakaFrontend(this.env, this.player, this.video); - this.poster = this.frontend.$poster; this.videoBox = this.frontend.$videoBox; this.errorBox = this.frontend.$errorBox; @@ -299,14 +295,6 @@ export default class DlfMediaPlayer { this.constants = typoConstants(constants, this.constants); } - /** - * - * @param {string} posterUrl - */ - setPoster(posterUrl) { - this.poster.src = posterUrl; - } - /** * Configures the Shaka player UI and mounts it into {@link mount}. The mount * point is being replaced with the player until {@link unmount} is called. @@ -386,7 +374,7 @@ export default class DlfMediaPlayer { || this.variantGroups.selectGroupByRole("main") || this.variantGroups.selectGroupByIndex(0); - this.emitControlEvent('dlf-media-variant-groups', { + this.frontend.updateMediaProperties({ variantGroups: this.variantGroups, }); @@ -412,7 +400,9 @@ export default class DlfMediaPlayer { }; } - this.emitControlEvent('dlf-media-fps', { fps: this.fps }); + this.frontend.updateMediaProperties({ + fps: this.fps, + }); } onTimeUpdate() { @@ -461,7 +451,7 @@ export default class DlfMediaPlayer { */ setChapters(chapters) { this.chapters = chapters; - this.emitControlEvent('dlf-media-chapters', { chapters }); + this.frontend.updateMediaProperties({ chapters }); } /** @@ -700,31 +690,6 @@ export default class DlfMediaPlayer { } } - /** - * - * @private - * @template {keyof dlf.media.EventDetail} K - * @param {K} key - * @param {dlf.media.EventDetail[K]} detail - */ - emitControlEvent(key, detail) { - this.controlEventQueue.push(new CustomEvent(key, { detail })); - this.dispatchControlEvents(); - } - - /** - * @private - */ - dispatchControlEvents() { - if (this.isMounted) { - for (const event of this.controlEventQueue) { - this.controls.dispatchEvent(event); - } - - this.controlEventQueue = []; - } - } - /** * * @param {Event} event diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/ThumbnailPreview.js b/Resources/Private/JavaScript/DlfMediaPlayer/ThumbnailPreview.js index 58af285..1246c3b 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/ThumbnailPreview.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/ThumbnailPreview.js @@ -168,7 +168,7 @@ export default class ThumbnailPreview { } /** - * @param {Chapters} chapters + * @param {Chapters | null} chapters */ setChapters(chapters) { this.chapters = chapters; diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js index 6523705..566ceac 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js @@ -6,7 +6,6 @@ import { e } from '../../lib/util'; import Chapters from '../Chapters'; import ImageFetcher from '../ImageFetcher'; import ThumbnailPreview from '../ThumbnailPreview'; -import VariantGroups from '../VariantGroups'; /** * Seek bar that is not based on an input range element. This provides more @@ -17,9 +16,7 @@ import VariantGroups from '../VariantGroups'; * mostly taken from Shaka. * * Listens to the following custom events: - * - {@link dlf.media.VariantGroupsEvent} - * - {@link dlf.media.ChaptersEvent} - * - {@link dlf.media.FpsEvent} + * - {@link dlf.media.MediaPropertiesEvent} * * Emits the following custom events: * - {@link dlf.media.SeekBarEvent} @@ -53,12 +50,15 @@ export default class FlatSeekBar extends shaka.ui.Element { /** @private Avoid naming conflicts with parent class */ this.dlf = { - /** @type {Chapters | null} */ - chapters: null, + /** @type {dlf.media.MediaProperties} */ + mediaProperties: { + poster: null, + chapters: null, + fps: null, + variantGroups: null, + }, /** @type {boolean} */ hasRenderedChapters: false, - /** @type {VariantGroups | null} */ - variantGroups: null, /** @type {number} */ value: 0, /** @type {shaka.extern.UIConfiguration} */ @@ -122,24 +122,20 @@ export default class FlatSeekBar extends shaka.ui.Element { this.updatePreviewImageTracks(); }); - this.eventManager.listen(this.controls, 'dlf-media-variant-groups', (e) => { - const detail = /** @type {dlf.media.VariantGroupsEvent} */(e).detail; - this.dlf.variantGroups = detail.variantGroups; - this.updatePreviewImageTracks(); - }); - - this.eventManager.listen(this.controls, 'dlf-media-chapters', (e) => { - const detail = /** @type {dlf.media.ChaptersEvent} */(e).detail; - this.dlf.chapters = detail.chapters; - this.dlf.hasRenderedChapters = false; - this.dlf.thumbnailPreview?.setChapters(detail.chapters); - this.update(); - }); - - this.eventManager.listen(this.controls, 'dlf-media-fps', (e) => { - const detail = /** @type {dlf.media.FpsEvent} */(e).detail; - if (detail.fps) { - this.dlf.thumbnailPreview?.setFps(detail.fps?.rate); + this.eventManager.listen(this.controls, 'dlf-media-properties', (e) => { + const detail = /** @type {dlf.media.MediaPropertiesEvent} */(e).detail; + this.dlf.mediaProperties = detail.fullProps; + const { chapters, fps, variantGroups } = detail.updateProps; + if (chapters !== undefined) { + this.dlf.hasRenderedChapters = false; + this.dlf.thumbnailPreview?.setChapters(chapters); + this.update(); + } + if (fps !== undefined) { + this.dlf.thumbnailPreview?.setFps(fps?.rate ?? null); + } + if (variantGroups) { + this.updatePreviewImageTracks(); } }); @@ -238,11 +234,12 @@ export default class FlatSeekBar extends shaka.ui.Element { return; } - if (this.dlf.variantGroups === null) { + const { variantGroups } = this.dlf.mediaProperties; + if (variantGroups === null) { return; } - const thumbTracks = this.dlf.variantGroups.findThumbnailTracks(); + const thumbTracks = variantGroups.findThumbnailTracks(); this.dlf.thumbnailPreview.setThumbnailTracks(thumbTracks); } @@ -296,8 +293,9 @@ export default class FlatSeekBar extends shaka.ui.Element { return; } - if (this.dlf.chapters !== null && !this.dlf.hasRenderedChapters) { - this.renderChapterMarkers(this.dlf.chapters, duration); + const { chapters } = this.dlf.mediaProperties; + if (chapters != null && !this.dlf.hasRenderedChapters) { + this.renderChapterMarkers(chapters, duration); this.dlf.hasRenderedChapters = true; } diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/PresentationTimeTracker.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/PresentationTimeTracker.js index 38d0d68..5338fcf 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/PresentationTimeTracker.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/PresentationTimeTracker.js @@ -1,11 +1,9 @@ // @ts-check import shaka from 'shaka-player/dist/shaka-player.ui'; -import VideoFrame from '../vendor/VideoFrame'; import { clamp, e } from '../../lib/util'; import buildTimeString from '../lib/buildTimeString'; -import Chapters from '../Chapters'; /** * @typedef {'current-time' | 'remaining-time' | 'current-frame'} TimeModeKey @@ -28,8 +26,7 @@ const TimeMode = { * activeMode: number; * duration: number; * totalSeconds: number; - * fps: dlf.media.Fps | null; - * chapters: Chapters | null; + * mediaProperties: Pick; * }} State */ @@ -39,8 +36,7 @@ const TimeMode = { * Originally based upon Shaka's PresentationTimeTracker. * * Listens to the following custom events: - * - {@link dlf.media.ChaptersEvent} - * - {@link dlf.media.FpsEvent} + * - {@link dlf.media.MediaPropertiesEvent} */ export default class PresentationTimeTracker extends shaka.ui.Element { /** @@ -87,8 +83,10 @@ export default class PresentationTimeTracker extends shaka.ui.Element { activeMode: TimeMode.CurrentTime, totalSeconds: 0, duration: 0, - fps: null, - chapters: null, + mediaProperties: { + chapters: null, + fps: null, + }, }; if (this.eventManager) { @@ -101,17 +99,10 @@ export default class PresentationTimeTracker extends shaka.ui.Element { const updateTime = this.updateTime.bind(this); this.eventManager.listen(this.controls, 'timeandseekrangeupdated', updateTime); - this.eventManager.listen(this.controls, 'dlf-media-chapters', (e) => { - const detail = /** @type {dlf.media.ChaptersEvent} */(e).detail; - this.render({ - chapters: detail.chapters, - }); - }); - - this.eventManager.listen(this.controls, 'dlf-media-fps', (e) => { - const detail = /** @type {dlf.media.FpsEvent} */(e).detail; + this.eventManager.listen(this.controls, 'dlf-media-properties', (e) => { + const detail = /** @type {dlf.media.MediaPropertiesEvent} */(e).detail; this.render({ - fps: detail.fps, + mediaProperties: detail.fullProps, }); }); } @@ -162,15 +153,16 @@ export default class PresentationTimeTracker extends shaka.ui.Element { /** * * @param {TimeModeKey} tKey - * @param {Pick} state + * @param {Pick} state * @returns {string} */ - getTimecodeText(tKey, { isReady, totalSeconds, duration, fps, chapters }) { + getTimecodeText(tKey, { isReady, totalSeconds, duration, mediaProperties }) { // Don't show incomplete info when duration is not yet available if (!isReady || duration === 0) { return this.dlf.env.t('player.loading'); } else { const showHour = duration >= 3600; + const { chapters, fps } = mediaProperties; const fpsRate = fps?.rate ?? null; const textValues = { diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/VideoTrackSelection.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/VideoTrackSelection.js index 22c3939..68db168 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/VideoTrackSelection.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/VideoTrackSelection.js @@ -9,7 +9,7 @@ import VariantGroups from '../VariantGroups'; * Control panel element to show current playback time. * * Listens to the following custom events: - * - {@link dlf.media.VariantGroupsEvent} + * - {@link dlf.media.MediaPropertiesEvent} */ export default class VideoTrackSelection extends shaka.ui.SettingsMenu { /** @@ -53,33 +53,16 @@ export default class VideoTrackSelection extends shaka.ui.SettingsMenu { this.menuButtons = {}; if (this.eventManager) { - this.eventManager.listen(this.controls, 'dlf-media-variant-groups', (ev) => { - const detail = /** @type {dlf.media.VariantGroupsEvent} */(ev).detail; - const variantGroups = - this.dlf.variantGroups = detail.variantGroups; - - this.clearMenu(); - this.updateVisibility(); - - try { - for (const group of variantGroups) { - const button = e("button", { - $click: () => { - this.dlf.variantGroups?.selectGroupByKey(group.key); - }, - }, [ - e("span", {}, [group.key]), - ]); - - this.menu.appendChild(button); - - this.menuButtons[group.key] = button; + this.eventManager.listen(this.controls, 'dlf-media-properties', (ev) => { + const detail = /** @type {dlf.media.MediaPropertiesEvent} */(ev).detail; + const { variantGroups } = detail.updateProps; + if (variantGroups !== undefined) { + try { + this.setVariantGroups(variantGroups); + } catch (err) { + // TODO: Shaka seems to handle exceptions occurring in listeners + console.error(err); } - - this.markActiveGroup(); - } catch (err) { - // TODO: Shaka seems to handle exceptions occurring in listeners - console.error(err); } }); @@ -89,6 +72,38 @@ export default class VideoTrackSelection extends shaka.ui.SettingsMenu { } } + /** + * @private + * + * @param {VariantGroups | null} variantGroups + */ + setVariantGroups(variantGroups) { + this.dlf.variantGroups = variantGroups; + + this.clearMenu(); + this.updateVisibility(); + + if (variantGroups === null) { + return; + } + + for (const group of variantGroups) { + const button = e("button", { + $click: () => { + this.dlf.variantGroups?.selectGroupByKey(group.key); + }, + }, [ + e("span", {}, [group.key]), + ]); + + this.menu.appendChild(button); + + this.menuButtons[group.key] = button; + } + + this.markActiveGroup(); + } + /** * @private */ @@ -101,6 +116,8 @@ export default class VideoTrackSelection extends shaka.ui.SettingsMenu { /** * Updates UI to show which group is active + * + * @private */ markActiveGroup() { const activeGroup = this.dlf.variantGroups?.findActiveGroup(); diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index 314cb68..12d2007 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -4,7 +4,7 @@ import shaka from 'shaka-player/dist/shaka-player.ui'; import 'shaka-player/ui/controls.less'; import Gestures from '../../lib/Gestures'; -import { e } from '../../lib/util'; +import { e, setElementClass } from '../../lib/util'; import { FlatSeekBar, PresentationTimeTracker, @@ -16,6 +16,9 @@ import { * - {@link dlf.media.SeekBarEvent} * - {@link dlf.media.ManualSeekEvent} * + * Emits the following custom events: + * - {@link dlf.media.MediaPropertiesEvent} + * * @implements {dlf.media.PlayerFrontend} */ export default class ShakaFrontend { @@ -35,6 +38,14 @@ export default class ShakaFrontend { /** @private */ this.media = media; + /** @private @type {dlf.media.MediaProperties} */ + this.mediaProperties = { + poster: null, + chapters: null, + fps: null, + variantGroups: null, + }; + /** @private @type {string[]} */ this.controlPanelButtons = []; @@ -109,6 +120,38 @@ export default class ShakaFrontend { return this.gestures_; } + /** + * + * @param {Partial} props + */ + updateMediaProperties(props) { + Object.assign(this.mediaProperties, props); + this.notifyMediaProperties(/* full= */this.mediaProperties, props); + } + + /** + * @private + * @param {dlf.media.MediaProperties} fullProps + * @param {Partial} updateProps + */ + notifyMediaProperties( + fullProps = this.mediaProperties, + updateProps = fullProps + ) { + if (updateProps.poster !== undefined) { + this.renderPoster(); + } + + /** @type {dlf.media.MediaPropertiesEvent} */ + const event = new CustomEvent('dlf-media-properties', { + detail: { + updateProps, + fullProps, + }, + }); + this.controls.dispatchEvent(event); + } + handleEscape() { if (this.seekBar?.isThumbnailPreviewOpen()) { this.seekBar?.endSeek(); @@ -187,12 +230,23 @@ export default class ShakaFrontend { // DOM is (re-)created in `ui.configure()`, so query container afterwards this.shakaBottomControls = this.$videoBox.querySelector('.shaka-bottom-controls'); + + this.notifyMediaProperties(); } hidePoster() { this.$poster.classList.remove('dlf-visible'); } + /** + * @private + */ + renderPoster() { + if (this.mediaProperties.poster !== null) { + this.$poster.src = this.mediaProperties.poster; + } + } + /** * @private * @param {PointerEvent} event diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index a029ac3..c5531a4 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -53,6 +53,13 @@ namespace dlf { vifa: import("./vendor/VideoFrame").default; }; + type MediaProperties = { + poster: string | null; + variantGroups: import("./VariantGroups").default | null; + chapters: import("./Chapters").default | null; + fps: Fps | null; + }; + interface PlayerFrontend { /** * Main DOM element / container of the frontend. @@ -69,6 +76,8 @@ namespace dlf { */ get gestures(): import("../lib/Gestures").default | null; + updateMediaProperties(props: Partial); + /** * Handle `Esc` key press, e.g., to close open tooltips or popups. * @@ -86,19 +95,12 @@ namespace dlf { } /** - * Signals chapters available in current video. - * - * Should be dispatched on a Shaka control ({@link shaka.ui.Controls}). - */ - interface ChaptersEvent - extends CustomEvent {} - - /** - * Signals information about FPS of current video. + * Signals that {@link MediaProperties} have changed or become available. * * Should be dispatched on a Shaka control ({@link shaka.ui.Controls}). */ - interface FpsEvent extends CustomEvent {} + interface MediaPropertiesEvent + extends CustomEvent {} /** * Registers seekbar to parent DlfMediaPlayer. @@ -116,28 +118,15 @@ namespace dlf { interface ManualSeekEvent extends CustomEvent {} - /** - * Signals variant groups of current video. - * - * Should be dispatched on a Shaka control ({@link shaka.ui.Controls}). - */ - interface VariantGroupsEvent - extends CustomEvent {} - type EventDetail = { - "dlf-media-chapters": { - chapters: import("./Chapters").default; - }; - "dlf-media-fps": { - fps: Fps | null; + "dlf-media-properties": { + updateProps: Partial; + fullProps: MediaProperties; }; "dlf-media-seek-bar": { seekBar: import("./controls/FlatSeekBar").default; }; "dlf-media-manual-seek": {}; - "dlf-media-variant-groups": { - variantGroups: import("./VariantGroups").default; - }; }; /** diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index 869aea1..07b6323 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -253,9 +253,9 @@ export default class SlubMediaPlayer { this.dlfPlayer.ui.configure(); } this.dlfPlayer.setLocale(this.config.lang.twoLetterIsoCode); - if (this.videoInfo.url.poster !== undefined) { - this.dlfPlayer.setPoster(this.videoInfo.url.poster); - } + this.dlfPlayer.ui.updateMediaProperties({ + poster: this.videoInfo.url.poster, + }); this.dlfPlayer.setChapters(chapters); this.dlfPlayer.setStartTime(startTime ?? null); this.dlfPlayer.setSources(this.videoInfo.sources); From 0c65b3e83e442fc8daf3c229f1196b234b1fbc22 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Sat, 14 May 2022 13:04:15 +0200 Subject: [PATCH 16/25] Extract `PlayerProperties` in PlayerFrontend --- .../DlfMediaPlayer/DlfMediaPlayer.js | 30 +++--------- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 49 ++++++++++++++++++- .../JavaScript/DlfMediaPlayer/types.d.ts | 11 ++++- .../SlubMediaPlayer/SlubMediaPlayer.js | 4 +- 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 35b7873..b3258c8 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -5,7 +5,7 @@ import shaka from 'shaka-player/dist/shaka-player.ui'; import VideoFrame from './vendor/VideoFrame'; import typoConstants from '../lib/typoConstants'; -import { clamp, e, setElementClass } from '../lib/util'; +import { clamp, e } from '../lib/util'; import ShakaFrontend from './frontend/ShakaFrontend'; import Chapters from './Chapters'; import VariantGroups from './VariantGroups'; @@ -328,21 +328,11 @@ export default class DlfMediaPlayer { } } - /** - * - * @param {string | null} langKey - */ - showError(langKey) { - if (langKey !== null) { - this.errorBox.innerText = this.env.t(langKey); - } - - setElementClass(this.errorBox, 'dlf-visible', langKey !== null); - } - async load() { if (this.sources_.length === 0) { - this.showError('error.playback-not-supported'); + this.frontend.updatePlayerProperties({ + error: 'error.playback-not-supported', + }); return false; } @@ -356,7 +346,9 @@ export default class DlfMediaPlayer { } } - this.showError('error.load-failed'); + this.frontend.updatePlayerProperties({ + error: 'error.load-failed', + }); return false; } @@ -437,14 +429,6 @@ export default class DlfMediaPlayer { } } - /** - * - * @param {string} locale - */ - setLocale(locale) { - this.controls.getLocalization()?.changeLocale([locale]); - } - /** * * @param {Chapters} chapters diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index 12d2007..f56e46c 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -46,6 +46,13 @@ export default class ShakaFrontend { variantGroups: null, }; + /** @private @type {dlf.media.PlayerProperties} */ + this.playerProperties = { + locale: '', + state: 'poster', + error: null, + }; + /** @private @type {string[]} */ this.controlPanelButtons = []; @@ -152,6 +159,26 @@ export default class ShakaFrontend { this.controls.dispatchEvent(event); } + /** + * + * @param {Partial} props + */ + updatePlayerProperties(props) { + Object.assign(this.playerProperties, props); + + if (props.locale !== undefined) { + this.controls.getLocalization()?.changeLocale([props.locale]); + } + + if (props.state !== undefined) { + this.renderPoster(); + } + + if (props.error !== undefined) { + this.renderError(); + } + } + handleEscape() { if (this.seekBar?.isThumbnailPreviewOpen()) { this.seekBar?.endSeek(); @@ -242,9 +269,29 @@ export default class ShakaFrontend { * @private */ renderPoster() { - if (this.mediaProperties.poster !== null) { + const showPoster = ( + this.mediaProperties.poster !== null + && this.playerProperties.state === 'poster' + ); + + if (showPoster) { + // @ts-expect-error this.$poster.src = this.mediaProperties.poster; } + + setElementClass(this.$poster, 'dlf-visible', showPoster); + } + + /** + * @private + */ + renderError() { + if (this.playerProperties.error === null) { + setElementClass(this.$errorBox, 'dlf-visible', false); + } else { + setElementClass(this.$errorBox, 'dlf-visible', true); + this.$errorBox.textContent = this.env.t(this.playerProperties.error); + } } /** diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index c5531a4..b2f723f 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -60,7 +60,15 @@ namespace dlf { fps: Fps | null; }; - interface PlayerFrontend { + type PlayerProperties = { + locale: string; + state: "poster" | "media"; + error: string | null; + }; + + interface PlayerFrontend< + PlayerT extends PlayerProperties = PlayerProperties + > { /** * Main DOM element / container of the frontend. */ @@ -77,6 +85,7 @@ namespace dlf { get gestures(): import("../lib/Gestures").default | null; updateMediaProperties(props: Partial); + updatePlayerProperties(props: Partial); /** * Handle `Esc` key press, e.g., to close open tooltips or popups. diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index 07b6323..5031521 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -252,7 +252,9 @@ export default class SlubMediaPlayer { this.dlfPlayer.ui.configure(); } - this.dlfPlayer.setLocale(this.config.lang.twoLetterIsoCode); + this.dlfPlayer.ui.updatePlayerProperties({ + locale: this.config.lang.twoLetterIsoCode, + }); this.dlfPlayer.ui.updateMediaProperties({ poster: this.videoInfo.url.poster, }); From 36b2d8c6ba4fd516a536b9647ea2c2d5649b6f3a Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Sat, 14 May 2022 21:48:35 +0200 Subject: [PATCH 17/25] Fix: Don't let Shaka handle `Esc` key --- .../Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index 5031521..5bb3ce5 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -274,7 +274,7 @@ export default class SlubMediaPlayer { } registerEventHandlers() { - document.addEventListener('keydown', this.handlers.onKeyDown); + document.addEventListener('keydown', this.handlers.onKeyDown, { capture: true }); document.addEventListener('keyup', this.handlers.onKeyUp, { capture: true }); } @@ -302,6 +302,12 @@ export default class SlubMediaPlayer { * @param {KeyboardEvent} e */ onKeyDown(e) { + // Hack against Shaka reacting to Escape key to close overflow menu; + // we do this ourselves. (TODO: Find a better solution) + if (e.key === 'Escape') { + e.stopImmediatePropagation(); + } + this.handleKey(e, 'down'); } From 155ebeaf70e3078145ee4a0b1f50a05cd8a83152 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Fri, 8 Apr 2022 13:02:20 +0200 Subject: [PATCH 18/25] Finish extracting `ShakaFrontend` --- .../DlfMediaPlayer/DlfMediaPlayer.js | 62 +++------------- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 71 ++++++++++++++++++- .../JavaScript/DlfMediaPlayer/types.d.ts | 2 - 3 files changed, 78 insertions(+), 57 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index b3258c8..4026e02 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -33,7 +33,6 @@ export default class DlfMediaPlayer { volumeStep: 0.05, seekStep: 5, trickPlayFactor: 4, - minBottomControlsReadyState: 2, // Enough data for current position }; /** @private @type {HTMLElement | null} */ @@ -64,9 +63,6 @@ export default class DlfMediaPlayer { /** @private @type {shaka.Player} */ this.player = new shaka.Player(this.video); - /** @private */ - this.lastReadyState = 0; - /** @private @type {dlf.media.Fps | null} */ this.fps = null; @@ -76,15 +72,13 @@ export default class DlfMediaPlayer { /** @private @type {Chapters} */ this.chapters = new Chapters([]); - /** @private */ + /** @private @type {dlf.media.PlayerFrontend} */ this.frontend = new ShakaFrontend(this.env, this.player, this.video); - this.videoBox = this.frontend.$videoBox; - this.errorBox = this.frontend.$errorBox; + /** @private */ this.handlers = { - onErrorEvent: this.onErrorEvent.bind(this), + onPlayerErrorEvent: this.onPlayerErrorEvent.bind(this), onTrackChange: this.onTrackChange.bind(this), - onTimeUpdate: this.onTimeUpdate.bind(this), onPlay: this.onPlay.bind(this), }; @@ -163,26 +157,14 @@ export default class DlfMediaPlayer { } } - get controls() { - return this.frontend.controls; - } - - get shakaBottomControls() { - return this.frontend.shakaBottomControls; - } - /** * @private */ registerEventHandlers() { - this.player.addEventListener('error', this.handlers.onErrorEvent); - this.controls.addEventListener('error', this.handlers.onErrorEvent); - + this.player.addEventListener('error', this.handlers.onPlayerErrorEvent); this.player.addEventListener('adaptation', this.handlers.onTrackChange); this.player.addEventListener('variantchanged', this.handlers.onTrackChange); - this.controls.addEventListener('timeandseekrangeupdated', this.handlers.onTimeUpdate); - this.video.addEventListener('play', this.handlers.onPlay); this.registerGestures(); @@ -397,36 +379,8 @@ export default class DlfMediaPlayer { }); } - onTimeUpdate() { - const readyState = this.video.readyState; - - if (readyState !== this.lastReadyState) { - this.updateBottomControlsVisibility(readyState); - } - } - onPlay() { this.videoPausedOn = null; - - // Hide poster once playback has started the first time - // This is necessary because "onTimeUpdate" may be fired with a delay - this.frontend.hidePoster(); - } - - /** - * @private - * @param {number} readyState - */ - updateBottomControlsVisibility(readyState) { - // When readyState is strictly between 0 and minBottomControlsReadyState, - // don't change whether controls are shown. Thus, on first load the controls - // may remain hidden, and on seeking the controls remain visible. - - if (readyState === 0) { - this.shakaBottomControls?.classList.remove('dlf-visible'); - } else if (readyState >= this.constants.minBottomControlsReadyState) { - this.shakaBottomControls?.classList.add('dlf-visible'); - } } /** @@ -527,7 +481,8 @@ export default class DlfMediaPlayer { * @type {number} */ get displayTime() { - return this.controls.getDisplayTime(); + // Adopted from "getDisplayTime" in "shaka.ui.Controls" + return this.frontend.seekBar?.getValue() ?? this.video.currentTime; } /** @@ -557,7 +512,6 @@ export default class DlfMediaPlayer { */ play() { this.video.play(); - this.videoPausedOn = null; } /** @@ -678,11 +632,11 @@ export default class DlfMediaPlayer { * * @param {Event} event */ - onErrorEvent(event) { + onPlayerErrorEvent(event) { if (event instanceof CustomEvent) { // TODO: Propagate to user const error = event.detail; - console.error('Error code', error.code, 'object', error); + console.error('Error from Shaka player', error.code, error); } } } diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index f56e46c..c0d3d05 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -29,6 +29,11 @@ export default class ShakaFrontend { * @param {HTMLMediaElement} media */ constructor(env, player, media) { + /** @private */ + this.constants = { + minBottomControlsReadyState: 2, // Enough data for current position + }; + /** @private */ this.env = env; @@ -46,6 +51,9 @@ export default class ShakaFrontend { variantGroups: null, }; + /** @private */ + this.lastReadyState = 0; + /** @private @type {dlf.media.PlayerProperties} */ this.playerProperties = { locale: '', @@ -59,7 +67,7 @@ export default class ShakaFrontend { /** @private @type {string[]} */ this.overflowMenuButtons = []; - /** @type {HTMLElement | null} */ + /** @private @type {HTMLElement | null} */ this.shakaBottomControls = null; /** @private @type {FlatSeekBar | null} */ @@ -86,6 +94,7 @@ export default class ShakaFrontend { /** @private */ this.ui = new shaka.ui.Overlay(this.player, this.$videoBox, this.media); + /** @private */ this.controls = /** @type {shaka.ui.Controls} */(this.ui.getControls()); /** @private */ @@ -95,6 +104,9 @@ export default class ShakaFrontend { /** @private */ this.handlers = { + onControlsErrorEvent: this.onControlsErrorEvent.bind(this), + onPlay: this.onPlay.bind(this), + onTimeUpdate: this.onTimeUpdate.bind(this), afterManualSeek: this.afterManualSeek.bind(this), }; @@ -105,13 +117,17 @@ export default class ShakaFrontend { * @private */ registerEventHandlers() { + this.controls.addEventListener('error', this.handlers.onControlsErrorEvent); // TODO: Figure out a good flow of events this.controls.addEventListener('dlf-media-seek-bar', (e) => { const detail = /** @type {dlf.media.SeekBarEvent} */(e).detail; this.seekBar_ = detail.seekBar; }); + this.controls.addEventListener('timeandseekrangeupdated', this.handlers.onTimeUpdate); this.controls.addEventListener('dlf-media-manual-seek', this.handlers.afterManualSeek); + this.media.addEventListener('play', this.handlers.onPlay); + this.gestures_.register(this.$videoBox); } @@ -261,6 +277,9 @@ export default class ShakaFrontend { this.notifyMediaProperties(); } + /** + * @private + */ hidePoster() { this.$poster.classList.remove('dlf-visible'); } @@ -314,4 +333,54 @@ export default class ShakaFrontend { return true; } + + /** + * @param {Event} event + */ + onControlsErrorEvent(event) { + if (event instanceof CustomEvent) { + // TODO: Propagate to user + const error = event.detail; + console.error('Error from Shaka controls', error.code, error); + } + } + + /** + * @private + */ + onPlay() { + // Hide poster once playback has started the first time. + // Reasons for doing this here instead of in `onTimeUpdate`: + // - Keep poster when using startTime in player + // - `onTimeUpdate` may be called with a delay + // - No need to call it on every time update anyways + this.hidePoster(); + } + + /** + * @private + */ + onTimeUpdate() { + const readyState = this.media.readyState; + + if (readyState !== this.lastReadyState) { + this.updateBottomControlsVisibility(readyState); + } + } + + /** + * @private + * @param {number} readyState + */ + updateBottomControlsVisibility(readyState) { + // When readyState is strictly between 0 and minBottomControlsReadyState, + // don't change whether controls are shown. Thus, on first load the controls + // may remain hidden, and on seeking the controls remain visible. + + if (readyState === 0) { + this.shakaBottomControls?.classList.remove('dlf-visible'); + } else if (readyState >= this.constants.minBottomControlsReadyState) { + this.shakaBottomControls?.classList.add('dlf-visible'); + } + } } diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index b2f723f..3730d95 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -44,8 +44,6 @@ namespace dlf { * TODO: Check if this should be input as setting or retrieved from current manifest */ trickPlayFactor: number; - - minBottomControlsReadyState: number; }; type Fps = { From a7bee04f338c021251af77a5fc7e83e0947c2c72 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Mon, 16 May 2022 12:54:47 +0200 Subject: [PATCH 19/25] Refactor: Auto-configure when adding controls --- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 25 +++++++++++++++++++ .../SlubMediaPlayer/SlubMediaPlayer.js | 2 -- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index c0d3d05..487484b 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -97,6 +97,9 @@ export default class ShakaFrontend { /** @private */ this.controls = /** @type {shaka.ui.Controls} */(this.ui.getControls()); + /** @private @type {ReturnType | null} */ + this.configureTimeout = null; + /** @private */ this.gestures_ = new Gestures({ allowGesture: this.allowGesture.bind(this), @@ -111,6 +114,7 @@ export default class ShakaFrontend { }; this.registerEventHandlers(); + this.scheduleConfigure(); } /** @@ -222,6 +226,7 @@ export default class ShakaFrontend { */ addControlElement(...elementKey) { this.controlPanelButtons.push(...elementKey); + this.scheduleConfigure(); } /** @@ -230,8 +235,28 @@ export default class ShakaFrontend { */ addOverflowButton(...elementKey) { this.overflowMenuButtons.push(...elementKey); + this.scheduleConfigure(); + } + + /** + * Set timeout to (re-)configure the UI. + * + * This is used to reconfigure only once for multiple successive changes. + * + * @private + */ + scheduleConfigure() { + if (this.configureTimeout === null) { + this.configureTimeout = setTimeout(() => { + this.configureTimeout = null; + this.configure(); + }); + } } + /** + * @private + */ configure() { // TODO: Somehow avoid overriding the SeekBar globally? FlatSeekBar.register(); diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index 5bb3ce5..514f007 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -249,8 +249,6 @@ export default class SlubMediaPlayer { onClick: this.actions['modal.help.open'], }) ); - - this.dlfPlayer.ui.configure(); } this.dlfPlayer.ui.updatePlayerProperties({ locale: this.config.lang.twoLetterIsoCode, From 400e287002dcf6fb4c32b05157c8cfc6f7263342 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Tue, 12 Apr 2022 17:48:20 +0200 Subject: [PATCH 20/25] Add Shaka-based audio-only mode --- .../DlfMediaPlayer/DlfMediaPlayer.js | 3 + .../DlfMediaPlayer/frontend/ShakaFrontend.js | 78 +++++++++++++++---- .../JavaScript/DlfMediaPlayer/types.d.ts | 3 + .../SlubMediaPlayer/SlubMediaPlayer.js | 11 +++ .../Private/Language/de.locallang_video.xlf | 8 +- .../Private/Language/locallang_video.xlf | 4 +- .../Less/DlfMediaPlayer/ShakaFrontend.less | 62 ++++++++++++++- .../Less/SlubMediaPlayer/SlubMediaPlayer.less | 2 - Resources/Private/Less/SxndMediaPlayer.less | 5 ++ 9 files changed, 152 insertions(+), 24 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index 4026e02..c36764b 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -322,6 +322,9 @@ export default class DlfMediaPlayer { for (const source of this.sources_) { try { await this.loadManifest(source); + this.ui.updatePlayerProperties({ + mode: this.player.isAudioOnly() ? 'audio' : 'video', + }); return true; } catch (e) { console.error(e); diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index 487484b..469c786 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -56,6 +56,7 @@ export default class ShakaFrontend { /** @private @type {dlf.media.PlayerProperties} */ this.playerProperties = { + mode: 'audio', locale: '', state: 'poster', error: null, @@ -75,7 +76,7 @@ export default class ShakaFrontend { /** @private */ this.$container = e('div', { - className: "dlf-media-player dlf-media-frontend-shaka" + className: "dlf-media-player dlf-shaka" }, [ this.$videoBox = e('div', { className: "dlf-media-shaka-box" }, [ this.$video = media, @@ -100,6 +101,9 @@ export default class ShakaFrontend { /** @private @type {ReturnType | null} */ this.configureTimeout = null; + /** @private */ + this.isConfigured = false; + /** @private */ this.gestures_ = new Gestures({ allowGesture: this.allowGesture.bind(this), @@ -184,6 +188,11 @@ export default class ShakaFrontend { * @param {Partial} props */ updatePlayerProperties(props) { + const shouldReconfigure = ( + props.mode !== undefined + && (!this.isConfigured || props.mode !== this.playerProperties.mode) + ); + Object.assign(this.playerProperties, props); if (props.locale !== undefined) { @@ -197,6 +206,10 @@ export default class ShakaFrontend { if (props.error !== undefined) { this.renderError(); } + + if (shouldReconfigure) { + this.scheduleConfigure(); + } } handleEscape() { @@ -261,7 +274,30 @@ export default class ShakaFrontend { // TODO: Somehow avoid overriding the SeekBar globally? FlatSeekBar.register(); - this.ui.configure({ + this.$container.setAttribute("data-mode", this.playerProperties.mode); + + // TODO: Refactor insertion at custom position (left or right of fullscreen) + this.ui.configure(this.getShakaConfiguration()); + this.isConfigured = true; + + // Fade in controls, especially when switching from video to audio (TODO: Refactor) + this.$videoBox.dispatchEvent(new MouseEvent('mousemove')); + + // DOM is (re-)created in `ui.configure()`, so query container afterwards + this.shakaBottomControls = + this.$videoBox.querySelector('.shaka-bottom-controls'); + + this.notifyMediaProperties(); + } + + /** + * @private + */ + getShakaConfiguration() { + const playerMode = this.playerProperties.mode; + + /** @type {any} */ + const result = { addSeekBar: true, enableTooltips: true, controlPanelElements: [ @@ -283,23 +319,35 @@ export default class ShakaFrontend { 'captions', ...this.overflowMenuButtons, ], - addBigPlayButton: true, - seekBarColors: { - base: 'rgba(255, 255, 255, 0.3)', - buffered: 'rgba(255, 255, 255, 0.54)', - played: 'rgb(255, 255, 255)', - adBreaks: 'rgb(255, 204, 0)', - }, + addBigPlayButton: playerMode === 'video', + fadeDelay: playerMode === 'audio' + ? 100_000_000 // Just some large value + : undefined, // Use default + seekBarColors: playerMode === 'video' + ? { + base: 'rgba(255, 255, 255, 0.3)', + buffered: 'rgba(255, 255, 255, 0.54)', + played: 'rgb(255, 255, 255)', + adBreaks: 'rgb(255, 204, 0)', + } + : { + base: 'rgba(0, 0, 0, 0.3)', + buffered: 'rgba(0, 0, 0, 0.54)', + played: '#2a2b2c', + adBreaks: 'rgb(255, 204, 0)', + }, + volumeBarColors: playerMode === 'audio' + ? { + base: 'rgba(0, 0, 0, 40%)', + level: 'rgb(0, 0, 0, 80%)', + } + : undefined, // Use default enableKeyboardPlaybackControls: false, doubleClickForFullscreen: false, singleClickForPlayAndPause: false, - }); - - // DOM is (re-)created in `ui.configure()`, so query container afterwards - this.shakaBottomControls = - this.$videoBox.querySelector('.shaka-bottom-controls'); + }; - this.notifyMediaProperties(); + return result; } /** diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index 3730d95..58d13ac 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -23,6 +23,8 @@ namespace dlf { url: string; }; + type PlayerMode = "audio" | "video"; + type PlayerConstants = { /** * Number of seconds in which to still rewind to previous chapter. @@ -59,6 +61,7 @@ namespace dlf { }; type PlayerProperties = { + mode: PlayerMode; locale: string; state: "poster" | "media"; error: string | null; diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index 514f007..f2f69d8 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -306,6 +306,17 @@ export default class SlubMediaPlayer { e.stopImmediatePropagation(); } + // TODO: Remove + if (this.dlfPlayer.ui instanceof ShakaFrontend) { + if (e.key === 'F2') { + this.dlfPlayer.ui.updatePlayerProperties({ mode: 'audio' }); + this.modals?.resize(); + } else if (e.key === 'F4') { + this.dlfPlayer.ui.updatePlayerProperties({ mode: 'video' }); + this.modals?.resize(); + } + } + this.handleKey(e, 'down'); } diff --git a/Resources/Private/Language/de.locallang_video.xlf b/Resources/Private/Language/de.locallang_video.xlf index 6fa939e..41285ab 100644 --- a/Resources/Private/Language/de.locallang_video.xlf +++ b/Resources/Private/Language/de.locallang_video.xlf @@ -150,12 +150,12 @@ - - + + - - + + diff --git a/Resources/Private/Language/locallang_video.xlf b/Resources/Private/Language/locallang_video.xlf index 1c841b6..b9dffc4 100644 --- a/Resources/Private/Language/locallang_video.xlf +++ b/Resources/Private/Language/locallang_video.xlf @@ -114,10 +114,10 @@ - + - + diff --git a/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less b/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less index c54e8a1..4817626 100644 --- a/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less +++ b/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less @@ -1,4 +1,6 @@ -.dlf-media-frontend-shaka { +@audio-controls-color: #2a2b2c; + +.dlf-shaka { .dlf-media-shaka-box { background-color: black; position: absolute; @@ -19,4 +21,62 @@ align-items: center; } } + + .shaka-scrim-container { + display: none; + } + + &[data-mode=""] { + display: none; + } + + &[data-mode="audio"] { + height: 3.5em; + position: absolute; + bottom: 0; + width: 100%; + + background-color: rgba(79, 179, 199, 0.6); + + .dlf-media-error { + color: @audio-controls-color; + } + + video { + display: none; + } + + .dlf-media-poster { + display: none !important; + } + + // Use background color from parent + .dlf-media-shaka-box { + background-color: transparent; + } + + .shaka-spinner-container { + display: none; + } + + .shaka-controls-button-panel > * { + color: @audio-controls-color; + } + + .shaka-bottom-controls { + width: 100%; + padding-bottom: 0.4em; + } + + .shaka-volume-bar { + &::-webkit-slider-thumb, + &::-moz-range-thumb { + background: @audio-controls-color; + } + } + + .dlf-media-chapter-marker { + background-color: lighten(rgb(79, 179, 199), 30%); + } + } } diff --git a/Resources/Private/Less/SlubMediaPlayer/SlubMediaPlayer.less b/Resources/Private/Less/SlubMediaPlayer/SlubMediaPlayer.less index 9021ee4..a0b1a29 100644 --- a/Resources/Private/Less/SlubMediaPlayer/SlubMediaPlayer.less +++ b/Resources/Private/Less/SlubMediaPlayer/SlubMediaPlayer.less @@ -19,8 +19,6 @@ } .document-view { - background-color: black; - @media screen and (max-width: (@tabletLandscapeViewportWidth - 1px)) { top: 50px; } diff --git a/Resources/Private/Less/SxndMediaPlayer.less b/Resources/Private/Less/SxndMediaPlayer.less index e5d9cb4..767cd0b 100644 --- a/Resources/Private/Less/SxndMediaPlayer.less +++ b/Resources/Private/Less/SxndMediaPlayer.less @@ -13,3 +13,8 @@ .chapter { cursor: pointer; } + +// Avoid white flash at page load +.document-view { + background-color: black; +} From 5cdbbace61ba3f73af3a8be6baf88cbf98d95632 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Mon, 9 May 2022 17:45:33 +0200 Subject: [PATCH 21/25] Refactor: Expose and use `media` property in player --- .../DlfMediaPlayer/DlfMediaPlayer.js | 65 ++++--------------- .../SlubMediaPlayer/SlubMediaPlayer.js | 4 +- 2 files changed, 13 insertions(+), 56 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index c36764b..a6c4711 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -90,14 +90,14 @@ export default class DlfMediaPlayer { // Override in application }, 'playback.toggle': () => { - if (this.paused) { - this.play(); + if (this.video.paused) { + this.video.play(); } else { - this.pause(); + this.video.pause(); } }, 'playback.volume.mute.toggle': () => { - this.muted = !this.muted; + this.video.muted = !this.video.muted; }, 'playback.volume.inc': () => { this.volume = this.volume + this.constants.volumeStep; @@ -422,7 +422,7 @@ export default class DlfMediaPlayer { * @returns {dlf.media.Chapter | undefined} */ getCurrentChapter() { - return this.timeToChapter(this.currentTime); + return this.timeToChapter(this.video.currentTime); } /** @@ -435,10 +435,9 @@ export default class DlfMediaPlayer { } /** - * * @returns {HTMLVideoElement} */ - getVideo() { + get media() { return this.video; } @@ -473,13 +472,6 @@ export default class DlfMediaPlayer { this.video.volume = clamp(value, [0, 1]); } - /** - * @type {number} - */ - get currentTime() { - return this.video.currentTime; - } - /** * @type {number} */ @@ -488,42 +480,6 @@ export default class DlfMediaPlayer { return this.frontend.seekBar?.getValue() ?? this.video.currentTime; } - /** - * Whether or not the video is muted. - * - * @type {boolean} - */ - get muted() { - return this.video.muted; - } - - set muted(value) { - this.video.muted = value; - } - - /** - * Whether or not the video is paused. - * - * @type {boolean} - */ - get paused() { - return this.video.paused; - } - - /** - * Start playback. - */ - play() { - this.video.play(); - } - - /** - * Pause playback. - */ - pause() { - this.video.pause(); - } - /** * Pause playback on the given {@link obj}. See {@link resumeOn}. * @@ -533,9 +489,9 @@ export default class DlfMediaPlayer { * @param {any} obj */ pauseOn(obj) { - if (this.videoPausedOn === null && !this.paused) { + if (this.videoPausedOn === null && !this.video.paused) { this.videoPausedOn = obj; - this.pause(); + this.video.pause(); } } @@ -547,7 +503,7 @@ export default class DlfMediaPlayer { */ resumeOn(obj) { if (this.videoPausedOn === obj) { - this.play(); + this.video.play(); } } @@ -560,6 +516,7 @@ export default class DlfMediaPlayer { } /** + * Seek to the specified {@link position} and mark this as a manual seek. * * @param {number | dlf.media.Chapter} position Timecode (in seconds) or chapter */ @@ -589,7 +546,7 @@ export default class DlfMediaPlayer { */ prevChapter() { const tolerance = this.constants.prevChapterTolerance; - const prev = this.chapters.timeToChapter(this.currentTime - tolerance); + const prev = this.chapters.timeToChapter(this.video.currentTime - tolerance); this.seekTo(prev ?? 0); } diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index f2f69d8..e1bdf25 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -372,7 +372,7 @@ export default class SlubMediaPlayer { // been attached. const target = /** @type {ChapterLink} */(e.currentTarget); - this.dlfPlayer.play(); + this.dlfPlayer.media.play(); this.dlfPlayer.seekTo(target.dlfTimecode); } @@ -416,7 +416,7 @@ export default class SlubMediaPlayer { return ( this.modals?.screenshot - .setVideo(this.dlfPlayer.getVideo()) + .setVideo(this.dlfPlayer.media) .setMetadata(this.videoInfo.metadata) .setFps(this.dlfPlayer.getFps()) .setTimecode(this.dlfPlayer.displayTime) From f70bbbcdcbbc179843a2a1befc960b672920e5e3 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Mon, 9 May 2022 19:55:31 +0200 Subject: [PATCH 22/25] Use only available actions in keybindings and help --- .../DlfMediaPlayer/DlfMediaPlayer.js | 140 +++++++++++------- .../controls/ControlPanelButton.js | 20 ++- .../controls/ControlPanelButton.test.js | 9 +- .../controls/FullScreenButton.js | 2 +- .../JavaScript/DlfMediaPlayer/index.js | 1 + .../JavaScript/DlfMediaPlayer/lib/action.js | 19 +++ .../JavaScript/DlfMediaPlayer/types.d.ts | 10 ++ .../SlubMediaPlayer/SlubMediaPlayer.js | 64 +++++--- .../SlubMediaPlayer/modals/HelpModal.js | 109 ++++++++++---- .../Private/Language/de.locallang_video.xlf | 4 + .../Private/Language/locallang_video.xlf | 3 + .../SlubMediaPlayer/modals/HelpModal.less | 6 + 12 files changed, 274 insertions(+), 113 deletions(-) create mode 100644 Resources/Private/JavaScript/DlfMediaPlayer/lib/action.js diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index a6c4711..d376671 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -5,6 +5,7 @@ import shaka from 'shaka-player/dist/shaka-player.ui'; import VideoFrame from './vendor/VideoFrame'; import typoConstants from '../lib/typoConstants'; +import { action } from './lib/action'; import { clamp, e } from '../lib/util'; import ShakaFrontend from './frontend/ShakaFrontend'; import Chapters from './Chapters'; @@ -86,58 +87,79 @@ export default class DlfMediaPlayer { /** @readonly */ this.actions = { - 'fullscreen.toggle': () => { - // Override in application - }, - 'playback.toggle': () => { + 'fullscreen.toggle': action({ + isAvailable: () => { + return document.fullscreenEnabled; + }, + execute: () => { + // Override in application + }, + }), + 'playback.toggle': action(() => { if (this.video.paused) { this.video.play(); } else { this.video.pause(); } - }, - 'playback.volume.mute.toggle': () => { + }), + 'playback.volume.mute.toggle': action(() => { this.video.muted = !this.video.muted; - }, - 'playback.volume.inc': () => { + }), + 'playback.volume.inc': action(() => { this.volume = this.volume + this.constants.volumeStep; - }, - 'playback.volume.dec': () => { + }), + 'playback.volume.dec': action(() => { this.volume = this.volume - this.constants.volumeStep; - }, - 'playback.captions.toggle': () => { - this.showCaptions = !this.showCaptions; - }, - 'navigate.rewind': () => { + }), + 'playback.captions.toggle': action({ + isAvailable: () => { + return this.player.getTextTracks().length > 0; + }, + execute: () => { + this.showCaptions = !this.showCaptions; + }, + }), + 'navigate.rewind': action(() => { this.skipSeconds(-this.constants.seekStep); - }, - 'navigate.seek': () => { + }), + 'navigate.seek': action(() => { this.skipSeconds(+this.constants.seekStep); - }, - 'navigate.continuous-rewind': () => { + }), + 'navigate.continuous-rewind': action(() => { this.ensureTrickPlay(-this.constants.trickPlayFactor); - }, - 'navigate.continuous-seek': () => { + }), + 'navigate.continuous-seek': action(() => { this.ensureTrickPlay(this.constants.trickPlayFactor); - }, - 'navigate.chapter.prev': () => { + }), + 'navigate.chapter.prev': action(() => { this.prevChapter(); - }, - 'navigate.chapter.next': () => { + }), + 'navigate.chapter.next': action(() => { this.nextChapter(); - }, - 'navigate.frame.prev': () => { - this.fps?.vifa.seekBackward(1); - this.frontend.afterManualSeek(); - }, - 'navigate.frame.next': () => { - this.fps?.vifa.seekForward(1); - this.frontend.afterManualSeek(); - }, - 'navigate.position.percental': ( - /** @type {Keybinding} */ kb, - /** @type {number} */ keyIndex - ) => { + }), + 'navigate.frame.prev': action({ + isAvailable: () => { + return this.fps !== null; + }, + execute: () => { + this.fps?.vifa.seekBackward(1); + this.frontend.afterManualSeek(); + }, + }), + 'navigate.frame.next': action(({ + isAvailable: () => { + return this.fps !== null; + }, + execute: () => { + this.fps?.vifa.seekForward(1); + this.frontend.afterManualSeek(); + }, + })), + 'navigate.position.percental': action((kb, keyIndex) => { + if (kb === undefined || keyIndex === undefined) { + return; + } + if (0 <= keyIndex && keyIndex < kb.keys.length) { // Implies kb.keys.length > 0 @@ -146,14 +168,18 @@ export default class DlfMediaPlayer { this.seekTo(absolute); } - }, - 'navigate.thumbnails.snap': ( - /** @type {Keybinding} */ _kb, - /** @type {number} */ _keyIndex, - /** @type {KeyEventMode} */ mode - ) => { - this.frontend.seekBar?.setThumbnailSnap(mode === 'down'); - }, + }), + 'navigate.thumbnails.snap': action({ + isAvailable: () => { + return ( + this.variantGroups !== null + && this.variantGroups.findThumbnailTracks().length > 0 + ); + }, + execute: (_kb, _keyIndex, mode) => { + this.frontend.seekBar?.setThumbnailSnap(mode === 'down'); + }, + }), } } @@ -184,19 +210,19 @@ export default class DlfMediaPlayer { case 'tapup': if (e.event.pointerType === 'mouse') { if (e.tapCount <= 2) { - this.actions['playback.toggle'](); + this.actions['playback.toggle'].execute(); } if (e.tapCount === 2) { - this.actions['fullscreen.toggle'](); + this.actions['fullscreen.toggle'].execute(); } } else if (e.tapCount >= 2) { if (e.position.x < 1 / 3) { - this.actions['navigate.rewind'](); + this.actions['navigate.rewind'].execute(); } else if (e.position.x > 2 / 3) { - this.actions['navigate.seek'](); + this.actions['navigate.seek'].execute(); } else if (e.tapCount === 2 && !this.env.isInFullScreen()) { - this.actions['fullscreen.toggle'](); + this.actions['fullscreen.toggle'].execute(); } } break; @@ -207,9 +233,9 @@ export default class DlfMediaPlayer { this.frontend.seekBar?.thumbnailPreview?.beginChange(e.event.clientX); } else if (e.tapCount >= 2) { if (e.position.x < 1 / 3) { - this.actions['navigate.continuous-rewind'](); + this.actions['navigate.continuous-rewind'].execute(); } else if (e.position.x > 2 / 3) { - this.actions['navigate.continuous-seek'](); + this.actions['navigate.continuous-seek'].execute(); } } break; @@ -217,9 +243,9 @@ export default class DlfMediaPlayer { case 'swipe': // "Natural" swiping if (e.direction === 'east') { - this.actions['navigate.rewind'](); + this.actions['navigate.rewind'].execute(); } else if (e.direction === 'west') { - this.actions['navigate.seek'](); + this.actions['navigate.seek'].execute(); } break; } @@ -459,6 +485,10 @@ export default class DlfMediaPlayer { this.player.setTextTrackVisibility(value); } + isAudioOnly() { + return this.player.isAudioOnly(); + } + /** * Volume in range [0, 1]. Out-of-bounds values are clamped when set. * diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.js index dc4b2f8..05e308c 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.js @@ -9,7 +9,7 @@ import { e, setElementClass } from '../../lib/util'; * @property {string} className * @property {string} material_icon Key of button icon * @property {string} title Text of button tooltip - * @property {() => void} onClick + * @property {dlf.media.PlayerAction} onClickAction */ /** @@ -58,8 +58,17 @@ export default class ControlPanelButton extends shaka.ui.Element { /** @protected Avoid naming conflicts with parent class */ this.dlf = { config, button }; - if (this.eventManager && config.onClick) { - this.eventManager.listen(button, 'click', config.onClick); + const { onClickAction } = config; + if (this.eventManager && onClickAction) { + this.eventManager.listen(button, 'click', () => { + if (onClickAction.isAvailable()) { + onClickAction.execute(); + } + }); + + this.eventManager.listen(this.player, 'loaded', () => { + this.updateControlPanelButton(); + }); } this.updateControlPanelButton(); @@ -72,5 +81,10 @@ export default class ControlPanelButton extends shaka.ui.Element { let tooltip = this.dlf.config.title ?? ""; this.dlf.button.ariaLabel = tooltip; setElementClass(this.dlf.button, 'shaka-tooltip', tooltip !== ""); + + const { onClickAction } = this.dlf.config; + if (onClickAction) { + setElementClass(this.dlf.button, 'shaka-hidden', !onClickAction.isAvailable()); + } } } diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.test.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.test.js index 9167086..b5ca4f3 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.test.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/ControlPanelButton.test.js @@ -6,6 +6,7 @@ import { describe, expect, test } from '@jest/globals'; import Environment from '../../SlubMediaPlayer/Environment'; +import { action } from '../lib/action'; import ControlPanelButton from './ControlPanelButton'; import { createShakaPlayer } from './test-util'; @@ -19,9 +20,11 @@ describe('ControlPanelButton', () => { const button = new ControlPanelButton(buttonContainer, shk.controls, env, { material_icon: 'info', title: "Do it now", - onClick: () => { - clicked++; - }, + onClickAction: action({ + execute: () => { + clicked++; + }, + }), }); const domButton = buttonContainer.querySelector('button'); expect(domButton?.ariaLabel).toBe("Do it now"); diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/FullScreenButton.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FullScreenButton.js index 5e586cb..cae1e82 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/FullScreenButton.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FullScreenButton.js @@ -5,7 +5,7 @@ import ControlPanelButton from './ControlPanelButton'; /** * @typedef Config - * @property {() => void} onClick + * @property {dlf.media.PlayerAction} onClickAction */ /** diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/index.js b/Resources/Private/JavaScript/DlfMediaPlayer/index.js index 2b75820..77a66ac 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/index.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/index.js @@ -2,5 +2,6 @@ export { default as Chapters } from './Chapters'; export { ControlPanelButton, FullScreenButton, OverflowMenuButton } from './controls'; +export { action } from './lib/action'; export { default as buildTimeString, timeStringFromTemplate } from './lib/buildTimeString'; export { default as DlfMediaPlayer } from './DlfMediaPlayer'; diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/lib/action.js b/Resources/Private/JavaScript/DlfMediaPlayer/lib/action.js new file mode 100644 index 0000000..ce1e2a2 --- /dev/null +++ b/Resources/Private/JavaScript/DlfMediaPlayer/lib/action.js @@ -0,0 +1,19 @@ +// @ts-check + +/** + * @param {Partial | dlf.media.PlayerAction['execute']} obj + * @return {dlf.media.PlayerAction} + */ +export function action(obj) { + if (typeof obj === 'function') { + return { + isAvailable: () => true, + execute: obj, + }; + } else { + return { + isAvailable: obj.isAvailable ?? (() => true), + execute: obj.execute ?? (() => { }), + }; + } +} diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts index 58d13ac..d2c2faf 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/DlfMediaPlayer/types.d.ts @@ -48,6 +48,16 @@ namespace dlf { trickPlayFactor: number; }; + type PlayerAction = { + isAvailable: () => boolean; + // TODO: Make action more independent of keybindings; could also be triggered in gesture + execute: ( + kb?: Keybinding, + keyIndex?: number, + mode?: KeyEventMode + ) => void; + }; + type Fps = { rate: number; vifa: import("./vendor/VideoFrame").default; diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index e1bdf25..cc4b1bc 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -4,6 +4,7 @@ import { e } from '../lib/util'; import { Keybindings$find } from '../lib/Keyboard'; import typoConstants from '../lib/typoConstants'; import { + action, Chapters, ControlPanelButton, DlfMediaPlayer, @@ -81,37 +82,47 @@ export default class SlubMediaPlayer { this.modals = null; /** @private */ - this.dlfPlayer.actions['fullscreen.toggle'] = () => { + this.dlfPlayer.actions['fullscreen.toggle'].execute = () => { this.dlfPlayer.ui.seekBar?.endSeek(); this.toggleFullScreen(); }; this.actions = Object.assign({}, this.dlfPlayer.actions, { - 'cancel': () => { + 'cancel': action(() => { if (this.modals?.hasOpen()) { this.modals.closeNext(); } else { this.dlfPlayer.ui.handleEscape(); } - }, - 'modal.help.open': () => { + }), + 'modal.help.open': action(() => { this.openModal(this.modals?.help); - }, - 'modal.help.toggle': () => { + }), + 'modal.help.toggle': action(() => { if (this.modals !== null) { this.dlfPlayer.ui.seekBar?.endSeek(); this.modals.toggleExclusive(this.modals.help); } - }, - 'modal.bookmark.open': () => { + }), + 'modal.bookmark.open': action(() => { this.showBookmarkUrl(); - }, - 'modal.screenshot.open': () => { - this.showScreenshot(); - }, - 'modal.screenshot.snap': () => { - this.snapScreenshot(); - }, - 'theater.toggle': () => { + }), + 'modal.screenshot.open': action({ + isAvailable: () => { + return !this.dlfPlayer.isAudioOnly(); + }, + execute: () => { + this.showScreenshot(); + }, + }), + 'modal.screenshot.snap': action({ + isAvailable: () => { + return !this.dlfPlayer.isAudioOnly(); + }, + execute: () => { + this.snapScreenshot(); + }, + }), + 'theater.toggle': action(() => { this.dlfPlayer.ui.seekBar?.endSeek(); // @see DigitalcollectionsScripts.js @@ -124,7 +135,7 @@ export default class SlubMediaPlayer { }, }); window.dispatchEvent(ev); - }, + }), }); this.createModals(); @@ -142,6 +153,11 @@ export default class SlubMediaPlayer { forceLandscapeOnFullscreen: Number(this.constants.forceLandscapeOnFullscreen), }, keybindings: this.keybindings, + actionIsAvailable: (actionKey) => { + // @ts-expect-error + const action = this.actions[actionKey]; + return action !== undefined && action.isAvailable(); + }, }), bookmark: new BookmarkModal(this.fullscreenElement, this.env, { shareButtons: this.config.shareButtons, @@ -231,22 +247,22 @@ export default class SlubMediaPlayer { className: "sxnd-screenshot-button", material_icon: 'photo_camera', title: this.env.t('control.screenshot.tooltip'), - onClick: this.actions['modal.screenshot.open'], + onClickAction: this.actions['modal.screenshot.open'], }), ControlPanelButton.register(this.env, { className: "sxnd-bookmark-button", material_icon: 'bookmark_border', title: this.env.t('control.bookmark.tooltip'), - onClick: this.actions['modal.bookmark.open'], + onClickAction: this.actions['modal.bookmark.open'], }), FullScreenButton.register(this.env, { - onClick: this.actions['fullscreen.toggle'], + onClickAction: this.actions['fullscreen.toggle'], }), ControlPanelButton.register(this.env, { className: "sxnd-help-button", material_icon: 'info_outline', title: this.env.t('control.help.tooltip'), - onClick: this.actions['modal.help.open'], + onClickAction: this.actions['modal.help.open'], }) ); } @@ -355,8 +371,10 @@ export default class SlubMediaPlayer { || (mode === 'up' && (keybinding.keyup ?? false)) ); - if (shouldHandle) { - this.actions[keybinding.action]?.(keybinding, keyIndex, mode); + const action = this.actions[keybinding.action]; + + if (shouldHandle && action !== undefined && action.isAvailable()) { + action.execute(keybinding, keyIndex, mode); } } } diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/modals/HelpModal.js b/Resources/Private/JavaScript/SlubMediaPlayer/modals/HelpModal.js index 9871ef4..5b89d75 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/modals/HelpModal.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/modals/HelpModal.js @@ -1,6 +1,6 @@ // @ts-check -import { domJoin, e } from '../../lib/util'; +import { domJoin, e, setElementClass } from '../../lib/util'; import SimpleModal from '../lib/SimpleModal'; import { getKeybindingText } from '../lib/trans'; @@ -9,6 +9,15 @@ import { getKeybindingText } from '../lib/trans'; * @typedef {string} KeybindingAction See `Keybinding::action`. * @typedef {Keybinding} ShownKeybinding * @typedef {Record>} KeybindingGroups + * + * @typedef {{ + * thead: HTMLTableSectionElement; + * tbody: HTMLTableSectionElement; + * rows: { + * action: KeybindingAction; + * tr: HTMLTableRowElement; + * }[]; + * }} TableSection */ /** @@ -58,6 +67,7 @@ export default class HelpModal extends SimpleModal { * @param {object} config * @param {Record} config.constants * @param {ShownKeybinding[]} config.keybindings + * @param {(actionKey: KeybindingAction) => boolean} config.actionIsAvailable */ constructor(parent, env, config) { super(parent, {}); @@ -67,7 +77,24 @@ export default class HelpModal extends SimpleModal { /** @private */ this.config = config; + /** @private @type {TableSection[]} */ + this.tableSections = []; + this.createBodyDom(); + this.updateRowVisibility(); + } + + /** + * + * @override + * @param {boolean} value + */ + open(value = true) { + if (value) { + this.updateRowVisibility(); + } + + super.open(value); } createBodyDom() { @@ -76,35 +103,41 @@ export default class HelpModal extends SimpleModal { this.$main.classList.add('help-modal'); this.$title.innerText = env.t('modal.help.title'); + const $table = e("table", { className: "keybindings-table" }); + const allKbGrouped = groupKeybindings(this.config.keybindings); + for (const [kind, kbGrouped] of Object.entries(allKbGrouped)) { + const keybindings = [...Object.entries(kbGrouped)]; + if (keybindings.length === 0) { + continue; + } + + /** @type {TableSection} */ + const section = { + thead: e("thead", {}, [ + e("th", { className: "kb-group", colSpan: 2 }, [ + env.t(`action.kind.${kind}`) + ]), + ]), + tbody: e("tbody", {}), + rows: [], + }; + + for (const [action, kbs] of keybindings) { + const tr = e("tr", {}, [ + e("td", { className: "key" }, this.listKeybindings(kbs)), + e("td", { className: "action" }, [this.describeAction(action)]), + ]); + + section.rows.push({ action, tr }); + section.tbody.append(tr); + } + + $table.append(section.thead, section.tbody); + this.tableSections.push(section); + } - const els = e("table", { className: "keybindings-table" }, ( - Object.entries(allKbGrouped) - .flatMap(([kind, kbGrouped]) => { - const keybindings = [...Object.entries(kbGrouped)]; - if (keybindings.length === 0) { - return; - } - - return [ - e("thead", {}, [ - e("th", { className: "kb-group", colSpan: 2 }, [ - env.t(`action.kind.${kind}`) - ]), - ]), - e("tbody", {}, ( - keybindings.map(([action, kbs]) => ( - e("tr", {}, [ - e("td", { className: "key" }, this.listKeybindings(kbs)), - e("td", { className: "action" }, [this.describeAction(action)]), - ]) - )) - )) - ]; - }) - )); - - this.$body.append(els); + this.$body.append($table); } /** @@ -125,4 +158,24 @@ export default class HelpModal extends SimpleModal { describeAction(action) { return this.env.t(`action.${action}`, this.config.constants); } + + updateRowVisibility() { + for (const section of this.tableSections) { + let someIsAvailable = false; + + for (const row of section.rows) { + const isAvailable = this.config.actionIsAvailable(row.action); + + row.tr.setAttribute('aria-disabled', isAvailable ? 'false' : 'true'); + const text = isAvailable ? "" : this.env.t('action.unavailable'); + row.tr.setAttribute('title', text); + row.tr.setAttribute('aria-label', text); + + someIsAvailable ||= isAvailable; + } + + setElementClass(section.thead, 'action-unavailable', !someIsAvailable); + setElementClass(section.tbody, 'action-unavailable', !someIsAvailable); + } + } } diff --git a/Resources/Private/Language/de.locallang_video.xlf b/Resources/Private/Language/de.locallang_video.xlf index 41285ab..02dd7e7 100644 --- a/Resources/Private/Language/de.locallang_video.xlf +++ b/Resources/Private/Language/de.locallang_video.xlf @@ -105,6 +105,10 @@ + + + + diff --git a/Resources/Private/Language/locallang_video.xlf b/Resources/Private/Language/locallang_video.xlf index b9dffc4..fe94c8b 100644 --- a/Resources/Private/Language/locallang_video.xlf +++ b/Resources/Private/Language/locallang_video.xlf @@ -80,6 +80,9 @@ + + + diff --git a/Resources/Private/Less/SlubMediaPlayer/modals/HelpModal.less b/Resources/Private/Less/SlubMediaPlayer/modals/HelpModal.less index 4b87f1e..64dd4b2 100644 --- a/Resources/Private/Less/SlubMediaPlayer/modals/HelpModal.less +++ b/Resources/Private/Less/SlubMediaPlayer/modals/HelpModal.less @@ -30,6 +30,12 @@ font-weight: bold; } + thead, tbody, tbody tr { + [aria-disabled="true"] { + opacity: 0.4; + } + } + tbody tr { &:first-child { border-top: 1px solid @light-color; From ecd08133abd10f9bf3c60f259e1e51a65846e9ed Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Thu, 12 May 2022 22:16:39 +0200 Subject: [PATCH 23/25] In audio mode, hide preview when leaving seekbar --- .../DlfMediaPlayer/ThumbnailPreview.js | 25 ++++++++++++++++++- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 10 +++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/ThumbnailPreview.js b/Resources/Private/JavaScript/DlfMediaPlayer/ThumbnailPreview.js index 1246c3b..f100796 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/ThumbnailPreview.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/ThumbnailPreview.js @@ -50,10 +50,19 @@ import sanitizeThumbnail from './lib/thumbnails/sanitizeThumbnail'; * network: ImageFetcher; * interaction: Interaction; * }} Params + * + * @typedef {'wide' | 'narrow'} SeekMode In wide mode, the whole height of the thumbnail container + * can be used and stays open for seeking. In narrow mode, only the seek bar can be used; the thumbnail + * is closed when the mouse leaves the seek bar. */ const DISPLAY_WIDTH = 160; const INITIAL_ASPECT_RATIO = 16 / 9; + +/** + * Delay in milliseconds from hovering the seek bar until the thumbnail + * container is opened. + */ const OPEN_DISPLAY_DELAY = 100; /** @@ -108,6 +117,8 @@ export default class ThumbnailPreview { this.current = null; /** @private @type {number | null} */ this.renderAnimationFrame = null; + /** @private @type {SeekMode} */ + this.seekMode = 'wide'; /** @private */ this.openDisplayTimeout = null; @@ -207,6 +218,15 @@ export default class ThumbnailPreview { this.currentRenderBest(); } + /** + * + * @param {SeekMode} mode + */ + setSeekMode(mode) { + this.seekMode = mode; + this.currentRenderBest(); + } + /** * @private */ @@ -331,7 +351,10 @@ export default class ThumbnailPreview { return; } - let { top } = this.$container.getBoundingClientRect(); + let { top } = this.seekMode === 'wide' + ? this.$container.getBoundingClientRect() + : this.seekBar.getBoundingClientRect(); + // We don't want the thumbnail preview to be opened accidentally. If the // user is hovering quickly from below to above the seek bar, shrink the // area above the seek bar that would keep the thumbnail preview open. diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index 469c786..cd7c3c4 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -130,9 +130,10 @@ export default class ShakaFrontend { this.controls.addEventListener('dlf-media-seek-bar', (e) => { const detail = /** @type {dlf.media.SeekBarEvent} */(e).detail; this.seekBar_ = detail.seekBar; + this.autosetSeekMode(); }); - this.controls.addEventListener('timeandseekrangeupdated', this.handlers.onTimeUpdate); this.controls.addEventListener('dlf-media-manual-seek', this.handlers.afterManualSeek); + this.controls.addEventListener('timeandseekrangeupdated', this.handlers.onTimeUpdate); this.media.addEventListener('play', this.handlers.onPlay); @@ -280,6 +281,8 @@ export default class ShakaFrontend { this.ui.configure(this.getShakaConfiguration()); this.isConfigured = true; + this.autosetSeekMode(); + // Fade in controls, especially when switching from video to audio (TODO: Refactor) this.$videoBox.dispatchEvent(new MouseEvent('mousemove')); @@ -290,6 +293,11 @@ export default class ShakaFrontend { this.notifyMediaProperties(); } + autosetSeekMode() { + const seekMode = this.playerProperties.mode === 'audio' ? 'narrow' : 'wide'; + this.seekBar_?.thumbnailPreview?.setSeekMode(seekMode); + } + /** * @private */ From 9d7d007c7c3c05fdcc889f5cd34c8e2c075ef8e4 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Mon, 16 May 2022 13:55:40 +0200 Subject: [PATCH 24/25] Allow configuring default and fallback mode --- Classes/ViewHelpers/VideoToJsonViewHelper.php | 2 ++ .../DlfMediaPlayer/DlfMediaPlayer.js | 24 ++++++++++++++++--- .../SlubMediaPlayer/SlubMediaPlayer.js | 1 + .../JavaScript/SlubMediaPlayer/types.d.ts | 2 ++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Classes/ViewHelpers/VideoToJsonViewHelper.php b/Classes/ViewHelpers/VideoToJsonViewHelper.php index ab7864a..f76df2a 100755 --- a/Classes/ViewHelpers/VideoToJsonViewHelper.php +++ b/Classes/ViewHelpers/VideoToJsonViewHelper.php @@ -65,6 +65,8 @@ public static function renderStatic( 'url' => [ 'poster' => "https://media.sachsen.digital/$movieDir/$movieDir.jpg", ], + + 'mode' => 'video', ]; return json_encode($result); diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js index d376671..2897ad7 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/DlfMediaPlayer.js @@ -76,6 +76,9 @@ export default class DlfMediaPlayer { /** @private @type {dlf.media.PlayerFrontend} */ this.frontend = new ShakaFrontend(this.env, this.player, this.video); + /** @private @type {dlf.media.PlayerMode | 'auto'} */ + this.mode = 'auto'; + /** @private */ this.handlers = { onPlayerErrorEvent: this.onPlayerErrorEvent.bind(this), @@ -348,9 +351,11 @@ export default class DlfMediaPlayer { for (const source of this.sources_) { try { await this.loadManifest(source); - this.ui.updatePlayerProperties({ - mode: this.player.isAudioOnly() ? 'audio' : 'video', - }); + if (this.mode === 'auto') { + this.frontend.updatePlayerProperties({ + mode: this.player.isAudioOnly() ? 'audio' : 'video', + }); + } return true; } catch (e) { console.error(e); @@ -412,6 +417,19 @@ export default class DlfMediaPlayer { this.videoPausedOn = null; } + /** + * + * @param {dlf.media.PlayerMode | 'auto'} mode + * @param {dlf.media.PlayerMode} fallbackMode + */ + setPlayerMode(mode, fallbackMode = 'audio') { + this.frontend.updatePlayerProperties({ + mode: mode === 'auto' ? fallbackMode : mode, + }); + + this.mode = mode; + } + /** * * @param {Chapters} chapters diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js index cc4b1bc..fe31531 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js +++ b/Resources/Private/JavaScript/SlubMediaPlayer/SlubMediaPlayer.js @@ -74,6 +74,7 @@ export default class SlubMediaPlayer { /** @private */ this.dlfPlayer = new DlfMediaPlayer(this.env); this.dlfPlayer.parseConstants(config.constants ?? {}); + this.dlfPlayer.setPlayerMode(videoInfo.mode ?? 'auto', videoInfo.fallbackMode ?? 'audio'); /** @private @type {ChapterLink[]} */ this.chapterLinks = []; diff --git a/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts b/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts index f0b1c27..8f6e7b4 100644 --- a/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts +++ b/Resources/Private/JavaScript/SlubMediaPlayer/types.d.ts @@ -37,6 +37,8 @@ type VideoInfo = { url: { poster?: string; }; + mode?: dlf.media.PlayerMode | 'auto'; + fallbackMode?: dlf.media.PlayerMode; }; type MetadataArray = Record; From 54aae589b07cdf1004f5a04b9b63049ece0cfb62 Mon Sep 17 00:00:00 2001 From: Kajetan Dvoracek Date: Mon, 16 May 2022 20:07:26 +0200 Subject: [PATCH 25/25] Allow to style frontend via CSS variables --- .../DlfMediaPlayer/controls/FlatSeekBar.js | 8 ++- .../DlfMediaPlayer/frontend/ShakaFrontend.js | 25 ++----- .../Less/DlfMediaPlayer/ShakaFrontend.less | 66 +++++++++++++------ 3 files changed, 58 insertions(+), 41 deletions(-) diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js index 566ceac..7e562d2 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/controls/FlatSeekBar.js @@ -299,7 +299,13 @@ export default class FlatSeekBar extends shaka.ui.Element { this.dlf.hasRenderedChapters = true; } - const colors = this.dlf.uiConfig.seekBarColors; + const style = getComputedStyle(this.$container); + const colors = { + base: style.getPropertyValue('--base-color') || 'rgba(255, 255, 255, 0.3)', + buffered: style.getPropertyValue('--buffered-color') || 'rgba(255, 255, 255, 0.54)', + played: style.getPropertyValue('--played-color') || 'rgb(255, 255, 255)', + }; + const currentTime = this.getValue(); const bufferedLength = this.video.buffered.length; const bufferedStart = bufferedLength ? this.video.buffered.start(0) : 0; diff --git a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js index cd7c3c4..2010d75 100644 --- a/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js +++ b/Resources/Private/JavaScript/DlfMediaPlayer/frontend/ShakaFrontend.js @@ -304,6 +304,8 @@ export default class ShakaFrontend { getShakaConfiguration() { const playerMode = this.playerProperties.mode; + const style = getComputedStyle(this.$container); + /** @type {any} */ const result = { addSeekBar: true, @@ -331,25 +333,10 @@ export default class ShakaFrontend { fadeDelay: playerMode === 'audio' ? 100_000_000 // Just some large value : undefined, // Use default - seekBarColors: playerMode === 'video' - ? { - base: 'rgba(255, 255, 255, 0.3)', - buffered: 'rgba(255, 255, 255, 0.54)', - played: 'rgb(255, 255, 255)', - adBreaks: 'rgb(255, 204, 0)', - } - : { - base: 'rgba(0, 0, 0, 0.3)', - buffered: 'rgba(0, 0, 0, 0.54)', - played: '#2a2b2c', - adBreaks: 'rgb(255, 204, 0)', - }, - volumeBarColors: playerMode === 'audio' - ? { - base: 'rgba(0, 0, 0, 40%)', - level: 'rgb(0, 0, 0, 80%)', - } - : undefined, // Use default + volumeBarColors: { + base: style.getPropertyValue('--volume-base-color') || 'rgba(255, 255, 255, 0.54)', + level: style.getPropertyValue('--volume-level-color') || 'rgb(255, 255, 255)', + }, enableKeyboardPlaybackControls: false, doubleClickForFullscreen: false, singleClickForPlayAndPause: false, diff --git a/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less b/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less index 4817626..73e2f66 100644 --- a/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less +++ b/Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less @@ -1,6 +1,34 @@ -@audio-controls-color: #2a2b2c; - .dlf-shaka { + &[data-mode="video"] { + --controls-color: white; + + --volume-base-color: rgba(255, 255, 255, 0.54); + --volume-level-color: rgba(255, 255, 255); + + .dlf-media-flat-seek-bar { + --base-color: rgba(255, 255, 255, 0.3); + --buffered-color: rgba(255, 255, 255, 0.54); + --played-color: rgb(255, 255, 255); + } + } + + &[data-mode="audio"] { + --controls-color: #2a2b2c; + + --volume-base-color: rgba(0, 0, 0, 0.4); + --volume-level-color: rgba(0, 0, 0, 0.8); + + .dlf-media-flat-seek-bar { + --base-color: rgba(0, 0, 0, 0.3); + --buffered-color: rgba(0, 0, 0, 0.54); + --played-color: #2a2b2c; + + .dlf-media-chapter-marker { + background-color: lighten(rgb(79, 179, 199), 30%); + } + } + } + .dlf-media-shaka-box { background-color: black; position: absolute; @@ -30,6 +58,21 @@ display: none; } + .dlf-media-error { + color: var(--controls-color); + } + + .shaka-controls-button-panel > * { + color: var(--controls-color) !important; + } + + .shaka-volume-bar { + &::-webkit-slider-thumb, + &::-moz-range-thumb { + background: var(--volume-level-color); + } + } + &[data-mode="audio"] { height: 3.5em; position: absolute; @@ -38,10 +81,6 @@ background-color: rgba(79, 179, 199, 0.6); - .dlf-media-error { - color: @audio-controls-color; - } - video { display: none; } @@ -59,24 +98,9 @@ display: none; } - .shaka-controls-button-panel > * { - color: @audio-controls-color; - } - .shaka-bottom-controls { width: 100%; padding-bottom: 0.4em; } - - .shaka-volume-bar { - &::-webkit-slider-thumb, - &::-moz-range-thumb { - background: @audio-controls-color; - } - } - - .dlf-media-chapter-marker { - background-color: lighten(rgb(79, 179, 199), 30%); - } } }