UNPKG

svelte

Version:

Cybernetically enhanced web apps

218 lines (183 loc) • 5.78 kB
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; }); }