From 3723e02676a1783d2fa63f7896e55b9148172a6c Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Wed, 31 May 2023 20:33:03 +0900 Subject: [PATCH 01/23] feat: define `play()` `pause()` `stop()` --- src/player.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/player.ts b/src/player.ts index 5c35e6a..a6a8d18 100644 --- a/src/player.ts +++ b/src/player.ts @@ -36,7 +36,8 @@ let playingPlayer: PlayerElement = null; * @attr visualizer - A selector matching `midi-visualizer` elements to bind to this player * * @fires load - The content is loaded and ready to play - * @fires start - The player has started playing + * @fires play - The player has started playing + * @fires pause - The player has paused playing * @fires stop - The player has stopped playing * @fires loop - The player has automatically restarted playback after reaching the end * @fires note - A note starts @@ -215,6 +216,11 @@ export class PlayerElement extends HTMLElement { } start() { + console.warn('[html-midi-player] please use play() instead of start()'); + this._start(); + } + + play() { this._start(); } @@ -266,13 +272,20 @@ export class PlayerElement extends HTMLElement { })(); } - stop() { + pause() { if (this.player && this.player.isPlaying()) { this.player.stop(); } this.handleStop(false); } + stop() { + if (this.player && this.player.isPlaying()) { + this.player.stop(); + } + this.handleStop(true); + } + addVisualizer(visualizer: VisualizerElement) { const listeners = { start: () => { visualizer.noteSequence = this.noteSequence; }, From 998ee23a4c3cf46f0ec51cc893321fee988ef7f5 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 10:32:45 +0900 Subject: [PATCH 02/23] refactor(player): use async await pattern for play() --- src/player.ts | 87 +++++++++++++++++++++++---------------------------- 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/src/player.ts b/src/player.ts index a6a8d18..b747320 100644 --- a/src/player.ts +++ b/src/player.ts @@ -215,61 +215,54 @@ export class PlayerElement extends HTMLElement { this.initPlayerNow(); } - start() { - console.warn('[html-midi-player] please use play() instead of start()'); - this._start(); + async play() { + await this._start(); } - play() { - this._start(); - } - - protected _start(looped = false) { - (async () => { - if (this.player) { - if (this.player.getPlayState() == 'stopped') { - if (playingPlayer && playingPlayer.playing && !(playingPlayer == this && looped)) { - playingPlayer.stop(); - } - playingPlayer = this; - this._playing = true; + protected async _start(looped = false) { + if (this.player) { + if (this.player.getPlayState() == 'stopped') { + if (playingPlayer && playingPlayer.playing && !(playingPlayer == this && looped)) { + playingPlayer.stop(); + } + playingPlayer = this; + this._playing = true; - let offset = this.currentTime; - // Jump to the start if there are no notes left to play. - if (this.ns.notes.filter((note) => note.startTime > offset).length == 0) { - offset = 0; - } - this.currentTime = offset; - - this.controlPanel.classList.remove('stopped'); - this.controlPanel.classList.add('playing'); - try { - // Force reload visualizers to prevent stuttering at playback start - for (const visualizer of this.visualizerListeners.keys()) { - if (visualizer.noteSequence != this.ns) { - visualizer.noteSequence = this.ns; - visualizer.reload(); - } + let offset = this.currentTime; + // Jump to the start if there are no notes left to play. + if (this.ns.notes.filter((note) => note.startTime > offset).length == 0) { + offset = 0; + } + this.currentTime = offset; + + this.controlPanel.classList.remove('stopped'); + this.controlPanel.classList.add('playing'); + try { + // Force reload visualizers to prevent stuttering at playback start + for (const visualizer of this.visualizerListeners.keys()) { + if (visualizer.noteSequence != this.ns) { + visualizer.noteSequence = this.ns; + visualizer.reload(); } + } - const promise = this.player.start(this.ns, undefined, offset); - if (!looped) { - this.dispatchEvent(new CustomEvent('start')); - } else { - this.dispatchEvent(new CustomEvent('loop')); - } - await promise; - this.handleStop(true); - } catch (error) { - this.handleStop(); - throw error; + const promise = this.player.start(this.ns, undefined, offset); + if (!looped) { + this.dispatchEvent(new CustomEvent('start')); + } else { + this.dispatchEvent(new CustomEvent('loop')); } - } else if (this.player.getPlayState() == 'paused') { - // This normally should not happen, since we pause playback only when seeking. - this.player.resume(); + await promise; + this.handleStop(true); + } catch (error) { + this.handleStop(); + throw error; } + } else if (this.player.getPlayState() == 'paused') { + // This normally should not happen, since we pause playback only when seeking. + this.player.resume(); } - })(); + } } pause() { From bf10ce5100bf1a054190f3e3cb6c000845a9d275 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 10:41:24 +0900 Subject: [PATCH 03/23] refactor: change event name to 'play', 'pause' --- src/player.ts | 100 +++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/player.ts b/src/player.ts index b747320..b449ba1 100644 --- a/src/player.ts +++ b/src/player.ts @@ -7,7 +7,7 @@ import {VisualizerElement} from './visualizer'; export type NoteEvent = CustomEvent<{note: NoteSequence.INote}>; -const VISUALIZER_EVENTS = ['start', 'stop', 'note'] as const; +const VISUALIZER_EVENTS = ['play', 'pause', 'note'] as const; const DEFAULT_SOUNDFONT = 'https://storage.googleapis.com/magentadata/js/soundfonts/sgm_plus'; let playingPlayer: PlayerElement = null; @@ -98,25 +98,23 @@ export class PlayerElement extends HTMLElement { if (this.player.isPlaying()) { this.stop(); } else { - this.start(); + this.play(); } }); this.seekBar.addEventListener('input', () => { // Pause playback while the user is manipulating the control this.seeking = true; - if (this.player && this.player.getPlayState() === 'started') { + if (this.player?.getPlayState() === 'started') { this.player.pause(); } }); this.seekBar.addEventListener('change', () => { const time = this.currentTime; // This returns the seek bar value as a number this.currentTimeLabel.textContent = utils.formatTime(time); - if (this.player) { - if (this.player.isPlaying()) { - this.player.seekTo(time); - if (this.player.getPlayState() === 'paused') { - this.player.resume(); - } + if (this.player?.isPlaying()) { + this.player.seekTo(time); + if (this.player.getPlayState() === 'paused') { + this.player.resume(); } } this.seeking = false; @@ -220,54 +218,56 @@ export class PlayerElement extends HTMLElement { } protected async _start(looped = false) { - if (this.player) { - if (this.player.getPlayState() == 'stopped') { - if (playingPlayer && playingPlayer.playing && !(playingPlayer == this && looped)) { - playingPlayer.stop(); - } - playingPlayer = this; - this._playing = true; + if (!this.player) { + return; + } - let offset = this.currentTime; - // Jump to the start if there are no notes left to play. - if (this.ns.notes.filter((note) => note.startTime > offset).length == 0) { - offset = 0; - } - this.currentTime = offset; - - this.controlPanel.classList.remove('stopped'); - this.controlPanel.classList.add('playing'); - try { - // Force reload visualizers to prevent stuttering at playback start - for (const visualizer of this.visualizerListeners.keys()) { - if (visualizer.noteSequence != this.ns) { - visualizer.noteSequence = this.ns; - visualizer.reload(); - } - } + if (this.player.getPlayState() == 'stopped') { + if (playingPlayer && playingPlayer.playing && !(playingPlayer == this && looped)) { + playingPlayer.stop(); + } + playingPlayer = this; + this._playing = true; - const promise = this.player.start(this.ns, undefined, offset); - if (!looped) { - this.dispatchEvent(new CustomEvent('start')); - } else { - this.dispatchEvent(new CustomEvent('loop')); + let offset = this.currentTime; + // Jump to the start if there are no notes left to play. + if (this.ns.notes.filter((note) => note.startTime > offset).length == 0) { + offset = 0; + } + this.currentTime = offset; + + this.controlPanel.classList.remove('stopped'); + this.controlPanel.classList.add('playing'); + try { + // Force reload visualizers to prevent stuttering at playback start + for (const visualizer of this.visualizerListeners.keys()) { + if (visualizer.noteSequence != this.ns) { + visualizer.noteSequence = this.ns; + visualizer.reload(); } - await promise; - this.handleStop(true); - } catch (error) { - this.handleStop(); - throw error; } - } else if (this.player.getPlayState() == 'paused') { - // This normally should not happen, since we pause playback only when seeking. - this.player.resume(); + + const promise = this.player.start(this.ns, undefined, offset); + if (!looped) { + this.dispatchEvent(new CustomEvent('start')); + } else { + this.dispatchEvent(new CustomEvent('loop')); + } + await promise; + this.handleStop(true); + } catch (error) { + this.handleStop(); + throw error; } + } else if (this.player.getPlayState() == 'paused') { + // This normally should not happen, since we pause playback only when seeking. + this.player.resume(); } } pause() { if (this.player && this.player.isPlaying()) { - this.player.stop(); + this.player.pause(); } this.handleStop(false); } @@ -281,8 +281,8 @@ export class PlayerElement extends HTMLElement { addVisualizer(visualizer: VisualizerElement) { const listeners = { - start: () => { visualizer.noteSequence = this.noteSequence; }, - stop: () => { visualizer.clearActiveNotes(); }, + play: () => { visualizer.noteSequence = this.noteSequence; }, + pause: () => { visualizer.clearActiveNotes(); }, note: (event: NoteEvent) => { visualizer.redraw(event.detail.note); }, } as const; for (const name of VISUALIZER_EVENTS) { @@ -324,7 +324,7 @@ export class PlayerElement extends HTMLElement { this.controlPanel.classList.add('stopped'); if (this._playing) { this._playing = false; - this.dispatchEvent(new CustomEvent('stop', {detail: {finished}})); + this.dispatchEvent(new CustomEvent('pause', {detail: {finished}})); } } From 41ee92cf48e227d958519c6d42d2089890193b08 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 10:50:32 +0900 Subject: [PATCH 04/23] feat(canPlayType): add playType spec with partially support --- src/player.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/player.ts b/src/player.ts index b449ba1..3c1a4c2 100644 --- a/src/player.ts +++ b/src/player.ts @@ -437,6 +437,15 @@ export class PlayerElement extends HTMLElement { return this._playing; } + canPlayType(type: unknown): string { + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType + // return type: '' | 'maybe' | 'probably'; + // TODO: consider 'maybe' return case in some cases. + return typeof type === 'string' && ( + type === 'audio/midi' || type === 'audio/x-midi' + ) ? 'probably' : ''; + } + protected setOrRemoveAttribute(name: string, value: string) { if (value == null) { this.removeAttribute(name); From 48ba7f79dfd4035269f7aa40fac5998a9baa7475 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 10:53:55 +0900 Subject: [PATCH 05/23] feat: add paused property --- src/player.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/player.ts b/src/player.ts index 3c1a4c2..be63a5c 100644 --- a/src/player.ts +++ b/src/player.ts @@ -32,6 +32,7 @@ let playingPlayer: PlayerElement = null; * @prop loop - Indicates whether the player should loop * @prop currentTime - Current playback position in seconds * @prop duration - Content duration in seconds + * @prop paused - Indicates whether the player is currently paused * @prop playing - Indicates whether the player is currently playing * @attr visualizer - A selector matching `midi-visualizer` elements to bind to this player * @@ -437,6 +438,10 @@ export class PlayerElement extends HTMLElement { return this._playing; } + get paused() { + return this.player?.getPlayState() === 'paused'; + } + canPlayType(type: unknown): string { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType // return type: '' | 'maybe' | 'probably'; From ee3d180a4423240fae0862ff52ab70d8e27428dd Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 11:16:25 +0900 Subject: [PATCH 06/23] feat: add error event timing --- src/player.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/player.ts b/src/player.ts index be63a5c..f946993 100644 --- a/src/player.ts +++ b/src/player.ts @@ -167,6 +167,7 @@ export class PlayerElement extends HTMLElement { this.currentTime = 0; if (!this.ns) { this.setError('No content loaded'); + this.dispatchError('No content loaded'); } } ns = this.ns; @@ -206,6 +207,7 @@ export class PlayerElement extends HTMLElement { this.dispatchEvent(new CustomEvent('load')); } catch (error) { this.setError(String(error)); + this.dispatchError(error); throw error; } } @@ -258,6 +260,7 @@ export class PlayerElement extends HTMLElement { this.handleStop(true); } catch (error) { this.handleStop(); + this.dispatchError(error); throw error; } } else if (this.player.getPlayState() == 'paused') { @@ -312,6 +315,16 @@ export class PlayerElement extends HTMLElement { this.currentTimeLabel.textContent = utils.formatTime(note.startTime); } + protected dispatchError(error?: unknown) { + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error_event + // This event is not cancelable and does not bubble. + this.dispatchEvent(new CustomEvent('error', { + detail: error, + cancelable: false, + bubbles: false + })); + } + protected handleStop(finished = false) { if (finished) { if (this.loop) { From f07f8a49883954ac1197e242dae678c38daed760 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 11:20:56 +0900 Subject: [PATCH 07/23] feat: add error property --- src/player.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/player.ts b/src/player.ts index f946993..422ccce 100644 --- a/src/player.ts +++ b/src/player.ts @@ -67,6 +67,7 @@ export class PlayerElement extends HTMLElement { protected ns: INoteSequence = null; protected _playing = false; protected seeking = false; + protected _lastError: any = null; // TODO: should we follow MediaError interface? static get observedAttributes() { return ['sound-font', 'src', 'visualizer']; } @@ -318,6 +319,7 @@ export class PlayerElement extends HTMLElement { protected dispatchError(error?: unknown) { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error_event // This event is not cancelable and does not bubble. + this._lastError = error; // TODO: implement MediaError interface this.dispatchEvent(new CustomEvent('error', { detail: error, cancelable: false, @@ -409,6 +411,10 @@ export class PlayerElement extends HTMLElement { this.initPlayer(); } + get error(): MediaError | null { + return this._lastError; + } + /** * @attr sound-font */ From db02adc0f9f5cc4adfe7567d7a09d34cd43bcf3a Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 11:32:09 +0900 Subject: [PATCH 08/23] feat: add 'loadeddata', 'loadstart', 'canplay' event timing in `initPlayerNow` --- src/player.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/player.ts b/src/player.ts index 422ccce..3059cc8 100644 --- a/src/player.ts +++ b/src/player.ts @@ -162,6 +162,7 @@ export class PlayerElement extends HTMLElement { let ns: INoteSequence = null; if (initNs) { if (this.src) { + this.dispatchEvent(new CustomEvent('loadstart')); this.ns = null; this.ns = await mm.urlToNoteSequence(this.src); } @@ -205,7 +206,10 @@ export class PlayerElement extends HTMLElement { } this.setLoaded(); - this.dispatchEvent(new CustomEvent('load')); + this.dispatchEvent(new CustomEvent('loadeddata')); + if (this.src) { + this.dispatchEvent(new CustomEvent('canplay')); + } } catch (error) { this.setError(String(error)); this.dispatchError(error); From ed10c4a59a01c3b27694a04f0245f399b3f4c16a Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 11:40:29 +0900 Subject: [PATCH 09/23] feat: add 'play', 'playing', 'ended' event timing --- src/player.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/player.ts b/src/player.ts index 3059cc8..fae8750 100644 --- a/src/player.ts +++ b/src/player.ts @@ -222,7 +222,13 @@ export class PlayerElement extends HTMLElement { } async play() { - await this._start(); + const promise = this._start(); + // `play` event fired when + // the paused prop is changed from true to false, + // as a result of the play method, + // or the autoplay attribute. + this.dispatchEvent(new CustomEvent('play', { cancelable: false, bubbles: false })); + await promise; } protected async _start(looped = false) { @@ -256,21 +262,23 @@ export class PlayerElement extends HTMLElement { } const promise = this.player.start(this.ns, undefined, offset); - if (!looped) { - this.dispatchEvent(new CustomEvent('start')); - } else { + // fired after playback is first started, and whenever it is restarted + this.dispatchEvent(new CustomEvent('playing', { cancelable: false, bubbles: false })); + if (looped) { this.dispatchEvent(new CustomEvent('loop')); } await promise; this.handleStop(true); + this.dispatchEvent(new CustomEvent('ended', { cancelable: false, bubbles: false })); } catch (error) { this.handleStop(); this.dispatchError(error); throw error; } } else if (this.player.getPlayState() == 'paused') { - // This normally should not happen, since we pause playback only when seeking. this.player.resume(); + // fired after playback is first started, and whenever it is restarted + this.dispatchEvent(new CustomEvent('playing', { cancelable: false, bubbles: false })); } } From e18e415f4384223ccd7ef8c0756aa2349201ee2b Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 11:42:52 +0900 Subject: [PATCH 10/23] feat: add `load()` instance method (same with reload()) --- src/player.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/player.ts b/src/player.ts index fae8750..b6b4fee 100644 --- a/src/player.ts +++ b/src/player.ts @@ -217,10 +217,17 @@ export class PlayerElement extends HTMLElement { } } - reload() { + load() { + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/load + // resets the media element to its initial state and + // begins the process of selecting a media source and loading the media in preparation for playback to begin at the beginning. this.initPlayerNow(); } + reload() { + this.load(); // same behavior with load + } + async play() { const promise = this._start(); // `play` event fired when From 7d3a5cc9f96613b274b2657c28e5df363953cb2f Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 11:51:50 +0900 Subject: [PATCH 11/23] feat: add 'timeupdate' event in note callback --- src/player.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/player.ts b/src/player.ts index b6b4fee..93f9056 100644 --- a/src/player.ts +++ b/src/player.ts @@ -333,6 +333,7 @@ export class PlayerElement extends HTMLElement { } this.seekBar.value = String(note.startTime); this.currentTimeLabel.textContent = utils.formatTime(note.startTime); + this.dispatchEvent(new CustomEvent('timeupdate')); } protected dispatchError(error?: unknown) { From 186e5bc29ecd660c277c230081191fcd3a54d8ee Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 11:58:55 +0900 Subject: [PATCH 12/23] feat: support `autoplay` property --- src/player.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/player.ts b/src/player.ts index 93f9056..466a004 100644 --- a/src/player.ts +++ b/src/player.ts @@ -209,6 +209,9 @@ export class PlayerElement extends HTMLElement { this.dispatchEvent(new CustomEvent('loadeddata')); if (this.src) { this.dispatchEvent(new CustomEvent('canplay')); + if (this.autoplay) { + await this.play(); + } } } catch (error) { this.setError(String(error)); @@ -229,13 +232,13 @@ export class PlayerElement extends HTMLElement { } async play() { - const promise = this._start(); + this._start(); // not use `await` (because it waits for ended timing) + // `play` event fired when // the paused prop is changed from true to false, // as a result of the play method, // or the autoplay attribute. this.dispatchEvent(new CustomEvent('play', { cancelable: false, bubbles: false })); - await promise; } protected async _start(looped = false) { @@ -421,6 +424,10 @@ export class PlayerElement extends HTMLElement { this.initPlayer(); } + get autoplay(): boolean { + return this.hasAttribute('autoplay'); + } + get src() { return this.getAttribute('src'); } From 5499b8ee89bbb1eeaff390b70001b0adc7a11509 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 12:03:38 +0900 Subject: [PATCH 13/23] fix(autoplay): add setter for autoplay --- src/player.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/player.ts b/src/player.ts index 466a004..e585948 100644 --- a/src/player.ts +++ b/src/player.ts @@ -428,6 +428,10 @@ export class PlayerElement extends HTMLElement { return this.hasAttribute('autoplay'); } + set autoplay(value: boolean) { + this.setOrRemoveAttribute('autoplay', value ? '' : null); + } + get src() { return this.getAttribute('src'); } From 258364a17eac844af6c48cdfa5a0b8fee12ce614 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 12:08:47 +0900 Subject: [PATCH 14/23] refactor: do not specify 'cancellable', 'bubbles' (default value is false) --- src/player.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/player.ts b/src/player.ts index e585948..7670e6d 100644 --- a/src/player.ts +++ b/src/player.ts @@ -238,7 +238,7 @@ export class PlayerElement extends HTMLElement { // the paused prop is changed from true to false, // as a result of the play method, // or the autoplay attribute. - this.dispatchEvent(new CustomEvent('play', { cancelable: false, bubbles: false })); + this.dispatchEvent(new CustomEvent('play')); } protected async _start(looped = false) { @@ -273,13 +273,13 @@ export class PlayerElement extends HTMLElement { const promise = this.player.start(this.ns, undefined, offset); // fired after playback is first started, and whenever it is restarted - this.dispatchEvent(new CustomEvent('playing', { cancelable: false, bubbles: false })); + this.dispatchEvent(new CustomEvent('playing')); if (looped) { this.dispatchEvent(new CustomEvent('loop')); } await promise; this.handleStop(true); - this.dispatchEvent(new CustomEvent('ended', { cancelable: false, bubbles: false })); + this.dispatchEvent(new CustomEvent('ended')); } catch (error) { this.handleStop(); this.dispatchError(error); @@ -288,7 +288,7 @@ export class PlayerElement extends HTMLElement { } else if (this.player.getPlayState() == 'paused') { this.player.resume(); // fired after playback is first started, and whenever it is restarted - this.dispatchEvent(new CustomEvent('playing', { cancelable: false, bubbles: false })); + this.dispatchEvent(new CustomEvent('playing')); } } @@ -341,13 +341,8 @@ export class PlayerElement extends HTMLElement { protected dispatchError(error?: unknown) { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error_event - // This event is not cancelable and does not bubble. this._lastError = error; // TODO: implement MediaError interface - this.dispatchEvent(new CustomEvent('error', { - detail: error, - cancelable: false, - bubbles: false - })); + this.dispatchEvent(new CustomEvent('error')); } protected handleStop(finished = false) { From 5125ad0d2803fa699784d6b77fbc71707e08ffc4 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 12:22:16 +0900 Subject: [PATCH 15/23] feat: add fastSeek (same behavior with currentTime setter) --- src/player.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/player.ts b/src/player.ts index 7670e6d..fc5ffb9 100644 --- a/src/player.ts +++ b/src/player.ts @@ -496,6 +496,10 @@ export class PlayerElement extends HTMLElement { ) ? 'probably' : ''; } + fastSeek(seconds: number) { + this.currentTime = seconds; + } + protected setOrRemoveAttribute(name: string, value: string) { if (value == null) { this.removeAttribute(name); From 499b1e1211edb6f4c2e81ab00c536fc34c806c15 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 12:23:58 +0900 Subject: [PATCH 16/23] feat: add 'seeking', 'seeked' event timing --- src/player.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/player.ts b/src/player.ts index fc5ffb9..1c3b4df 100644 --- a/src/player.ts +++ b/src/player.ts @@ -106,6 +106,7 @@ export class PlayerElement extends HTMLElement { this.seekBar.addEventListener('input', () => { // Pause playback while the user is manipulating the control this.seeking = true; + this.dispatchEvent(new CustomEvent('seeking')); if (this.player?.getPlayState() === 'started') { this.player.pause(); } @@ -120,6 +121,7 @@ export class PlayerElement extends HTMLElement { } } this.seeking = false; + this.dispatchEvent(new CustomEvent('seeked')); }); this.initPlayerNow(); From 570262da6cdd9d89f2fd0c9dc6c0b99fa41c8c45 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 12:31:21 +0900 Subject: [PATCH 17/23] feat: add durationchange event for totalTime (duration) --- src/player.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/player.ts b/src/player.ts index 1c3b4df..0f6e4ee 100644 --- a/src/player.ts +++ b/src/player.ts @@ -179,9 +179,11 @@ export class PlayerElement extends HTMLElement { if (ns) { this.seekBar.max = String(ns.totalTime); this.totalTimeLabel.textContent = utils.formatTime(ns.totalTime); + this.dispatchEvent(new CustomEvent('durationchange')); } else { this.seekBar.max = '0'; this.totalTimeLabel.textContent = utils.formatTime(0); + this.dispatchEvent(new CustomEvent('durationchange')); return; } From e036a3b22396b004fe13123a47ae6f023f424fc0 Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 12:41:30 +0900 Subject: [PATCH 18/23] feat: add controls property input --- src/player.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/player.ts b/src/player.ts index 0f6e4ee..ab6b8a4 100644 --- a/src/player.ts +++ b/src/player.ts @@ -77,6 +77,7 @@ export class PlayerElement extends HTMLElement { this.attachShadow({mode: 'open'}); this.shadowRoot.appendChild(controlsTemplate.content.cloneNode(true)); + // TODO: hiding buttons when controls enabled this.controlPanel = this.shadowRoot.querySelector('.controls'); this.playButton = this.controlPanel.querySelector('.play'); this.currentTimeLabel = this.controlPanel.querySelector('.current-time'); @@ -456,6 +457,14 @@ export class PlayerElement extends HTMLElement { this.setOrRemoveAttribute('sound-font', value); } + get controls() { + return this.hasAttribute('controls'); + } + + set controls(value: boolean) { + this.setOrRemoveAttribute('controls', value ? '' : null); + } + /** * @attr loop */ From ecfc4d1fb98955abadbff5908e290589f98d529d Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 12:41:39 +0900 Subject: [PATCH 19/23] feat: add abort event timing --- src/player.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/player.ts b/src/player.ts index ab6b8a4..bb82da5 100644 --- a/src/player.ts +++ b/src/player.ts @@ -167,7 +167,12 @@ export class PlayerElement extends HTMLElement { if (this.src) { this.dispatchEvent(new CustomEvent('loadstart')); this.ns = null; - this.ns = await mm.urlToNoteSequence(this.src); + try { + this.ns = await mm.urlToNoteSequence(this.src); + } catch (e) { + this.dispatchEvent(new CustomEvent('abort')); + throw e; + } } this.currentTime = 0; if (!this.ns) { From fd6e1f63379f575a23f60f5488dfd897ddb6ed9c Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 14:56:41 +0900 Subject: [PATCH 20/23] refactor(canPlayType): use String.includes to check type value --- src/player.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/player.ts b/src/player.ts index bb82da5..e492e01 100644 --- a/src/player.ts +++ b/src/player.ts @@ -505,12 +505,13 @@ export class PlayerElement extends HTMLElement { return this.player?.getPlayState() === 'paused'; } + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType canPlayType(type: unknown): string { - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType - // return type: '' | 'maybe' | 'probably'; + // return: '' | 'maybe' | 'probably'; // TODO: consider 'maybe' return case in some cases. return typeof type === 'string' && ( - type === 'audio/midi' || type === 'audio/x-midi' + // type: https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter + type.includes('audio/midi') || type.includes('audio/x-midi') ) ? 'probably' : ''; } From f285c9d7d309897a564bb013c38ca77729dc671e Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 15:00:24 +0900 Subject: [PATCH 21/23] feat: add ended property & correct behavior for 'ended' event (consider loop option enabled case) --- src/player.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/player.ts b/src/player.ts index e492e01..2e790d5 100644 --- a/src/player.ts +++ b/src/player.ts @@ -65,6 +65,7 @@ export class PlayerElement extends HTMLElement { protected visualizerListeners = new Map(); protected ns: INoteSequence = null; + protected _ended = false; protected _playing = false; protected seeking = false; protected _lastError: any = null; // TODO: should we follow MediaError interface? @@ -281,6 +282,7 @@ export class PlayerElement extends HTMLElement { } } + this._ended = false; const promise = this.player.start(this.ns, undefined, offset); // fired after playback is first started, and whenever it is restarted this.dispatchEvent(new CustomEvent('playing')); @@ -288,14 +290,16 @@ export class PlayerElement extends HTMLElement { this.dispatchEvent(new CustomEvent('loop')); } await promise; - this.handleStop(true); this.dispatchEvent(new CustomEvent('ended')); + this._ended = true; + this.handleStop(true); // call after dispatch 'ended' event (it will trigger loop restart) } catch (error) { this.handleStop(); this.dispatchError(error); throw error; } } else if (this.player.getPlayState() == 'paused') { + this._ended = false; this.player.resume(); // fired after playback is first started, and whenever it is restarted this.dispatchEvent(new CustomEvent('playing')); @@ -501,6 +505,10 @@ export class PlayerElement extends HTMLElement { return this._playing; } + get ended() { + return this._ended; + } + get paused() { return this.player?.getPlayState() === 'paused'; } From 0d2bb91394eee420796fc5bd80efc9c72d4d11cf Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 15:04:04 +0900 Subject: [PATCH 22/23] refactor(paused): return false when !playerState || stopped case --- src/player.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/player.ts b/src/player.ts index 2e790d5..16cdd64 100644 --- a/src/player.ts +++ b/src/player.ts @@ -510,7 +510,8 @@ export class PlayerElement extends HTMLElement { } get paused() { - return this.player?.getPlayState() === 'paused'; + const playerState = this.player?.getPlayState(); + return !playerState || playerState === 'paused' || playerState === 'stopped'; } // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType From b5075958b8e64bae2b69c78062b253109253008b Mon Sep 17 00:00:00 2001 From: Heo Sangmin Date: Thu, 1 Jun 2023 17:59:00 +0900 Subject: [PATCH 23/23] feat: add more event listeners in index.html (for test) --- index.html | 6 ++++-- src/player.ts | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index ca1756e..8cc59fe 100644 --- a/index.html +++ b/index.html @@ -31,8 +31,10 @@

HTML MIDI Player window.addEventListener('DOMContentLoaded', function() { for (const player of document.querySelectorAll('midi-player')) { - for (const event of ['load', 'start', 'stop', 'loop']) { - player.addEventListener(event, console.log); + for (const event of ['loadstart', 'loadeddata', 'play', 'pause', 'stop', 'durationchange', 'timeupdate', 'ended', 'loop', 'error']) { + player.addEventListener(event, (e) => { + console.log(event, e); + }); } } diff --git a/src/player.ts b/src/player.ts index 16cdd64..80749ec 100644 --- a/src/player.ts +++ b/src/player.ts @@ -350,6 +350,8 @@ export class PlayerElement extends HTMLElement { } this.seekBar.value = String(note.startTime); this.currentTimeLabel.textContent = utils.formatTime(note.startTime); + + // TODO: more efficient way? - Currently, multiple track midi can cause too much event triggers. this.dispatchEvent(new CustomEvent('timeupdate')); } @@ -372,7 +374,9 @@ export class PlayerElement extends HTMLElement { this.controlPanel.classList.add('stopped'); if (this._playing) { this._playing = false; - this.dispatchEvent(new CustomEvent('pause', {detail: {finished}})); + if (!this._ended) { + this.dispatchEvent(new CustomEvent('pause', {detail: {finished}})); + } } }