@stemplayer-js/stemplayer-js
Version:
A streaming, low latency Stem Player Web-Component
357 lines (318 loc) • 9.66 kB
JavaScript
import { html, css } from 'lit';
import { StemPlayerBaseRow } from './StemPlayerBaseRow.js';
import formatSeconds from './lib/format-seconds.js';
import debounce from './lib/debounce.js';
/**
* A component to render a single stem
*
* @cssprop [--stemplayer-js-controls-color]
* @cssprop [--stemplayer-js-controls-background-color]
*/
export class FcStemPlayerControls extends StemPlayerBaseRow {
static get styles() {
return [
...super.styles,
css`
:host {
--fc-player-button-color: var(
--stemplayer-js-controls-color,
var(--stemplayer-js-color, white)
);
display: block;
color: var(--stemplayer-js-controls-color, inherit);
background-color: var(
--stemplayer-js-controls-background-color,
transparent
);
--stemplayer-js-row-controls-background-color: transparent;
--stemplayer-js-row-end-background-color: transparent;
}
`,
];
}
static get properties() {
return {
/**
* The current time of playback
*/
currentTime: { type: Number, hasChanged: () => false },
/**
* The peaks data that are to be used for displaying the waveform
*/
peaks: { type: Object },
/**
* The playing state
*/
isPlaying: { type: Boolean },
/**
* Whether the loop is toggled on or off
*/
loop: { type: Boolean },
/**
* The controls that are enables
*/
controls: {
type: String,
converter: {
fromAttribute: value => {
if (typeof value === 'string') return value.split(' ');
return value;
},
},
},
};
}
/**
* @private
*/
#debouncedHandleSeek;
set currentTime(val) {
this._currentTime = val;
if (this.shadowRoot) {
this.shadowRoot.querySelectorAll('.js-currentTime').forEach(el => {
el.textContent = formatSeconds(val || 0);
});
}
}
get currentTime() {
return this._currentTime;
}
updateProgress(val) {
super.updateProgress(val);
if (this.shadowRoot) {
this.shadowRoot.querySelectorAll('fc-range.js-progress').forEach(el => {
el.value = val * 100;
});
}
}
constructor() {
super();
this.#debouncedHandleSeek = debounce(this.#handleSeek, 100);
this.controls = ['playpause', 'loop', 'progress', 'duration', 'time'];
}
renderLargeScreen() {
return html`<div class="stem-row dFlex h100">
<div class="wControls stickLeft bgControls z9999 dFlex h100 flexNoShrink">
${this.#renderControl('playpause', true)} ${this.#renderControl('loop')}
${this.#renderControl('label', this.label) ||
html`<div class="flex1"></div>`}
${this.#renderControl(
'time',
this.#renderControl('waveform') || this.#renderControl('progress'),
)}
</div>
<div class="flex1 h100">
${this.#renderControl('waveform') || this.#renderControl('progress')}
</div>
<div class="wEnd stickRight bgEnd z99 dFlex h100">
${this.#renderControl(
'duration',
this.#renderControl('waveform') || this.#renderControl('progress'),
)}
${!this.#renderControl('waveform')
? html`${this.#renderControl('zoom')}
${this.#renderControl('download')}${this.#renderControl('collapse')}`
: ''}
</div>
</div>`;
}
renderSmallScreen() {
return html`<div class="stem-row dFlex h100 overflowHidden">
${this.#renderControl('playpause', true)} ${this.#renderControl('loop')}
${this.displayMode !== 'xs'
? html`<div
class="flex1 truncate hideXs px4 pr5 textCenter flexNoShrink"
>
${this.label}
</div>`
: ''}
<div
class="w2 truncate textCenter flexNoShrink z99 op75 top right textXs js-currentTime"
role="timer"
aria-label="Current time"
aria-live="off"
>
0:00
</div>
<fc-range
label="progress"
class="focusBgBrand px1 flex1 flexNoShrink js-progress"
.value=${0}
.step=${0.001}
=${this.#handleSeeking}
=${this.#debouncedHandleSeek}
></fc-range>
<div class="w2 op75 textCenter textXs">
<span class="p2" role="timer" aria-label="Duration"
>${formatSeconds(this.duration)}</span
>
</div>
</div>`;
}
/**
* @private
*/
#onPlayClick() {
this.dispatchEvent(new Event('controls:play', { bubbles: true }));
}
/**
* @private
*/
#onPauseClick() {
this.dispatchEvent(new Event('controls:pause', { bubbles: true }));
}
/**
* @private
*/
#onDownloadClick() {
this.dispatchEvent(new Event('controls:download', { bubbles: true }));
}
/**
* @private
*/
#onToggleCollapseClick() {
this.dispatchEvent(new Event('controls:collapse', { bubbles: true }));
}
/**
* @private
*/
#handleSeeking() {
this.dispatchEvent(new CustomEvent('controls:seeking', { bubbles: true }));
}
/**
* @private
*/
#handleSeek(e) {
this.dispatchEvent(
new CustomEvent('controls:seek', {
detail: e.detail / 100,
bubbles: true,
}),
);
}
#toggleLoop(e) {
this.dispatchEvent(new CustomEvent('controls:loop', { bubbles: true }));
e.target.blur();
}
#onZoominClick() {
this.dispatchEvent(new Event('controls:zoom:in', { bubbles: true }));
}
#onZoomoutClick() {
this.dispatchEvent(new Event('controls:zoom:out', { bubbles: true }));
}
isControlEnabled(value) {
return this.controls.indexOf(value) !== -1;
}
#renderControl(value, mandatory) {
const activeControls = this.controls.map(control => control.split(':')[0]);
if (!mandatory && activeControls.indexOf(value) === -1) return '';
const controls = {};
this.controls.forEach(control => {
const [name, qualifier] = control.split(':');
controls[name] = {
disabled: qualifier === 'disabled',
toggled: qualifier === 'toggled',
};
});
if (value === 'playpause')
return html`<fc-player-button
class="w2 flexNoShrink"
.disabled=${!this.duration}
=${this.isPlaying ? this.#onPauseClick : this.#onPlayClick}
.label=${this.isPlaying ? 'Pause' : 'Play'}
.type=${this.isPlaying ? 'pause' : 'play'}
></fc-player-button>`;
if (value === 'loop')
return html`<fc-player-button
class="w2 flexNoShrink ${this.loop ? '' : 'textMuted'}"
=${this.#toggleLoop}
.label=${this.loop ? 'Disable loop' : 'Enable Loop'}
.disabled=${controls.loop?.disabled}
type="loop"
></fc-player-button>`;
if (value === 'zoom')
return html`<fc-player-button
class="w2 flexNoShrink"
label="zoom in"
type="zoomin"
.disabled=${controls.zoom.disabled}
=${this.#onZoominClick}
></fc-player-button
><fc-player-button
class="w2 flexNoShrink"
label="zoom out"
type="zoomout"
.disabled=${controls.zoom.disabled}
=${this.#onZoomoutClick}
></fc-player-button>`;
if (value === 'progress')
return html`<fc-range
label="progress"
class="focusBgBrand px1 dBlock h100 js-progress"
.value=${0}
.step=${0.001}
=${this.#handleSeeking}
=${this.#debouncedHandleSeek}
></fc-range>`;
if (value === 'waveform') {
const styles = this.getComputedWaveformStyles();
return html`
<fc-waveform
class="dBlock h100 w100"
.peaks=${this.peaks}
.duration=${this.duration}
.progress=${this.currentPct || 0}
.progressColor=${styles.waveProgressColor}
.waveColor=${styles.waveColor}
.barWidth=${styles.barWidth}
.barGap=${styles.barGap}
.pixelRatio=${styles.devicePixelRatio}
></fc-waveform>
`;
}
if (value === 'label')
return html`<div
class="flex1 w100 truncate hideXs px4 pr5 textCenter flexNoShrink textSm"
label=${this.label}
>
${this.label}
</div>`;
if (value === 'duration')
return html`<div class="textCenter w2">
<span class="p2 textXs" role="timer" aria-label="Duration"
>${formatSeconds(this.duration)}</span
>
</div>`;
if (value === 'time')
return html`<div class="w2 textCenter flexNoShrink z99 top right">
<span
class="p2 textXs js-currentTime"
role="timer"
aria-label="Current time"
aria-live="off"
>0:00</span
>
</div>`;
if (value === 'download')
return html`<fc-player-button
class="w2 flexNoShrink"
label="download"
type="download"
.disabled=${controls.download.disabled}
=${this.#onDownloadClick}
></fc-player-button>`;
if (value === 'collapse')
return html`<fc-player-button
class="w2 flexNoShrink"
=${this.#onToggleCollapseClick}
label="toggle"
type="${controls.collapse.toggled ? 'unfoldmore' : 'unfoldless'}"
></fc-player-button>`;
return '';
}
set collapsed(v) {
const from = v ? 'collapse' : 'collapse:toggled';
const to = v ? 'collapse:toggled' : 'collapse';
this.controls = this.controls.join(' ').replace(from, to).split(' ');
}
}