/*! * artplayer-plugin-hls-control.js v2.0.0 * Github: https://github.com/zhw2590582/ArtPlayer * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ const $audio = ''; 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) { first.default = true; first.value = item.value; } return false; } seen.set(item.html, item); return true; }); } function qualitySelection(hls) { return hls.autoLevelEnabled === true ? -1 : hls.currentLevel; } function qualityModel(hls, config) { if (!hls.levels.length) return null; const auto = config.auto || "Auto"; const getName = config.getName || ((level2) => level2.name || `${level2.height}P`); const selected = qualitySelection(hls); const level = hls.levels[selected]; const html = level ? getName(level) : auto; const selector = uniqueLabels(hls.levels.map((item, index) => ({ html: getName(item, index), value: index, default: selected === index }))).sort((a, b) => b.value - a.value); selector.push({ html: auto, value: -1, default: selected === -1 }); return { html, title: config.title || "Quality", selector }; } function audioModel(hls, config) { if (!hls.audioTracks.length) return null; const auto = config.auto || "Auto"; const getName = config.getName || ((track2) => track2.name || track2.lang || track2.language); const track = hls.audioTracks[hls.audioTrack]; const html = track ? getName(track) : auto; const selector = uniqueLabels(hls.audioTracks.map((item, index) => ({ html: getName(item, index), value: item.id, default: hls.audioTrack === item.id }))); return { html, title: config.title || "Audio", selector }; } function createMenu(art, name, field, icon, active, select) { let current; const owned = { control: false, setting: false }; function remove(surface) { if (!owned[surface]) return; owned[surface] = false; art[surface === "control" ? "controls" : "setting"].remove(name); } function clear() { current = void 0; remove("control"); remove("setting"); } function check(state, item) { for (const entry of state.model.selector) entry.default = entry === item; if (state.config.control) art.controls.check(item); if (state !== current || !active(state.hls)) return; if (state.config.setting) art.setting.check(item); } function update(hls, config, model, force) { if (!model) { clear(); return; } const previous = current; const selectedIndex = model.selector.findIndex((item) => item.default); const reusable = !force && previous?.hls === hls && previous.model.title === model.title && (selectedIndex !== -1 || !previous.model.selector.some((item) => item.default) && previous.model.html === model.html) && owned.control === Boolean(config.control) && owned.setting === Boolean(config.setting) && previous.model.selector.length === model.selector.length && previous.model.selector.every((item, index) => item.html === model.selector[index].html && item.value === model.selector[index].value); if (reusable) { previous.config = config; const target = previous.model.selector[selectedIndex]; if (target && (!target.default || previous.model.html !== model.html)) { previous.model.html = model.html; check(previous, target); } return; } const state = { hls, config, model }; current = state; const valid = () => current === state && active(hls); const onSelect = (item) => select(() => { if (!valid()) return item.html; hls[field] = item.value; if (!valid()) return item.html; model.html = item.html; art.notice.show = `${model.title}: ${item.html}`; if (valid()) check(state, item); return item.html; }); if (config.control) { owned.control = true; art.controls.update({ name, position: "right", html: model.html, style: { padding: "0 10px" }, selector: model.selector, onSelect }); } else { remove("control"); } if (!valid()) return; if (config.setting) { owned.setting = true; art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect }); } else { remove("setting"); } } function invalidate() { current = void 0; } return { update, clear, invalidate }; } const $quality = ''; function subscribeHls(hls, refresh, destroyed) { const events = hls.constructor?.Events; if (!events || typeof hls.on !== "function" || typeof hls.off !== "function") return () => { }; const source = hls; const subscriptions = []; let active = true; function release() { if (!active) return; active = false; for (const [event, callback] of subscriptions) source.off(event, callback); subscriptions.length = 0; } try { for (const key of ["MANIFEST_PARSED", "LEVELS_UPDATED", "LEVEL_SWITCHED", "AUDIO_TRACKS_UPDATED", "AUDIO_TRACK_SWITCHED", "DESTROYING"]) { const event = events[key]; if (typeof event !== "string" || subscriptions.some(([name]) => name === event)) continue; const callback = () => { if (!active) return; if (key === "DESTROYING") destroyed(); else refresh(); }; subscriptions.push([event, callback]); source.on(event, callback); } } catch (error) { release(); throw error; } return release; } function artplayerPluginHlsControl(option = {}) { return (player) => { const art = player; const { $video } = art.template; const { errorHandle } = art.constructor.utils; let closed = false; let revision = 0; let engine; let unsubscribe = () => { }; let selecting = 0; let pending; const retired = /* @__PURE__ */ new WeakSet(); const active = (hls) => !closed && !retired.has(hls) && art.hls === hls && hls.media === $video; const quality = createMenu(art, "hls-quality", "currentLevel", $quality, active, select); const audio = createMenu(art, "hls-audio", "audioTrack", $audio, active, select); function select(callback) { selecting++; let succeeded = false; try { const result = callback(); succeeded = true; return result; } finally { selecting--; if (!selecting) { const hls = pending; pending = void 0; if (succeeded && hls) refresh(hls, false); } } } function refresh(hls, force) { if (!active(hls)) return; const version = ++revision; const config = option.quality || {}; const model = qualityModel(hls, config); if (!active(hls) || version !== revision) return; quality.update(hls, config, model, force); if (!active(hls) || version !== revision) return; const audioConfig = option.audio || {}; const audioView = audioModel(hls, audioConfig); if (active(hls) && version === revision) audio.update(hls, audioConfig, audioView, force); } function update() { if (closed) return; const hls = art.hls; errorHandle(hls?.media === $video, 'Cannot find instance of HLS from "art.hls"'); if (retired.has(hls)) return; if (engine !== hls) { unsubscribe(); engine = hls; try { unsubscribe = subscribeHls(hls, () => { if (selecting) pending = hls; else refresh(hls, false); }, () => { retired.add(hls); unsubscribe(); if (engine === hls) { engine = void 0; if (!closed) { quality.clear(); audio.clear(); } } }); } catch (error) { engine = void 0; throw error; } } refresh(hls, true); } function destroy() { if (closed) return; closed = true; revision++; pending = void 0; quality.invalidate(); audio.invalidate(); unsubscribe(); art.off("ready", update); art.off("restart", update); art.off("destroy", destroy); } art.on("ready", update); art.on("restart", update); art.on("destroy", destroy); return { name: "artplayerPluginHlsControl", update }; }; } export { artplayerPluginHlsControl as default };