nixamp 0.18.0 → 0.18.1

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/dist/naming.d.ts CHANGED
@@ -55,3 +55,17 @@ export declare function readCertFiles(stateDir: string, host: string): {
55
55
  * first label of its hostname, made safe for a subdomain.
56
56
  */
57
57
  export declare function labelFor(name: string, hostname: string): string;
58
+ /**
59
+ * What to call a stream that was started on a path, for people.
60
+ *
61
+ * `nixamp serve ~/Music/live-sets_2024` used to be listed as the machine's
62
+ * hostname, which tells a stranger nothing. The last segment of the path is
63
+ * what the person who made the folder called it, so that is the title: the
64
+ * separators that a filesystem forces become spaces, a playlist loses its
65
+ * ending, and each lowercase word gets a capital. Words with a capital in
66
+ * them already are left alone, so "DJ" and "LoFi" stay as written, and a
67
+ * dash or dot between two digits stays too, so a date is still a date.
68
+ * Empty when the path has no segment worth saying, and the caller falls
69
+ * back to whatever it used before.
70
+ */
71
+ export declare function humanizeSource(source: string): string;
package/dist/naming.js CHANGED
@@ -148,3 +148,46 @@ export function labelFor(name, hostname) {
148
148
  }
149
149
  return "server";
150
150
  }
151
+ /** File endings that name a playlist rather than a folder, dropped from a title. */
152
+ const PLAYLIST_ENDING = /\.(m3u8?|pls|xspf|txt|json)$/i;
153
+ /**
154
+ * What to call a stream that was started on a path, for people.
155
+ *
156
+ * `nixamp serve ~/Music/live-sets_2024` used to be listed as the machine's
157
+ * hostname, which tells a stranger nothing. The last segment of the path is
158
+ * what the person who made the folder called it, so that is the title: the
159
+ * separators that a filesystem forces become spaces, a playlist loses its
160
+ * ending, and each lowercase word gets a capital. Words with a capital in
161
+ * them already are left alone, so "DJ" and "LoFi" stay as written, and a
162
+ * dash or dot between two digits stays too, so a date is still a date.
163
+ * Empty when the path has no segment worth saying, and the caller falls
164
+ * back to whatever it used before.
165
+ */
166
+ export function humanizeSource(source) {
167
+ const remote = /^[a-z][a-z0-9+.-]*:\/\//i.test(source);
168
+ let last = "";
169
+ try {
170
+ const path = remote ? new URL(source).pathname : source;
171
+ last = path.split("/").filter(Boolean).pop() ?? "";
172
+ if (remote)
173
+ last = decodeURIComponent(last);
174
+ }
175
+ catch {
176
+ return "";
177
+ }
178
+ if (last === "" || last === "~" || last === ".")
179
+ return "";
180
+ const words = last
181
+ .replace(PLAYLIST_ENDING, "")
182
+ .replace(/(?<!\d)[-_.+]+|[-_.+]+(?!\d)/g, " ")
183
+ .replace(/\s+/g, " ")
184
+ .trim();
185
+ if (words === "")
186
+ return "";
187
+ return words
188
+ .split(" ")
189
+ .map((word) => (word === word.toLowerCase() ? word.charAt(0).toUpperCase() + word.slice(1) : word))
190
+ .join(" ")
191
+ .slice(0, 60)
192
+ .trim();
193
+ }
package/dist/server.d.ts CHANGED
@@ -458,9 +458,29 @@ export declare function isSignInPath(path: string): boolean;
458
458
  *
459
459
  * Read from the page rather than hard-coded, because one NixAmp serves several
460
460
  * branded clients and each brings its own head. og:site_name first, then the
