refactor bindings to directly use element

This commit is contained in:
Ollie Taylor
2023-02-22 23:41:29 +00:00
parent 4137cda798
commit f4e2e8d628
6 changed files with 235 additions and 223 deletions
+66 -63
View File
@@ -1,80 +1,83 @@
<script lang="ts"> <script lang="ts">
import { usePlayer, type PlayerElement } from '$lib/components/audio/helper';
import { import {
_currentTime, __internal_audio_current_time,
_duration, __internal_audio_element,
_muted, __internal_audio_metadata,
_paused, __internal_audio_src,
_playbackRate, } from '$lib/components/audio/store';
_seek, import type { PlayerElement } from '$lib/components/audio/types';
_src,
_volume,
} from '$lib/components/audio/state';
export let autoplay: boolean;
let audioElement: PlayerElement;
let element: PlayerElement;
let currentTime = 0; let currentTime = 0;
let paused = true;
let playbackRate = 1;
let muted = false; let muted = false;
let volume = 0;
let duration = 0;
$: usePlayer(audioElement, 'playbackRate', playbackRate); $: src = $__internal_audio_src;
$: __internal_audio_element.set(element);
function handleSeek(value: number) { $: __internal_audio_current_time.set(currentTime);
if (value < 0) return; $: __internal_audio_metadata.update((x) => ({ ...x, muted }));
if (value > duration) return;
currentTime = value;
}
$: _currentTime.set(currentTime);
$: _duration.set(duration);
$: _paused.set(paused);
$: handleSeek($_seek);
$: volume = $_volume;
$: muted = $_muted;
$: playbackRate = $_playbackRate;
$: paused = $_paused;
</script> </script>
{#key $_src} {#if src}
{#if $_src}
<audio <audio
bind:this={audioElement} bind:this={element}
src={$_src} {src}
{autoplay} autoplay={false}
bind:currentTime bind:currentTime
bind:duration
bind:paused
bind:muted bind:muted
bind:volume
preload="metadata" preload="metadata"
controls={false} controls={false}
on:seeked={() => _seek.set(-1)} on:canplay={(e) => false && console.log('canplay', e)}
on:timeupdate={() => console.log('progress', { currentTime })} on:canplaythrough={(e) => false && console.log('canplaythrough', e)}
on:ratechange={() => console.log('rateChange', { playbackRate })} on:timeupdate={(e) => false && console.log('progress', e)}
on:ended={() => console.log('finished')} on:loadedmetadata={(e) => false && console.log('loadedmetadata', e)}
on:playing={() => console.log('playing')} on:suspend={(e) => false && console.log('suspend', e)}
on:pause={() => console.log('pause')} on:seeked={(e) => false && console.log('seeked', e)}
on:durationchange={() => console.log('durationchange')} on:play={(e) => false && console.log('play', e)}
on:loadeddata={() => console.log('loaded data')} on:waiting={(e) => console.log('waiting', e)}
on:loadedmetadata={() => console.log('loadedmetadata')}
on:loadstart={() => console.log('load start')}
on:seeking={() => console.log('seeking')}
on:suspend={() => console.log('suspend')}
on:volumechange={() => console.log('volumechange')}
on:play={() => console.log('play')}
on:canplay={() => console.log('canplay')}
on:canplaythrough={() => console.log('canplaythrough')}
on:waiting={() => console.log('waiting')}
on:stalled={(e) => console.log('stalled', e)} on:stalled={(e) => console.log('stalled', e)}
on:emptied={(e) => console.log('emptied', e)}
on:load={(e) => console.log('load', e)} on:load={(e) => console.log('load', e)}
on:emptied={(e) => {
// TODO: reset player state
// console.log('emptied', e);
}}
aria-activedescendant="audio-progress"
aria-atomic="true"
aria-busy="false"
aria-controls="audio-progress"
on:loadeddata={() => {
__internal_audio_metadata.update((m) => ({ ...m, loading: false }));
}}
on:loadstart={() => {
__internal_audio_metadata.update((m) => ({ ...m, loading: true }));
}}
on:ratechange={(e) => {
// @ts-expect-error
const rate = e.target.playbackRate;
console.log('rateChange', rate);
if (typeof rate != 'number' || isNaN(rate)) return;
__internal_audio_metadata.update((m) => ({ ...m, playbackRate: rate }));
}}
on:playing={(e) => {
// @ts-expect-error
const is_paused = e.target.paused;
console.log('playing paused:', is_paused);
if (typeof is_paused != 'boolean') return;
__internal_audio_metadata.update((m) => ({ ...m, paused: is_paused }));
}}
on:pause={(e) => {
// @ts-expect-error
const is_paused = e.target.paused;
console.log('pause paused:', is_paused);
if (typeof is_paused != 'boolean') return;
__internal_audio_metadata.update((m) => ({ ...m, paused: is_paused }));
}}
on:durationchange={(e) => {
// @ts-expect-error
const dur = e.target?.duration;
console.log('durationchange', dur);
if (typeof dur != 'number' || isNaN(dur)) return;
__internal_audio_metadata.update((m) => ({ ...m, duration: dur }));
}}
/> />
<!-- <AudioA11yProgress {currentTime} {paused} {duration} /> --> <!-- <AudioA11yProgress {currentTime} {paused} {duration} /> -->
{/if}\ {/if}
{/key}
-10
View File
@@ -1,10 +0,0 @@
export type PlayerElement = HTMLAudioElement | undefined;
export function usePlayer<K extends keyof HTMLAudioElement>(
el: PlayerElement,
key: K,
value: HTMLAudioElement[K],
) {
if (!el) return;
el[key] = value;
}
-115
View File
@@ -1,115 +0,0 @@
import clamp from 'just-clamp';
import { derived, writable } from 'svelte/store';
export const _src = writable<string | undefined>(undefined);
export const _duration = writable<number>(0);
export const _currentTime = writable<number>(0);
export const _muted = writable<boolean>(false);
export const _paused = writable<boolean>(true);
export const _playbackRate = writable<number>(1);
export const _seek = writable<number>(0);
export const _volume = writable<number>(1);
const state = derived(
[_src, _currentTime, _duration, _muted, _paused, _playbackRate, _volume, _seek],
([$src, $currentTime, $duration, $muted, $paused, $playbackRate, $volume, $seek]) => {
const currentTime = $currentTime > $duration ? $duration : $currentTime;
return {
src: $src,
duration: $duration,
currentTime,
muted: $muted,
paused: $paused,
playbackRate: $playbackRate,
seek: $seek,
volume: $volume,
};
},
);
type AudioState = Parameters<Parameters<(typeof state)['subscribe']>[0]>[0];
type PlayerState = Omit<AudioState, 'src' | 'duration'>;
const default_player_state: PlayerState = {
currentTime: 0,
muted: false,
paused: true,
playbackRate: 1,
seek: 0,
volume: 1,
};
export function resetAudio() {
_currentTime.set(0);
_duration.set(0);
_muted.set(false);
_paused.set(true);
_playbackRate.set(1);
_volume.set(1);
}
function set_player_state(opts?: Partial<PlayerState>) {
_currentTime.set(opts?.currentTime ?? default_player_state.currentTime);
_muted.set(opts?.muted ?? default_player_state.muted);
_paused.set(opts?.paused ?? default_player_state.paused);
_playbackRate.set(opts?.playbackRate ?? default_player_state.playbackRate);
_volume.set(opts?.volume ?? default_player_state.volume);
_seek.set(opts?.seek ?? default_player_state.seek);
}
function unload() {
set_player_state();
_duration.set(0);
_src.set(undefined);
}
function load(src: string, opts?: Partial<PlayerState>) {
set_player_state(opts);
_src.set(src);
}
function play() {
// Incase of a missmatch between the html and js, we first pause and then play
_paused.set(true);
_paused.set(false);
}
function pause() {
// Incase of a missmatch between the html and js, we first play and then pause
_paused.set(false);
_paused.set(true);
}
export const audio = {
subscribe: state.subscribe,
currentTime: { subscribe: _currentTime.subscribe },
duration: { subscribe: _duration.subscribe },
muted: { subscribe: _muted.subscribe },
paused: { subscribe: _paused.subscribe },
playbackRate: { subscribe: _playbackRate.subscribe },
volume: { subscribe: _volume.subscribe },
load,
unload,
seek: (value: number) => {
console.log('seek', value);
// let duration = 0
// _duration.subscribe(d => duration = d)
return _duration.subscribe((dur) => {
_seek.set(clamp(0, dur, value));
});
},
toggle_mute: () => _muted.update((previous) => !previous),
unmute: () => _muted.set(false),
mute: () => _muted.set(true),
toggle_pause: () => _paused.update((previous) => !previous),
play,
pause: () => _paused.set(true),
setVolume: (value: number) => _volume.set(clamp(0, 1, value)),
setPlaybackRate: (value: number) => _playbackRate.set(clamp(0, 10, value)),
};
+131
View File
@@ -0,0 +1,131 @@
import { dev } from '$app/environment';
import type { PlayerElement, PlayerMetadata } from '$lib/components/audio/types';
import clamp from 'just-clamp';
import { writable } from 'svelte/store';
function log(...val: (string | number | boolean)[]) {
if (!dev) return;
console.info('🎶 ', ...val);
}
function warn(...val: (string | number | boolean)[]) {
if (!dev) return;
console.warn('🎶 ', ...val);
}
export const __internal_audio_current_time = writable<number>(0);
export const __internal_audio_src = writable<string | null>(null);
export const __internal_audio_element = writable<PlayerElement>();
export const __internal_audio_metadata = writable<PlayerMetadata>({
duration: 0,
muted: false,
paused: true,
playbackRate: 1,
loading: false,
});
type HandleType = 'toggle' | 'set';
function play(type: HandleType = 'set') {
log('play: ', type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.paused ? el.play() : el.pause();
} else {
el.play();
}
})();
}
function pause(type: HandleType = 'set') {
log('pause: ', type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.paused ? el.play() : el.pause();
} else {
el.pause();
}
})();
}
function mute(type: HandleType = 'set') {
log('mute: ', type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.muted = !el.muted;
} else {
el.muted = true;
}
})();
}
function unmute(type: HandleType = 'set') {
log('unmute: ', type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.muted = !el.muted;
} else {
el.muted = false;
}
})();
}
function load(src: string) {
log('load: ', src);
__internal_audio_src.set(src);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
el.volume = 1;
el.pause();
})();
}
function unload() {
log('unload: ');
__internal_audio_src.set(null);
}
function setPlaybackRate(rate: number) {
log('setPlaybackRate: ', rate);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
el.playbackRate = clamp(0, 10, rate);
})();
}
function seek(seconds: number, from: 'from-start' | 'from-end' = 'from-start') {
log('seek: ', seconds, from);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (from === 'from-end') {
el.currentTime = clamp(0, el.duration, el.duration - seconds);
} else {
el.currentTime = clamp(0, el.duration, seconds);
}
})();
}
function skip(seconds: number, type: 'forward' | 'backward' = 'forward') {
log('skip: ', seconds, type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'backward') {
el.currentTime = clamp(0, el.duration, el.currentTime - seconds);
} else {
el.currentTime = clamp(0, el.duration, el.currentTime + seconds);
}
})();
}
export const audio = {
subscribe: __internal_audio_metadata.subscribe,
play,
pause,
mute,
unmute,
load,
unload,
setPlaybackRate,
seek,
skip,
};
+9
View File
@@ -0,0 +1,9 @@
export type PlayerElement = HTMLAudioElement | undefined;
export type PlayerMetadata = {
duration: number;
muted: boolean;
paused: boolean;
playbackRate: number;
loading: boolean;
};
+20 -26
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Audio from '$lib/components/audio/audio.svelte'; import Audio from '$lib/components/audio/audio.svelte';
import { audio, _currentTime, _duration, _paused } from '$lib/components/audio/state'; import { audio } from '$lib/components/audio/store';
const sources = { const sources = {
syntax: '/example-syntax.mp3', syntax: '/example-syntax.mp3',
@@ -8,52 +8,46 @@
} as const; } as const;
</script> </script>
<pre> <pre>{JSON.stringify($audio, null, 3)}</pre>
{JSON.stringify($audio, null, 3)}
</pre>
<h1>Demo</h1> <h1>Demo</h1>
<Audio on:progress={(e) => console.log(e.detail)} autoplay={false} /> <Audio on:progress={(e) => console.log(e.detail)} />
<progress <!-- <progress
style="width: 960px; max-width:100%" style="width: 960px; max-width:100%"
data-paused={$_paused ? 'true' : 'false'} data-paused={$_paused ? 'true' : 'false'}
max={$_duration} max={$_duration}
value={$_currentTime} value={$_currentTime}
/> /> -->
<h5>Load Audio</h5> <h5>Load Audio</h5>
<button type="button" on:click={() => audio.load(sources['syntax'], { paused: false })} <button type="button" on:click={() => audio.load(sources['syntax'])}>Syntax</button>
>Syntax</button <button type="button" on:click={() => audio.load(sources['knomii'])}>Knomii</button>
>
<button type="button" on:click={() => audio.load(sources['knomii'], { paused: false })}
>Knomii</button
>
<button type="button" on:click={() => audio.unload()}>None</button> <button type="button" on:click={() => audio.unload()}>None</button>
<h5>Custom audio controls</h5> <h5>Custom audio controls</h5>
<h6>Play / Pause Actions</h6>
<button type="button" on:click={() => audio.play()}>Play</button>
<button type="button" on:click={() => audio.pause()}>Pause</button>
<button type="button" on:click={() => audio.pause('toggle')}>Toggle</button>
<h6>Audio Actions</h6> <h6>Audio Actions</h6>
<button type="button" on:click={audio.play}>Play</button> <button type="button" on:click={() => audio.mute()}>Mute</button>
<button type="button" on:click={audio.pause}>Pause</button> <button type="button" on:click={() => audio.unmute()}>Unmute</button>
<button type="button" on:click={audio.toggle_pause}>Toggle</button> <button type="button" on:click={() => audio.mute('toggle')}>Toggle</button>
<h6>Seeking</h6> <h6>Seeking</h6>
<button type="button" on:click={() => audio.seek(30)}>Go to 30s</button> <button type="button" on:click={() => audio.seek(30)}>Go to 30s from start </button>
<!-- <button type="button" on:click={() => (currentTime = currentTime + 5)}>Skip 5s</button> <button type="button" on:click={() => audio.seek(30, 'from-end')}>Go to 30s from end</button>
<button type="button" on:click={() => (currentTime = currentTime - 5)}>Rewind 5s</button> <button type="button" on:click={() => audio.skip(10, 'forward')}>Skip 10s</button>
<button type="button" on:click={() => (currentTime = duration - 30)}>Go to 30s before end</button> --> <button type="button" on:click={() => audio.skip(10, 'backward')}>Rewind 10s</button>
<h6>Playback Rate</h6> <h6>Playback Rate</h6>
{#each [0.5, 1, 2, 3] as rate} {#each [0.5, 1, 2, 3] as rate}
<button type="button" on:click={() => audio.setPlaybackRate(rate)}>{rate}x</button> <button type="button" on:click={() => audio.setPlaybackRate(rate)}>{rate}x</button>
{/each} {/each}
<h6>Volume</h6>
{#each [0, 0.25, 0.5, 0.75, 1] as v}
<button type="button" on:click={() => audio.setVolume(v)}>{v * 100}%</button>
{/each}