nixamp 0.28.2 → 0.28.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -574,7 +574,7 @@ uses its direct model; it does not first translate the audio into English.
574
574
 
575
575
  ### Hear it in your language
576
576
 
577
- Every signed-in account gets **five free live-use sessions per UTC day**, shared
577
+ Every signed-in account gets **ten free live-use sessions per UTC day**, shared
578
578
  across Nixamp's paid panels and upgrades. There is no timed cutoff: a session
579
579
  continues while its listener remains, within the existing API usage limits.
580
580
  Reconnects to the same session within 90 seconds reuse it. A stopped session
@@ -614,7 +614,7 @@ multiplied by five. Credentials and balances never travel in checkout URLs.
614
614
  New checkout creation is capped at five per account and fifty per account server
615
615
  per UTC day, plus IP and request throttles; retries reuse the original invoice.
616
616
 
617
- After the five free sessions, new sessions require purchased credit. Configure `COINPAY_X402_KEY`
617
+ After the ten free sessions, new sessions require purchased credit. Configure `COINPAY_X402_KEY`
618
618
  with `payments:create` permission and at least one business wallet; the scoped
619
619
  key supplies the merchant identity. Free sessions and existing credit still work during a
620
620
  checkout outage. A self-hosted operator explicitly sponsoring API usage may set
