/*! * artplayer-plugin-dash-control.js v1.1.0 * Github: https://github.com/zhw2590582/ArtPlayer * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ const $audio = ''; function qualityAdapter(dash) { const modern = typeof dash.getRepresentationsByType === "function"; return { levels: () => modern ? dash.getRepresentationsByType("video") : dash.getBitrateInfoListFor("video"), current(levels) { if (modern) return dash.getCurrentRepresentationForType("video"); const quality = dash.getQualityFor("video"); return levels.find((level) => level.qualityIndex === quality); }, item(level, index, selected, automatic) { return modern ? { value: index, id: level.id, default: !automatic && selected != null && level.id === selected.id } : { value: level.qualityIndex, default: !automatic && selected != null && level.qualityIndex === selected.qualityIndex }; }, select(item, valid) { const automatic = item.value === "auto"; dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: automatic } } } }); if (!automatic && valid()) { if (modern) dash.setRepresentationForTypeById("video", item.id); else dash.setQualityFor("video", item.value); } } }; } function uniqueLabels(items) { const seen = /* @__PURE__ */ new Map(); return items.filter((item) => { if (item.html === void 0) return true; const first = seen.get(item.html); if (first) { if (item.default) Object.assign(first, item); return false; } seen.set(item.html, item); return true; }); } function qualityModel(dash, config, active) { const adapter = qualityAdapter(dash); const levels = adapter.levels(); if (!active() || !levels?.length) return null; const auto = config.auto || "Auto"; const getName = config.getName || ((level) => `${level.height}p`); const selected = adapter.current(levels); if (!active()) return null; const automatic = dash.getSettings().streaming.abr.autoSwitchBitrate.video; if (!active()) return null; const html = !automatic && selected ? getName(selected) : auto; if (!active()) return null; const items = []; for (const [index, level] of levels.entries()) { const label = getName(level); if (!active()) return null; items.push({ html: label, ...adapter.item(level, index, selected, automatic) }); } const selector = uniqueLabels(items).sort((left, right) => right.value - left.value); selector.push({ html: auto, value: "auto", default: automatic }); return { html, title: config.title || "Quality", selector, select: adapter.select }; } function selectedTrack(tracks, current) { if (tracks.includes(current)) return current; if (current.id == null && current.index == null) return void 0; const fields = ["id", "index", "lang"]; const matching = tracks.filter((track) => fields.every((key) => current[key] == null || track[key] === current[key])); return matching.length === 1 ? matching[0] : void 0; } function audioModel(dash, config, active) { const tracks = dash.getTracksFor("audio"); if (!active() || !tracks?.length) return null; const auto = config.auto || "Auto"; const getName = config.getName || ((track) => track.lang || track.id); const current = dash.getCurrentTrackFor("audio") || tracks[0]; if (!active()) return null; const html = current ? getName(current) : auto; if (!active()) return null; const selected = selectedTrack(tracks, current); const items = []; for (const track of tracks) { const label = getName(track); if (!active()) return null; items.push({ html: label, value: track, default: track === selected }); } const selector = uniqueLabels(items); return { html, title: config.title || "Audio", selector, select: (item) => dash.setCurrentTrack(item.value) }; } function createMenu(art, name, icon) { let current; const owned = /* @__PURE__ */ new Map(); function remove(surface, callback = owned.get(surface)) { if (!callback || owned.get(surface) !== callback) return; owned.delete(surface); const registry = art[surface]; if (surface === "controls" && registry.cache?.get && registry.cache.get(name)?.option?.onSelect !== callback) return; if (surface === "setting" && registry.find && registry.find(name)?.onSelect !== callback) return; registry.remove(name); } function clear() { current = void 0; let failure; for (const [surface, callback] of [...owned]) { try { remove(surface, callback); } catch (error) { failure || (failure = error); } } if (failure) throw failure; } function update(config, model, active) { if (!model) { clear(); return; } const state = {}; current = state; const valid = () => state === current && active(); const onSelect = (item) => { if (!valid()) return item.html; model.select(item, valid); if (!valid()) return item.html; art.notice.show = `${model.title}: ${item.html}`; if (valid() && config.control) art.controls.check(item); if (valid() && config.setting) art.setting.check(item); return item.html; }; if (config.control) { owned.set("controls", onSelect); art.controls.update({ name, position: "right", html: model.html, style: { padding: "0 10px" }, selector: model.selector, onSelect }); } else { remove("controls"); } if (!valid()) return; if (config.setting) { owned.set("setting", onSelect); art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect }); } else { remove("setting"); } } return { update, clear }; } const $quality = ''; function observeSDK(options) { let binding; const active = (record) => binding === record && options.active(record.dash); const automatic = (record) => record.dash.getSettings().streaming.abr.autoSwitchBitrate.video; function release() { const record = binding; if (!record) return; binding = void 0; record.epoch++; let failure; for (const [name, callback] of record.callbacks.splice(0)) { try { record.off.call(record.dash, name, callback); } catch (error) { failure || (failure = error); } } if (failure) throw failure; } function fail(record, error) { if (binding === record) { try { release(); } catch (cleanupError) { console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError); } if (!binding && options.active(record.dash)) { try { options.reset(); } catch (cleanupError) { console.warn("ArtPlayer DASH cleanup failed:", cleanupError); } } } console.warn("ArtPlayer DASH refresh failed:", error); } function schedule(record) { if (!active(record) || record.suspended || record.queued || record.running) return; record.queued = true; const epoch = record.epoch; void Promise.resolve().then(() => { if (!active(record) || record.epoch !== epoch || record.suspended) return; record.queued = false; record.running = true; try { record.automatic = automatic(record); if (active(record)) options.refresh(); } catch (error) { fail(record, error); } finally { record.running = false; } }); } function bind(dash) { const previous = binding; if (previous?.dash === dash) { const value = automatic(previous); if (active(previous)) { previous.automatic = value; previous.suspended = false; } return; } release(); if (binding || !options.active(dash) || typeof dash.on !== "function" || typeof dash.off !== "function") return; const record = { dash, off: dash.off, callbacks: [], epoch: 0, queued: false, running: false, suspended: false, automatic: false }; binding = record; const on = dash.on; try { record.automatic = automatic(record); const changed = ["qualityChangeRequested", "qualityChangeRendered", "trackChangeRendered", "streamUpdated", "streamInitialized"]; const entries = changed.map((name) => [name, () => { if (name === "streamUpdated" || name === "streamInitialized") record.suspended = false; schedule(record); }]); entries.push(["playbackTimeUpdated", () => { if (!active(record) || record.suspended) return; try { const value = automatic(record); if (active(record) && value !== record.automatic) { record.automatic = value; schedule(record); } } catch (error) { fail(record, error); } }]); entries.push(["streamTeardownComplete", () => { if (!active(record)) return; record.epoch++; record.queued = false; record.suspended = true; try { options.reset(); } catch (error) { fail(record, error); } }]); for (const entry of entries) { if (!active(record)) break; record.callbacks.push(entry); on.call(dash, ...entry); } } catch (error) { if (binding === record) { try { release(); } catch (cleanupError) { console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError); } } throw error; } } return { bind, release }; } function artplayerPluginDashControl(option = {}) { return (player) => { const art = player; const { $video } = art.template; const { errorHandle } = art.constructor.utils; let closed = false; let revision = 0; const quality = createMenu(art, "dash-quality", $quality); const audio = createMenu(art, "dash-audio", $audio); const subscriptions = []; const observer = observeSDK({ active: (dash) => !closed && !art.isDestroy && art.dash === dash, refresh: update, reset() { const version = ++revision; clear(() => version === revision); } }); function clear(current = () => true) { let failure; for (const cleanup of [quality.clear, audio.clear]) { if (!current()) break; try { cleanup(); } catch (error) { failure || (failure = error); } } if (failure) throw failure; } function update() { if (closed || art.isDestroy) return; const version = ++revision; const dash = art.dash; const current = () => !closed && !art.isDestroy && version === revision && art.dash === dash; const valid = () => current() && dash.getVideoElement() === $video && current(); try { errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"'); if (!current()) return; observer.bind(dash); if (!valid()) return; const qualityConfig = option.quality || {}; const qualities = qualityModel(dash, qualityConfig, valid); if (!valid()) return; quality.update(qualityConfig, qualities, valid); if (!valid()) return; const audioConfig = option.audio || {}; const tracks = audioModel(dash, audioConfig, valid); if (valid()) audio.update(audioConfig, tracks, valid); } catch (error) { if (current()) { const cleanupVersion = ++revision; for (const cleanup of [observer.release, () => clear(() => revision === cleanupVersion)]) { try { cleanup(); } catch (cleanupError) { console.warn("ArtPlayer DASH cleanup failed:", cleanupError); } } } throw error; } } function destroy() { if (closed) return; closed = true; revision++; let failure; const actions = [observer.release, clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))]; for (const action of actions) { try { action(); } catch (error) { failure || (failure = error); } } if (failure) throw failure; } try { const entries = [["ready", update], ["restart", update], ["destroy", destroy]]; for (const entry of entries) { if (closed) break; subscriptions.push(entry); art.on(...entry); } } catch (error) { try { destroy(); } catch (cleanupError) { console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError); } throw error; } return { name: "artplayerPluginDashControl", update }; }; } export { artplayerPluginDashControl as default };