461
- * part of the title before the dash, then the hostname.
461
+ * part of the title before the dash or the colon ("nixamp: broadcast live
462
+ * radio..." is nixamp), then the hostname.
462
463
  */
463
464
  export declare function brandOf(shell: string, site: string): string;
465
+ /**
466
+ * What a join link is for, named by something this server actually knows.
467
+ *
468
+ * A link preview reads the page before any script runs, so the shell has to
469
+ * say what is on the air. On the directory that is the listing the link
470
+ * points at, and the channel on it if one is asked for; on a server it is
471
+ * the server itself, or one of its channels. A name that only appears in the
472
+ * query is not used: a page that titled itself with whatever the address said
473
+ * would be a preview anyone could put words in.
474
+ */
475
+ export declare function joinSubject(url: URL, options: Pick<HandlerOptions, "directory" | "channels" | "serverName">): {
476
+ title: string;
477
+ where: string;
478
+ } | null;
479
+ /** The shell, titled for what a join link opens. */
480
+ export declare function joinDocument(shell: string, subject: {
481
+ title: string;
482
+ where: string;
483
+ }, site: string): string;
464
484
  export interface HandlerOptions {
465
485
  web: string | null;
466
486
  media: boolean;
package/dist/server.js CHANGED
@@ -51,7 +51,7 @@ import { Catalogs, shownCatalog, shownEntry } from "./catalogs.js";
51
51
  import { Porkbun, isIPv4, isIPv6 } from "./dns.js";
52
52
  import { NameError, Names } from "./names.js";
53
53
  import { AcmeIssuer, Certs } from "./certs.js";
54
- import { claimName, fetchCert, labelFor, readCertFiles, writeCertFiles } from "./naming.js";
54
+ import { claimName, fetchCert, humanizeSource, labelFor, readCertFiles, writeCertFiles } from "./naming.js";
55
55
  import { forbiddenLibrary, readLibrary } from "./library.js";
56
56
  import { createThrottle, presentedCredential } from "@profullstack/throttle";
57
57
  import { Durable } from "./durable.js";
@@ -1057,14 +1057,15 @@ function htmlText(value) {
1057
1057
  *
1058
1058
  * Read from the page rather than hard-coded, because one NixAmp serves several
1059
1059
  * branded clients and each brings its own head. og:site_name first, then the
1060
- * part of the title before the dash, then the hostname.
1060
+ * part of the title before the dash or the colon ("nixamp: broadcast live
1061
+ * radio..." is nixamp), then the hostname.
1061
1062
  */
1062
1063
  export function brandOf(shell, site) {
1063
1064
  const named = /<meta\s+property="og:site_name"\s+content="([^"]+)"/i.exec(shell);
1064
1065
  if (named?.[1])
1065
1066
  return named[1];
1066
1067
  const titled = /<title>([^<]*)<\/title>/i.exec(shell);
1067
- const head = titled?.[1]?.split(/\s[—-]\s/)[0]?.trim();
1068
+ const head = titled?.[1]?.split(/\s[—-]\s|:\s/)[0]?.trim();
1068
1069
  if (head)
1069
1070
  return head;
1070
1071
  try {
@@ -1090,6 +1091,65 @@ function eventDocument(shell, event, site) {
1090
1091
  .replace(/<meta property="og:description" content="[^"]*"\s*\/>/, `<meta property="og:description" content="${htmlText(description)}" />`)
1091
1092
  .replace("</head>", `${event.visibility === "public" ? "" : '<meta name="robots" content="noindex,nofollow" />'}\n <meta property="og:url" content="${htmlText(canonical)}" />\n <link rel="canonical" href="${htmlText(canonical)}" />\n <script type="application/ld+json">${structured}</script>\n </head>`);
1092
1093
  }
1094
+ /**
1095
+ * What a join link is for, named by something this server actually knows.
1096
+ *
1097
+ * A link preview reads the page before any script runs, so the shell has to
1098
+ * say what is on the air. On the directory that is the listing the link
1099
+ * points at, and the channel on it if one is asked for; on a server it is
1100
+ * the server itself, or one of its channels. A name that only appears in the
1101
+ * query is not used: a page that titled itself with whatever the address said
1102
+ * would be a preview anyone could put words in.
1103
+ */
1104
+ export function joinSubject(url, options) {
1105
+ const play = url.searchParams.get("play") ?? "";
1106
+ const wanted = play.startsWith("channel:") ? play.slice("channel:".length) : "";
1107
+ const link = url.searchParams.get("url") ?? "";
1108
+ if (options.directory) {
1109
+ if (link === "")
1110
+ return null;
1111
+ const listings = options.directory.list();
1112
+ const listing = listings.find((one) => one.url === link) ?? listings.find((one) => {
1113
+ try {
1114
+ return new URL(one.url).origin === new URL(link).origin;
1115
+ }
1116
+ catch {
1117
+ return false;
1118
+ }
1119
+ });
1120
+ if (!listing)
1121
+ return null;
1122
+ if (wanted !== "" && listing.channels.includes(wanted))
1123
+ return { title: wanted, where: listing.name };
1124
+ return { title: listing.name, where: "" };
1125
+ }
1126
+ // A link to somewhere else is that server's to name.
1127
+ if (link !== "")
1128
+ return null;
1129
+ const name = options.serverName ?? "";
1130
+ if (wanted !== "") {
1131
+ const channel = options.channels?.list().find((one) => one.id === wanted || one.name === wanted);
1132
+ return channel ? { title: channel.name, where: name } : null;
1133
+ }
1134
+ return name === "" ? null : { title: name, where: "" };
1135
+ }
1136
+ /** The shell, titled for what a join link opens. */
1137
+ export function joinDocument(shell, subject, site) {
1138
+ const brand = brandOf(shell, site);
1139
+ const shellTitle = /<title>([^<]*)<\/title>/i.exec(shell)?.[1] ?? "";
1140
+ const title = `${subject.title} — ${brand}`;
1141
+ const description = subject.where === ""
1142
+ ? `${subject.title} is live on ${brand}. Tune in free, no account needed.`
1143
+ : `${subject.title} is live on ${subject.where}. Tune in free on ${brand}, no account needed.`;
1144
+ return shell
1145
+ .replace(/<title>.*?<\/title>/s, `<title>${htmlText(title)}</title>`)
1146
+ .replace(/<meta name="description" content="[^"]*"\s*\/>/, `<meta name="description" content="${htmlText(description)}" />`)
1147
+ .replace(/<meta property="og:title" content="[^"]*"\s*\/>/, `<meta property="og:title" content="${htmlText(subject.title)}" />`)
1148
+ .replace(/<meta property="og:description" content="[^"]*"\s*\/>/, `<meta property="og:description" content="${htmlText(description)}" />`)
1149
+ // What the tab is called once the page takes over, so it does not stack
1150
+ // the channel on a title that already names the channel.
1151
+ .replace("</head>", `<meta name="nixamp-shell-title" content="${htmlText(shellTitle)}" />\n </head>`);
1152
+ }
1093
1153
  function json(response, code, body) {
1094
1154
  const text = JSON.stringify(body);
1095
1155
  response.writeHead(code, {
@@ -3720,6 +3780,14 @@ export function createHandler(engine, options) {
3720
3780
  }
3721
3781
  catch { }
3722
3782
  }
3783
+ if (file.endsWith("index.html") && path === "/") {
3784
+ const subject = joinSubject(url, options);
3785
+ const shell = subject ? readIfPossible(file) : null;
3786
+ if (subject && shell !== null) {
3787
+ html(response, 200, joinDocument(shell, subject, options.invites?.site ?? "https://nixamp.com"));
3788
+ return;
3789
+ }
3790
+ }
3723
3791
  sendFile(request, response, file);
3724
3792
  return;
3725
3793
  }
@@ -4089,6 +4157,9 @@ export async function serve(argv, version = "0.1.0") {
4089
4157
  throw new Error("nixamp serve: which folder? Say `nixamp library ~/Music` once, or `nixamp daemon start ~/Music`.");
4090
4158
  }
4091
4159
  const root = isRemote(chosen) ? chosen : resolve(chosen);
4160
+ // What the stream is called where people see it: the name given, else the
4161
+ // folder or playlist it was started on, in words, else this machine.
4162
+ const streamName = options.name || humanizeSource(root) || hostname();
4092
4163
  const why = isRemote(root) ? "" : forbiddenLibrary(root);
4093
4164
  if (why) {
4094
4165
  throw new Error(`nixamp will not serve ${why}. Pick a folder with your media in it: nixamp library ~/Music`);
@@ -4548,14 +4619,14 @@ export async function serve(argv, version = "0.1.0") {
4548
4619
  rememberChannels: remembering,
4549
4620
  catalogs,
4550
4621
  publishUrls: () => publishUrls,
4551
- serverName: options.name || hostname(),
4622
+ serverName: streamName,
4552
4623
  homeSource: root,
4553
4624
  live: {
4554
4625
  status: () => ({
4555
4626
  live: publisher !== null,
4556
4627
  code: listing?.code ?? "",
4557
4628
  channelCodes: listing?.channelCodes ?? {},
4558
- name: listing?.name ?? (options.name || hostname()),
4629
+ name: listing?.name ?? streamName,
4559
4630
  url: listing?.url ?? (publishable_ ? shareLink(publishable_.url, listenKey, false) : ""),
4560
4631
  // Whether going live is even possible here. A laptop behind a router
4561
4632
  // with no address the world can reach cannot be listed, and a button
@@ -4916,7 +4987,7 @@ export async function serve(argv, version = "0.1.0") {
4916
4987
  const audio = audioLink(publishable_.url, listenKey);
4917
4988
  return new Publisher({
4918
4989
  directory: DEFAULT_DIRECTORY,
4919
- name: options.name || hostname(),
4990
+ name: streamName,
4920
4991
  url: listen,
4921
4992
  audio,
4922
4993
  // The control link, for the owner to open this machine as its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
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/naming.ts CHANGED
@@ -195,3 +195,44 @@ export function labelFor(name: string, hostname: string): string {
195
195
  }
196
196
  return "server";
197
197
  }
198
+
199
+ /** File endings that name a playlist rather than a folder, dropped from a title. */
200
+ const PLAYLIST_ENDING = /\.(m3u8?|pls|xspf|txt|json)$/i;
201
+
202
+ /**
203
+ * What to call a stream that was started on a path, for people.
204
+ *
205
+ * `nixamp serve ~/Music/live-sets_2024` used to be listed as the machine's
206
+ * hostname, which tells a stranger nothing. The last segment of the path is
207
+ * what the person who made the folder called it, so that is the title: the
208
+ * separators that a filesystem forces become spaces, a playlist loses its
209
+ * ending, and each lowercase word gets a capital. Words with a capital in
210
+ * them already are left alone, so "DJ" and "LoFi" stay as written, and a
211
+ * dash or dot between two digits stays too, so a date is still a date.
212
+ * Empty when the path has no segment worth saying, and the caller falls
213
+ * back to whatever it used before.
214
+ */
215
+ export function humanizeSource(source: string): string {
216
+ const remote = /^[a-z][a-z0-9+.-]*:\/\//i.test(source);
217
+ let last = "";
218
+ try {
219
+ const path = remote ? new URL(source).pathname : source;
220
+ last = path.split("/").filter(Boolean).pop() ?? "";
221
+ if (remote) last = decodeURIComponent(last);
222
+ } catch {
223
+ return "";
224
+ }
225
+ if (last === "" || last === "~" || last === ".") return "";
226
+ const words = last
227
+ .replace(PLAYLIST_ENDING, "")
228
+ .replace(/(?<!\d)[-_.+]+|[-_.+]+(?!\d)/g, " ")
229
+ .replace(/\s+/g, " ")
230
+ .trim();
231
+ if (words === "") return "";
232
+ return words
233
+ .split(" ")
234
+ .map((word) => (word === word.toLowerCase() ? word.charAt(0).toUpperCase() + word.slice(1) : word))
235
+ .join(" ")
236
+ .slice(0, 60)
237
+ .trim();
238
+ }
package/src/server.ts CHANGED
@@ -72,7 +72,7 @@ import { Catalogs, shownCatalog, shownEntry } from "./catalogs.ts";
72
72
  import { Porkbun, isIPv4, isIPv6, type DnsZone } from "./dns.ts";
73
73
  import { NameError, Names } from "./names.ts";
74
74
  import { AcmeIssuer, Certs } from "./certs.ts";
75
- import { claimName, fetchCert, labelFor, readCertFiles, writeCertFiles } from "./naming.ts";
75
+ import { claimName, fetchCert, humanizeSource, labelFor, readCertFiles, writeCertFiles } from "./naming.ts";
76
76
  import { forbiddenLibrary, readLibrary } from "./library.ts";
77
77
  import { createThrottle, presentedCredential, type Throttle } from "@profullstack/throttle";
78
78
  import { Durable } from "./durable.ts";
@@ -1304,13 +1304,14 @@ function htmlText(value: string): string {
1304
1304
  *
1305
1305
  * Read from the page rather than hard-coded, because one NixAmp serves several
1306
1306
  * branded clients and each brings its own head. og:site_name first, then the
1307
- * part of the title before the dash, then the hostname.
1307
+ * part of the title before the dash or the colon ("nixamp: broadcast live
1308
+ * radio..." is nixamp), then the hostname.
1308
1309
  */
1309
1310
  export function brandOf(shell: string, site: string): string {
1310
1311
  const named = /<meta\s+property="og:site_name"\s+content="([^"]+)"/i.exec(shell);
1311
1312
  if (named?.[1]) return named[1];
1312
1313
  const titled = /<title>([^<]*)<\/title>/i.exec(shell);
1313
- const head = titled?.[1]?.split(/\s[—-]\s/)[0]?.trim();
1314
+ const head = titled?.[1]?.split(/\s[—-]\s|:\s/)[0]?.trim();
1314
1315
  if (head) return head;
1315
1316
  try {
1316
1317
  return new URL(site).hostname.replace(/^www\./, "");
@@ -1335,6 +1336,65 @@ function eventDocument(shell: string, event: Awaited<ReturnType<LiveEvents["get"
1335
1336
  .replace("</head>", `${event.visibility === "public" ? "" : '<meta name="robots" content="noindex,nofollow" />'}\n <meta property="og:url" content="${htmlText(canonical)}" />\n <link rel="canonical" href="${htmlText(canonical)}" />\n <script type="application/ld+json">${structured}</script>\n </head>`);
1336
1337
  }
1337
1338
 
1339
+ /**
1340
+ * What a join link is for, named by something this server actually knows.
1341
+ *
1342
+ * A link preview reads the page before any script runs, so the shell has to
1343
+ * say what is on the air. On the directory that is the listing the link
1344
+ * points at, and the channel on it if one is asked for; on a server it is
1345
+ * the server itself, or one of its channels. A name that only appears in the
1346
+ * query is not used: a page that titled itself with whatever the address said
1347
+ * would be a preview anyone could put words in.
1348
+ */
1349
+ export function joinSubject(
1350
+ url: URL,
1351
+ options: Pick<HandlerOptions, "directory" | "channels" | "serverName">,
1352
+ ): { title: string; where: string } | null {
1353
+ const play = url.searchParams.get("play") ?? "";
1354
+ const wanted = play.startsWith("channel:") ? play.slice("channel:".length) : "";
1355
+ const link = url.searchParams.get("url") ?? "";
1356
+ if (options.directory) {
1357
+ if (link === "") return null;
1358
+ const listings = options.directory.list();
1359
+ const listing = listings.find((one) => one.url === link) ?? listings.find((one) => {
1360
+ try {
1361
+ return new URL(one.url).origin === new URL(link).origin;
1362
+ } catch {
1363
+ return false;
1364
+ }
1365
+ });
1366
+ if (!listing) return null;
1367
+ if (wanted !== "" && listing.channels.includes(wanted)) return { title: wanted, where: listing.name };
1368
+ return { title: listing.name, where: "" };
1369
+ }
1370
+ // A link to somewhere else is that server's to name.
1371
+ if (link !== "") return null;
1372
+ const name = options.serverName ?? "";
1373
+ if (wanted !== "") {
1374
+ const channel = options.channels?.list().find((one) => one.id === wanted || one.name === wanted);
1375
+ return channel ? { title: channel.name, where: name } : null;
1376
+ }
1377
+ return name === "" ? null : { title: name, where: "" };
1378
+ }
1379
+
1380
+ /** The shell, titled for what a join link opens. */
1381
+ export function joinDocument(shell: string, subject: { title: string; where: string }, site: string): string {
1382
+ const brand = brandOf(shell, site);
1383
+ const shellTitle = /<title>([^<]*)<\/title>/i.exec(shell)?.[1] ?? "";
1384
+ const title = `${subject.title} — ${brand}`;
1385
+ const description = subject.where === ""
1386
+ ? `${subject.title} is live on ${brand}. Tune in free, no account needed.`
1387
+ : `${subject.title} is live on ${subject.where}. Tune in free on ${brand}, no account needed.`;
1388
+ return shell
1389
+ .replace(/<title>.*?<\/title>/s, `<title>${htmlText(title)}</title>`)
1390
+ .replace(/<meta name="description" content="[^"]*"\s*\/>/, `<meta name="description" content="${htmlText(description)}" />`)
1391
+ .replace(/<meta property="og:title" content="[^"]*"\s*\/>/, `<meta property="og:title" content="${htmlText(subject.title)}" />`)
1392
+ .replace(/<meta property="og:description" content="[^"]*"\s*\/>/, `<meta property="og:description" content="${htmlText(description)}" />`)
1393
+ // What the tab is called once the page takes over, so it does not stack
1394
+ // the channel on a title that already names the channel.
1395
+ .replace("</head>", `<meta name="nixamp-shell-title" content="${htmlText(shellTitle)}" />\n </head>`);
1396
+ }
1397
+
1338
1398
  function json(response: ServerResponse, code: number, body: unknown): void {
1339
1399
  const text = JSON.stringify(body);
1340
1400
  response.writeHead(code, {
@@ -4276,6 +4336,14 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
4276
4336
  }
4277
4337
  } catch {}
4278
4338
  }
4339
+ if (file.endsWith("index.html") && path === "/") {
4340
+ const subject = joinSubject(url, options);
4341
+ const shell = subject ? readIfPossible(file) : null;
4342
+ if (subject && shell !== null) {
4343
+ html(response, 200, joinDocument(shell, subject, options.invites?.site ?? "https://nixamp.com"));
4344
+ return;
4345
+ }
4346
+ }
4279
4347
  sendFile(request, response, file);
4280
4348
  return;
4281
4349
  }
@@ -4675,6 +4743,9 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
4675
4743
  );
4676
4744
  }
4677
4745
  const root = isRemote(chosen) ? chosen : resolve(chosen);
4746
+ // What the stream is called where people see it: the name given, else the
4747
+ // folder or playlist it was started on, in words, else this machine.
4748
+ const streamName = options.name || humanizeSource(root) || hostname();
4678
4749
  const why = isRemote(root) ? "" : forbiddenLibrary(root);
4679
4750
  if (why) {
4680
4751
  throw new Error(`nixamp will not serve ${why}. Pick a folder with your media in it: nixamp library ~/Music`);
@@ -5161,14 +5232,14 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
5161
5232
  rememberChannels: remembering,
5162
5233
  catalogs,
5163
5234
  publishUrls: () => publishUrls,
5164
- serverName: options.name || hostname(),
5235
+ serverName: streamName,
5165
5236
  homeSource: root,
5166
5237
  live: {
5167
5238
  status: () => ({
5168
5239
  live: publisher !== null,
5169
5240
  code: listing?.code ?? "",
5170
5241
  channelCodes: listing?.channelCodes ?? {},
5171
- name: listing?.name ?? (options.name || hostname()),
5242
+ name: listing?.name ?? streamName,
5172
5243
  url: listing?.url ?? (publishable_ ? shareLink(publishable_.url, listenKey, false) : ""),
5173
5244
  // Whether going live is even possible here. A laptop behind a router
5174
5245
  // with no address the world can reach cannot be listed, and a button
@@ -5549,7 +5620,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
5549
5620
  const audio = audioLink(publishable_.url, listenKey);
5550
5621
  return new Publisher({
5551
5622
  directory: DEFAULT_DIRECTORY,
5552
- name: options.name || hostname(),
5623
+ name: streamName,
5553
5624
  url: listen,
5554
5625
  audio,
5555
5626
  // The control link, for the owner to open this machine as its
@@ -1 +1 @@
1
- import{t as e}from"./index-DF6O2leR.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-BafZs9FO.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};