@@ -141,6 +141,8 @@ export interface Announcement {
141
141
  export interface LineupEntry {
142
142
  id: string;
143
143
  name: string;
144
+ /** Account that put this channel on the air, when known. */
145
+ startedBy?: string;
144
146
  kind: "audio" | "video";
145
147
  art: string;
146
148
  about: string;
package/dist/directory.js CHANGED
@@ -112,6 +112,9 @@ export function parseLineupEntry(input) {
112
112
  return {
113
113
  id,
114
114
  name,
115
+ ...(typeof record["startedBy"] === "string" && /^[a-zA-Z0-9_-]{1,64}$/.test(record["startedBy"])
116
+ ? { startedBy: record["startedBy"] }
117
+ : {}),
115
118
  kind: record["kind"] === "video" ? "video" : "audio",
116
119
  // A picture is somewhere a crawler can go, or nothing: a data: URL is a
117
120
  // page in a tag, and javascript: is a page in a tag that runs.
package/dist/owner.js CHANGED
@@ -132,6 +132,8 @@ export function needsAdmin(path, method = "GET") {
132
132
  export function needsMember(path, method = "GET") {
133
133
  if (method === "POST" && /^\/api\/tracks\/\d+\/live$/.test(path))
134
134
  return true;
135
+ if (method === "POST" && path === "/api/folders/live")
136
+ return true;
135
137
  if (method === "POST" && /^\/api\/catalogs\/[^/]+\/entries\/[^/]+\/live$/.test(path))
136
138
  return true;
137
139
  if (method === "POST" && /^\/api\/channels\/[^/]+\/keep$/.test(path))
package/dist/server.js CHANGED
@@ -1009,6 +1009,7 @@ export function lineupEntry(listenLink, info) {
1009
1009
  return {
1010
1010
  id: info.id,
1011
1011
  name: info.name,
1012
+ ...(info.startedBy ? { startedBy: info.startedBy } : {}),
1012
1013
  kind: info.kind ?? "audio",
1013
1014
  art: absoluteArtOf(listenLink, info),
1014
1015
  about: info.about ?? "",
@@ -3568,7 +3569,15 @@ export function createHandler(engine, options) {
3568
3569
  redials: one.redials ?? 0,
3569
3570
  error: one.error ?? "",
3570
3571
  // For a list: how long it is, and which entry is on, from 0.
3571
- ...(one.playlist ? { entries: one.playlist.length, entry: one.playlistAt ?? 0 } : {}),
3572
+ ...(one.playlist ? {
3573
+ entries: one.playlist.length,
3574
+ entry: one.playlistAt ?? 0,
3575
+ playlist: one.playlist.map((source) => {
3576
+ const parts = source.split(/[\\/]/).filter(Boolean);
3577
+ return parts.length > 1 ? parts.slice(-2).join(" / ") : (parts[0] || "Untitled");
3578
+ }),
3579
+ live: one.live !== false,
3580
+ } : {}),
3572
3581
  // The member who put it on, when one did: theirs to take off.
3573
3582
  startedBy: one.startedBy ?? "",
3574
3583
  // A picture of it, and a line about it, where there is one.
@@ -4031,6 +4040,62 @@ export function createHandler(engine, options) {
4031
4040
  * do and which made two files at once impossible for anybody. The owner
4032
4041
  * and any member may; the caps and the throttle above say how much.
4033
4042
  */
4043
+ if (path === "/api/folders/live" && request.method === "POST") {
4044
+ if (!options.channels) {
4045
+ json(response, 503, { error: "this server cannot carry channels" });
4046
+ return;
4047
+ }
4048
+ let body = {};
4049
+ try {
4050
+ body = JSON.parse(await readBody(request));
4051
+ }
4052
+ catch {
4053
+ json(response, 400, { error: "bad JSON" });
4054
+ return;
4055
+ }
4056
+ const indices = Array.isArray(body.indices)
4057
+ ? [...new Set(body.indices.filter((one) => Number.isInteger(one) && one >= 0))]
4058
+ : [];
4059
+ if (indices.length === 0 || indices.length > 10_000) {
4060
+ json(response, 400, { error: "choose a folder with playable files" });
4061
+ return;
4062
+ }
4063
+ const tracks = engine.snapshot().tracks ?? [];
4064
+ const selected = indices
4065
+ .map((index) => ({ index, track: tracks[index], source: engine.trackPath(index) }))
4066
+ .filter((one) => Boolean(one.track && one.source && !isRemote(one.source)));
4067
+ if (selected.length !== indices.length) {
4068
+ json(response, 404, { error: "one or more folder files are no longer available" });
4069
+ return;
4070
+ }
4071
+ selected.sort((a, b) => String(a.track.title ?? a.source).localeCompare(String(b.track.title ?? b.source), undefined, { numeric: true, sensitivity: "base" }));
4072
+ const sources = selected.map((one) => one.source);
4073
+ const channelId = cleanId(`folder-${sha("sha1").update(sources.join("\n")).digest("hex").slice(0, 12)}`);
4074
+ const name = typeof body.name === "string" && body.name.trim() !== ""
4075
+ ? body.name.trim().slice(0, 120)
4076
+ : selected[0]?.track.folder || "folder";
4077
+ if (liveBy && !options.channels.has(channelId)) {
4078
+ const refusal = memberLiveRefusal(options.channels, liveBy);
4079
+ if (refusal) {
4080
+ json(response, 429, { error: refusal });
4081
+ return;
4082
+ }
4083
+ }
4084
+ if (!options.channels.has(channelId)) {
4085
+ const started = await pullChannel(options.channels, options.ffprobe ?? ["ffprobe"], channelId, name, sources[0], [], "", { playlist: sources, live: true });
4086
+ if (!started) {
4087
+ json(response, 409, { error: "that folder is already going on the air" });
4088
+ return;
4089
+ }
4090
+ if (liveBy)
4091
+ options.channels.info(channelId).startedBy = liveBy;
4092
+ }
4093
+ options.channels.keep(channelId);
4094
+ options.rememberChannels?.(rememberedNow(options.channels));
4095
+ void options.live?.announce?.();
4096
+ json(response, 200, { channel: channelId, name, video: options.channels.kindOf(channelId) !== "audio" });
4097
+ return;
4098
+ }
4034
4099
  const trackLive = /^\/api\/tracks\/(\d+)\/live$/.exec(path);
4035
4100
  if (trackLive && request.method === "POST") {
4036
4101
  if (!options.channels) {
@@ -5,17 +5,12 @@ export declare const TRANSLATION_PLANS: readonly [{
5
5
  readonly id: "day";
6
6
  readonly name: "Day";
7
7
  readonly days: 1;
8
- readonly priceCents: 500;
9
- }, {
10
- readonly id: "week";
11
- readonly name: "Week";
12
- readonly days: 7;
13
- readonly priceCents: 2500;
8
+ readonly priceCents: 100;
14
9
  }, {
15
10
  readonly id: "month";
16
11
  readonly name: "Month";
17
12
  readonly days: 30;
18
- readonly priceCents: 10000;
13
+ readonly priceCents: 500;
19
14
  }];
20
15
  export type TranslationPlan = typeof TRANSLATION_PLANS[number];
21
16
  export type TranslationUsage = "transcription" | "voice";
@@ -5,9 +5,8 @@ import { SpeechError } from "./speech.js";
5
5
  import { UpgradeAllowances } from "./upgrade-allowance.js";
6
6
  export const TRANSLATION_MULTIPLIER = 5;
7
7
  export const TRANSLATION_PLANS = [
8
- { id: "day", name: "Day", days: 1, priceCents: 500 },
9
- { id: "week", name: "Week", days: 7, priceCents: 2500 },
10
- { id: "month", name: "Month", days: 30, priceCents: 10000 },
8
+ { id: "day", name: "Day", days: 1, priceCents: 100 },
9
+ { id: "month", name: "Month", days: 30, priceCents: 500 },
11
10
  ];
12
11
  const COINS = ["USDC_POL", "USDC_SOL", "SOL", "POL", "BTC", "ETH", "USDC_ETH", "BCH", "DOGE"];
13
12
  const ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
@@ -1,5 +1,5 @@
1
1
  import type { Queryable } from "./follows.ts";
2
- export declare const FREE_UPGRADE_SESSIONS = 5;
2
+ export declare const FREE_UPGRADE_SESSIONS = 10;
3
3
  export declare const FREE_UPGRADE_RECONNECT_SECONDS = 90;
4
4
  export interface UpgradeAllowance {
5
5
  limit: number;
@@ -2,7 +2,7 @@
2
2
  * Claim only on explicit use, never from balance polling or provider chunks. */
3
3
  import { createHash } from "node:crypto";
4
4
  import { SpeechError } from "./speech.js";
5
- export const FREE_UPGRADE_SESSIONS = 5;
5
+ export const FREE_UPGRADE_SESSIONS = 10;
6
6
  export const FREE_UPGRADE_RECONNECT_SECONDS = 90;
7
7
  const DAY = 86_400_000;
8
8
  export class UpgradeAllowances {
@@ -16,8 +16,11 @@ export class UpgradeAllowances {
16
16
  async ensure() {
17
17
  this.schema ??= this.db.query(`CREATE TABLE IF NOT EXISTS upgrade_daily_uses (
18
18
  by_account TEXT NOT NULL, day BIGINT NOT NULL,
19
- uses INTEGER NOT NULL CHECK (uses BETWEEN 1 AND 5),
20
- sessions JSONB NOT NULL, PRIMARY KEY (by_account, day))`).then(() => undefined)
19
+ uses INTEGER NOT NULL CHECK (uses >= 1),
20
+ sessions JSONB NOT NULL, PRIMARY KEY (by_account, day))`)
21
+ .then(() => this.db.query(`ALTER TABLE upgrade_daily_uses DROP CONSTRAINT IF EXISTS upgrade_daily_uses_uses_check`))
22
+ .then(() => this.db.query(`ALTER TABLE upgrade_daily_uses ADD CONSTRAINT upgrade_daily_uses_uses_check CHECK (uses BETWEEN 1 AND 10)`))
23
+ .then(() => undefined)
21
24
  .catch(error => { this.schema = null; throw error; });
22
25
  await this.schema;
23
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.28.2",
3
+ "version": "0.28.3",
4
4
  "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/directory.ts CHANGED
@@ -149,6 +149,8 @@ export interface Announcement {
149
149
  export interface LineupEntry {
150
150
  id: string;
151
151
  name: string;
152
+ /** Account that put this channel on the air, when known. */
153
+ startedBy?: string;
152
154
  kind: "audio" | "video";
153
155
  art: string;
154
156
  about: string;
@@ -247,6 +249,9 @@ export function parseLineupEntry(input: unknown): LineupEntry | null {
247
249
  return {
248
250
  id,
249
251
  name,
252
+ ...(typeof record["startedBy"] === "string" && /^[a-zA-Z0-9_-]{1,64}$/.test(record["startedBy"])
253
+ ? { startedBy: record["startedBy"] }
254
+ : {}),
250
255
  kind: record["kind"] === "video" ? "video" : "audio",
251
256
  // A picture is somewhere a crawler can go, or nothing: a data: URL is a
252
257
  // page in a tag, and javascript: is a page in a tag that runs.
package/src/owner.ts CHANGED
@@ -146,6 +146,7 @@ export function needsAdmin(path: string, method = "GET"): boolean {
146
146
  */
147
147
  export function needsMember(path: string, method = "GET"): boolean {
148
148
  if (method === "POST" && /^\/api\/tracks\/\d+\/live$/.test(path)) return true;
149
+ if (method === "POST" && path === "/api/folders/live") return true;
149
150
  if (method === "POST" && /^\/api\/catalogs\/[^/]+\/entries\/[^/]+\/live$/.test(path)) return true;
150
151
  if (method === "POST" && /^\/api\/channels\/[^/]+\/keep$/.test(path)) return true;
151
152
  if (method === "POST" && /^\/api\/channels\/[^/]+\/(?:start|stop|restart)$/.test(path)) return true;
package/src/server.ts CHANGED
@@ -1253,6 +1253,7 @@ export function lineupEntry(listenLink: string, info: ChannelInfo): LineupEntry
1253
1253
  return {
1254
1254
  id: info.id,
1255
1255
  name: info.name,
1256
+ ...(info.startedBy ? { startedBy: info.startedBy } : {}),
1256
1257
  kind: info.kind ?? "audio",
1257
1258
  art: absoluteArtOf(listenLink, info),
1258
1259
  about: info.about ?? "",
@@ -4014,7 +4015,15 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
4014
4015
  redials: one.redials ?? 0,
4015
4016
  error: one.error ?? "",
4016
4017
  // For a list: how long it is, and which entry is on, from 0.
4017
- ...(one.playlist ? { entries: one.playlist.length, entry: one.playlistAt ?? 0 } : {}),
4018
+ ...(one.playlist ? {
4019
+ entries: one.playlist.length,
4020
+ entry: one.playlistAt ?? 0,
4021
+ playlist: one.playlist.map((source) => {
4022
+ const parts = source.split(/[\\/]/).filter(Boolean);
4023
+ return parts.length > 1 ? parts.slice(-2).join(" / ") : (parts[0] || "Untitled");
4024
+ }),
4025
+ live: one.live !== false,
4026
+ } : {}),
4018
4027
  // The member who put it on, when one did: theirs to take off.
4019
4028
  startedBy: one.startedBy ?? "",
4020
4029
  // A picture of it, and a line about it, where there is one.
@@ -4492,6 +4501,53 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
4492
4501
  * do and which made two files at once impossible for anybody. The owner
4493
4502
  * and any member may; the caps and the throttle above say how much.
4494
4503
  */
4504
+ if (path === "/api/folders/live" && request.method === "POST") {
4505
+ if (!options.channels) {
4506
+ json(response, 503, { error: "this server cannot carry channels" });
4507
+ return;
4508
+ }
4509
+ let body: { indices?: unknown; name?: unknown } = {};
4510
+ try { body = JSON.parse(await readBody(request)) as typeof body; } catch {
4511
+ json(response, 400, { error: "bad JSON" });
4512
+ return;
4513
+ }
4514
+ const indices = Array.isArray(body.indices)
4515
+ ? [...new Set(body.indices.filter((one): one is number => Number.isInteger(one) && one >= 0))]
4516
+ : [];
4517
+ if (indices.length === 0 || indices.length > 10_000) {
4518
+ json(response, 400, { error: "choose a folder with playable files" });
4519
+ return;
4520
+ }
4521
+ const tracks = engine.snapshot().tracks ?? [];
4522
+ const selected = indices
4523
+ .map((index) => ({ index, track: tracks[index], source: engine.trackPath(index) }))
4524
+ .filter((one): one is { index: number; track: (typeof tracks)[number]; source: string } => Boolean(one.track && one.source && !isRemote(one.source)));
4525
+ if (selected.length !== indices.length) {
4526
+ json(response, 404, { error: "one or more folder files are no longer available" });
4527
+ return;
4528
+ }
4529
+ selected.sort((a, b) => String(a.track.title ?? a.source).localeCompare(String(b.track.title ?? b.source), undefined, { numeric: true, sensitivity: "base" }));
4530
+ const sources = selected.map((one) => one.source);
4531
+ const channelId = cleanId(`folder-${sha("sha1").update(sources.join("\n")).digest("hex").slice(0, 12)}`);
4532
+ const name = typeof body.name === "string" && body.name.trim() !== ""
4533
+ ? body.name.trim().slice(0, 120)
4534
+ : selected[0]?.track.folder || "folder";
4535
+ if (liveBy && !options.channels.has(channelId)) {
4536
+ const refusal = memberLiveRefusal(options.channels, liveBy);
4537
+ if (refusal) { json(response, 429, { error: refusal }); return; }
4538
+ }
4539
+ if (!options.channels.has(channelId)) {
4540
+ const started = await pullChannel(options.channels, options.ffprobe ?? ["ffprobe"], channelId, name, sources[0]!, [], "", { playlist: sources, live: true });
4541
+ if (!started) { json(response, 409, { error: "that folder is already going on the air" }); return; }
4542
+ if (liveBy) options.channels.info(channelId)!.startedBy = liveBy;
4543
+ }
4544
+ options.channels.keep(channelId);
4545
+ options.rememberChannels?.(rememberedNow(options.channels));
4546
+ void options.live?.announce?.();
4547
+ json(response, 200, { channel: channelId, name, video: options.channels.kindOf(channelId) !== "audio" });
4548
+ return;
4549
+ }
4550
+
4495
4551
  const trackLive = /^\/api\/tracks\/(\d+)\/live$/.exec(path);
4496
4552
  if (trackLive && request.method === "POST") {
4497
4553
  if (!options.channels) {
@@ -7,9 +7,8 @@ import { UpgradeAllowances, type UpgradeAllowance } from "./upgrade-allowance.ts
7
7
 
8
8
  export const TRANSLATION_MULTIPLIER = 5;
9
9
  export const TRANSLATION_PLANS = [
10
- { id: "day", name: "Day", days: 1, priceCents: 500 },
11
- { id: "week", name: "Week", days: 7, priceCents: 2500 },
12
- { id: "month", name: "Month", days: 30, priceCents: 10000 },
10
+ { id: "day", name: "Day", days: 1, priceCents: 100 },
11
+ { id: "month", name: "Month", days: 30, priceCents: 500 },
13
12
  ] as const;
14
13
  export type TranslationPlan = typeof TRANSLATION_PLANS[number];
15
14
  export type TranslationUsage = "transcription" | "voice";
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
4
4
  import type { Queryable } from "./follows.ts";
5
5
  import { SpeechError } from "./speech.ts";
6
6
 
7
- export const FREE_UPGRADE_SESSIONS = 5;
7
+ export const FREE_UPGRADE_SESSIONS = 10;
8
8
  export const FREE_UPGRADE_RECONNECT_SECONDS = 90;
9
9
  const DAY = 86_400_000;
10
10
  export interface UpgradeAllowance {
@@ -18,8 +18,11 @@ export class UpgradeAllowances {
18
18
  async ensure(): Promise<void> {
19
19
  this.schema ??= this.db.query(`CREATE TABLE IF NOT EXISTS upgrade_daily_uses (
20
20
  by_account TEXT NOT NULL, day BIGINT NOT NULL,
21
- uses INTEGER NOT NULL CHECK (uses BETWEEN 1 AND 5),
22
- sessions JSONB NOT NULL, PRIMARY KEY (by_account, day))`).then(() => undefined)
21
+ uses INTEGER NOT NULL CHECK (uses >= 1),
22
+ sessions JSONB NOT NULL, PRIMARY KEY (by_account, day))`)
23
+ .then(() => this.db.query(`ALTER TABLE upgrade_daily_uses DROP CONSTRAINT IF EXISTS upgrade_daily_uses_uses_check`))
24
+ .then(() => this.db.query(`ALTER TABLE upgrade_daily_uses ADD CONSTRAINT upgrade_daily_uses_uses_check CHECK (uses BETWEEN 1 AND 10)`))
25
+ .then(() => undefined)
23
26
  .catch(error => { this.schema = null; throw error; });
24
27
  await this.schema;
25
28
  }
@@ -1 +1 @@
1
- import{t as e}from"./index-BqcDIh2Q.js";var t=3;async function n(n){let{media:r,src:i,isTv:a}=n,{default:o}=await e(async()=>{let{default:e}=await import(`./hls-n74Cnh8A.js`);return{default:e}},[]);if(!o.isSupported())return n.onError(`This browser cannot play HLS streams.`),{destroy:()=>void 0,levels:()=>[]};let s=new o({...a?{maxBufferLength:60,maxMaxBufferLength:120,backBufferLength:30,liveSyncDurationCount:4}:{backBufferLength:90},enableWorker:!0}),c=0,l=!1;s.on(o.Events.ERROR,(e,r)=>{if(!l&&r.fatal){if(c>=t){n.onError(`This stream kept failing and has been stopped.`),s.destroy();return}switch(c+=1,r.type){case o.ErrorTypes.NETWORK_ERROR:n.onNotice(`Reconnecting…`),s.startLoad();break;case o.ErrorTypes.MEDIA_ERROR:n.onNotice(`Recovering…`),s.recoverMediaError();break;default:n.onError(`This stream could not be played.`),s.destroy()}}}),s.on(o.Events.MANIFEST_PARSED,()=>{l||(n.onNotice(null),n.onReady?.({live:s.levels.length>0&&!Number.isFinite(r.duration),levels:u()}))}),s.on(o.Events.LEVEL_LOADED,(e,t)=>{l||n.onReady?.({live:t.details.live,levels:u()})}),s.on(o.Events.FRAG_BUFFERED,()=>{l||n.onNotice(null)});function u(){return s.levels.map((e,t)=>({index:t,height:e.height||null,bitrate:e.bitrate||null,label:e.height?`${String(e.height)}p`:`${String(Math.round((e.bitrate||0)/1e3))}k`}))}return s.loadSource(i),s.attachMedia(r),{destroy(){l=!0,s.destroy()},levels:u,setLevel(e){s.currentLevel=e},currentLevel:()=>s.autoLevelEnabled?-1:s.currentLevel}}export{n as createHlsEngine};
1
+ import{t as e}from"./index-B33hHbmF.js";var t=3;async function n(n){let{media:r,src:i,isTv:a}=n,{default:o}=await e(async()=>{let{default:e}=await import(`./hls-n74Cnh8A.js`);return{default:e}},[]);if(!o.isSupported())return n.onError(`This browser cannot play HLS streams.`),{destroy:()=>void 0,levels:()=>[]};let s=new o({...a?{maxBufferLength:60,maxMaxBufferLength:120,backBufferLength:30,liveSyncDurationCount:4}:{backBufferLength:90},enableWorker:!0}),c=0,l=!1;s.on(o.Events.ERROR,(e,r)=>{if(!l&&r.fatal){if(c>=t){n.onError(`This stream kept failing and has been stopped.`),s.destroy();return}switch(c+=1,r.type){case o.ErrorTypes.NETWORK_ERROR:n.onNotice(`Reconnecting…`),s.startLoad();break;case o.ErrorTypes.MEDIA_ERROR:n.onNotice(`Recovering…`),s.recoverMediaError();break;default:n.onError(`This stream could not be played.`),s.destroy()}}}),s.on(o.Events.MANIFEST_PARSED,()=>{l||(n.onNotice(null),n.onReady?.({live:s.levels.length>0&&!Number.isFinite(r.duration),levels:u()}))}),s.on(o.Events.LEVEL_LOADED,(e,t)=>{l||n.onReady?.({live:t.details.live,levels:u()})}),s.on(o.Events.FRAG_BUFFERED,()=>{l||n.onNotice(null)});function u(){return s.levels.map((e,t)=>({index:t,height:e.height||null,bitrate:e.bitrate||null,label:e.height?`${String(e.height)}p`:`${String(Math.round((e.bitrate||0)/1e3))}k`}))}return s.loadSource(i),s.attachMedia(r),{destroy(){l=!0,s.destroy()},levels:u,setLevel(e){s.currentLevel=e},currentLevel:()=>s.autoLevelEnabled?-1:s.currentLevel}}export{n as createHlsEngine};