svelte
Version:
Cybernetically enhanced web apps
218 lines (183 loc) • 5.78 kB
JavaScript
import { hydrating } from '../../hydration.js';
import { render_effect, effect, teardown } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
/** @param {TimeRanges} ranges */
function time_ranges_to_array(ranges) {
var array = [];
for (var i = 0; i < ranges.length; i += 1) {
array.push({ start: ranges.start(i), end: ranges.end(i) });
}
return array;
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => number | undefined} get
* @param {(value: number) => void} set
* @returns {void}
*/
export function bind_current_time(media, get, set = get) {
/** @type {number} */
var raf_id;
/** @type {number} */
var value;
// Ideally, listening to timeupdate would be enough, but it fires too infrequently for the currentTime
// binding, which is why we use a raf loop, too. We additionally still listen to timeupdate because
// the user could be scrubbing through the video using the native controls when the media is paused.
var callback = () => {
cancelAnimationFrame(raf_id);
if (!media.paused) {
raf_id = requestAnimationFrame(callback);
}
var next_value = media.currentTime;
if (value !== next_value) {
set((value = next_value));
}
};
raf_id = requestAnimationFrame(callback);
media.addEventListener('timeupdate', callback);
render_effect(() => {
var next_value = Number(get());
if (value !== next_value && !isNaN(/** @type {any} */ (next_value))) {
media.currentTime = value = next_value;
}
});
teardown(() => {
cancelAnimationFrame(raf_id);
media.removeEventListener('timeupdate', callback);
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(array: Array<{ start: number; end: number }>) => void} set
*/
export function bind_buffered(media, set) {
listen(media, ['loadedmetadata', 'progress'], () => set(time_ranges_to_array(media.buffered)));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(array: Array<{ start: number; end: number }>) => void} set
*/
export function bind_seekable(media, set) {
listen(media, ['loadedmetadata'], () => set(time_ranges_to_array(media.seekable)));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(array: Array<{ start: number; end: number }>) => void} set
*/
export function bind_played(media, set) {
listen(media, ['timeupdate'], () => set(time_ranges_to_array(media.played)));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(seeking: boolean) => void} set
*/
export function bind_seeking(media, set) {
listen(media, ['seeking', 'seeked'], () => set(media.seeking));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(seeking: boolean) => void} set
*/
export function bind_ended(media, set) {
listen(media, ['timeupdate', 'ended'], () => set(media.ended));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(ready_state: number) => void} set
*/
export function bind_ready_state(media, set) {
listen(
media,
['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'emptied'],
() => set(media.readyState)
);
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => number | undefined} get
* @param {(playback_rate: number) => void} set
*/
export function bind_playback_rate(media, get, set = get) {
// Needs to happen after element is inserted into the dom (which is guaranteed by using effect),
// else playback will be set back to 1 by the browser
effect(() => {
var value = Number(get());
if (value !== media.playbackRate && !isNaN(value)) {
media.playbackRate = value;
}
});
// Start listening to ratechange events after the element is inserted into the dom,
// else playback will be set to 1 by the browser
effect(() => {
listen(media, ['ratechange'], () => {
set(media.playbackRate);
});
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => boolean | undefined} get
* @param {(paused: boolean) => void} set
*/
export function bind_paused(media, get, set = get) {
var paused = get();
var update = () => {
if (paused !== media.paused) {
set((paused = media.paused));
}
};
// If someone switches the src while media is playing, the player will pause.
// Listen to the canplay event to get notified of this situation.
listen(media, ['play', 'pause', 'canplay'], update, paused == null);
// Needs to be an effect to ensure media element is mounted: else, if paused is `false` (i.e. should play right away)
// a "The play() request was interrupted by a new load request" error would be thrown because the resource isn't loaded yet.
effect(() => {
if ((paused = !!get()) !== media.paused) {
if (paused) {
media.pause();
} else {
media.play().catch(() => {
set((paused = true));
});
}
}
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => number | undefined} get
* @param {(volume: number) => void} set
*/
export function bind_volume(media, get, set = get) {
var callback = () => {
set(media.volume);
};
if (get() == null) {
callback();
}
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = Number(get());
if (value !== media.volume && !isNaN(value)) {
media.volume = value;
}
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => boolean | undefined} get
* @param {(muted: boolean) => void} set
*/
export function bind_muted(media, get, set = get) {
var callback = () => {
set(media.muted);
};
if (get() == null) {
callback();
}
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = !!get();
if (media.muted !== value) media.muted = value;
});
}