Files
ytplayer/frontend/audio-graph.js
2026-10-09 19:46:00 +08:00

70 lines
2.6 KiB
JavaScript

/* One lifetime owner for the player's realtime AudioContext and media sources. */
(function (root) {
'use strict';
function create(options = {}) {
const getConstructor = options.getConstructor || (() => root.AudioContext || root.webkitAudioContext);
let context = null;
const sources = new Map();
const routes = new Map();
const failed = new Map();
function getContext(contextOptions) {
if (context) return context;
const Constructor = getConstructor();
if (typeof Constructor !== 'function') throw new Error('Web Audio is unavailable');
context = contextOptions === undefined ? new Constructor() : new Constructor(contextOptions);
return context;
}
function source(element) {
if (!element) throw new TypeError('A media element is required');
if (sources.has(element)) return sources.get(element);
if (failed.has(element)) throw failed.get(element);
try {
const created = getContext().createMediaElementSource(element);
sources.set(element, created);
return created;
} catch (error) {
// MediaElementSource cannot be undone. Remember failure so a later
// route change can never attempt a second source for this element.
failed.set(element, error);
throw error;
}
}
function connect(element, destination) {
const mediaSource = source(element);
if (!destination) throw new TypeError('An audio destination is required');
if (routes.get(element) === destination) return mediaSource;
if (routes.has(element)) mediaSource.disconnect();
mediaSource.connect(destination);
routes.set(element, destination);
return mediaSource;
}
function routeAll(elements, destination) {
for (const element of elements || []) connect(element, destination);
}
function snapshot() {
return {
contextCreated: !!context,
sampleRate: context?.sampleRate || null,
sourceCount: sources.size,
failedCount: failed.size,
routedCount: routes.size,
};
}
return { context: getContext, source, connect, routeAll, snapshot };
}
const singleton = create();
const api = { create, context: (...args) => singleton.context(...args), source: (...args) => singleton.source(...args),
connect: (...args) => singleton.connect(...args), routeAll: (...args) => singleton.routeAll(...args),
snapshot: () => singleton.snapshot() };
if (typeof module !== 'undefined' && module.exports) module.exports = api;
else root.AudioGraph = api;
})(typeof window !== 'undefined' ? window : globalThis);