@gentomiyano/optimized-web-audio-player 0.2.3 → 0.3.0

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/ui/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useRef, useId, useEffect } from 'react';
1
+ import { useState, useRef, useId, useEffect, useMemo } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  // src/ui/player-primitives.tsx
@@ -174,6 +174,8 @@ function getErrorMessage(error) {
174
174
  return "This media could not be decoded.";
175
175
  case "unsupported":
176
176
  return "This media format is not supported.";
177
+ case "seek":
178
+ return "Seeking failed. Try again.";
177
179
  case "audio-context":
178
180
  return "Audio output is not available.";
179
181
  case "unknown":
@@ -293,6 +295,9 @@ function TrackMetadata({
293
295
  scrollTitle = false,
294
296
  status
295
297
  }) {
298
+ const intrinsicContext = item?.contentContext;
299
+ const creatorName = item?.creatorName ?? (intrinsicContext?.sourceType === "release-track" ? intrinsicContext.artistName : void 0);
300
+ const collectionTitle = item?.collectionTitle ?? (intrinsicContext?.sourceType === "release-track" ? intrinsicContext.releaseTitle : intrinsicContext?.sourceType === "podcast-episode" ? intrinsicContext.programTitle : void 0);
296
301
  return /* @__PURE__ */ jsxs(
297
302
  "div",
298
303
  {
@@ -307,8 +312,8 @@ function TrackMetadata({
307
312
  },
308
313
  item?.title ?? "No media selected"
309
314
  ),
310
- /* @__PURE__ */ jsx("span", { className: "player-creator", children: item === null ? "Select media to begin" : item.creatorName ?? "Artist unavailable" }),
311
- item?.collectionTitle === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-collection", children: item.collectionTitle })
315
+ /* @__PURE__ */ jsx("span", { className: "player-creator", children: item === null ? "Select media to begin" : creatorName ?? "Artist unavailable" }),
316
+ collectionTitle === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-collection", children: collectionTitle })
312
317
  ]
313
318
  }
314
319
  );
@@ -1134,6 +1139,747 @@ function DetailedPlayer({
1134
1139
  }
1135
1140
  );
1136
1141
  }
1142
+
1143
+ // src/consumer/content-context.ts
1144
+ function contentContextMatches(context, expected) {
1145
+ if (context?.sourceType !== expected.sourceType) {
1146
+ return false;
1147
+ }
1148
+ if (context.sourceType === "release-track") {
1149
+ return context.releaseId === expected.parentId && context.trackId === expected.entryId;
1150
+ }
1151
+ return context.programId === expected.parentId && context.episodeId === expected.entryId;
1152
+ }
1153
+ function buildIntrinsicContext(entries, contextId, label, expectedSourceType, parentId) {
1154
+ const queue = [];
1155
+ const rejected = [];
1156
+ const contentIds = /* @__PURE__ */ new Set();
1157
+ const queueItemIds = /* @__PURE__ */ new Set();
1158
+ entries.forEach((entry, entryIndex) => {
1159
+ if (contentIds.has(entry.id)) {
1160
+ rejected.push({
1161
+ entryId: entry.id,
1162
+ entryIndex,
1163
+ reason: "duplicate-content-id"
1164
+ });
1165
+ return;
1166
+ }
1167
+ contentIds.add(entry.id);
1168
+ if (entry.availability.state !== "ready") {
1169
+ if (entry.queueItem !== void 0) {
1170
+ rejected.push({
1171
+ entryId: entry.id,
1172
+ entryIndex,
1173
+ reason: "non-ready-with-queue-item"
1174
+ });
1175
+ }
1176
+ return;
1177
+ }
1178
+ if (entry.queueItem === void 0) {
1179
+ rejected.push({
1180
+ entryId: entry.id,
1181
+ entryIndex,
1182
+ reason: "ready-without-queue-item"
1183
+ });
1184
+ return;
1185
+ }
1186
+ if (queueItemIds.has(entry.queueItem.queueItemId)) {
1187
+ rejected.push({
1188
+ entryId: entry.id,
1189
+ entryIndex,
1190
+ reason: "duplicate-queue-item-id"
1191
+ });
1192
+ return;
1193
+ }
1194
+ if (!contentContextMatches(entry.queueItem.item.contentContext, {
1195
+ sourceType: expectedSourceType,
1196
+ parentId,
1197
+ entryId: entry.id
1198
+ })) {
1199
+ rejected.push({
1200
+ entryId: entry.id,
1201
+ entryIndex,
1202
+ reason: "content-context-mismatch"
1203
+ });
1204
+ return;
1205
+ }
1206
+ queueItemIds.add(entry.queueItem.queueItemId);
1207
+ queue.push(entry.queueItem);
1208
+ });
1209
+ return Object.freeze({
1210
+ context: Object.freeze({
1211
+ id: contextId,
1212
+ mode: "ordered-queue",
1213
+ label,
1214
+ queue: Object.freeze(queue)
1215
+ }),
1216
+ rejected: Object.freeze(
1217
+ rejected.map((rejection) => Object.freeze(rejection))
1218
+ )
1219
+ });
1220
+ }
1221
+ function buildReleasePlaybackContext(release) {
1222
+ return buildIntrinsicContext(
1223
+ release.tracks,
1224
+ release.contextId ?? `release:${release.id}`,
1225
+ release.title,
1226
+ "release-track",
1227
+ release.id
1228
+ );
1229
+ }
1230
+ function buildPodcastPlaybackContext(program) {
1231
+ return buildIntrinsicContext(
1232
+ program.episodes,
1233
+ program.contextId ?? `podcast:${program.id}`,
1234
+ program.title,
1235
+ "podcast-episode",
1236
+ program.id
1237
+ );
1238
+ }
1239
+ function findActivePodcastChapter(chapters, currentTimeSec) {
1240
+ if (chapters === void 0 || !Number.isFinite(currentTimeSec)) {
1241
+ return null;
1242
+ }
1243
+ let active = null;
1244
+ for (const chapter of chapters) {
1245
+ if (!Number.isFinite(chapter.startTimeSec) || chapter.startTimeSec < 0 || chapter.startTimeSec > currentTimeSec) {
1246
+ continue;
1247
+ }
1248
+ if (active === null || chapter.startTimeSec >= active.startTimeSec) {
1249
+ active = chapter;
1250
+ }
1251
+ }
1252
+ return active;
1253
+ }
1254
+ function ContentArtwork({
1255
+ alt,
1256
+ artwork,
1257
+ className
1258
+ }) {
1259
+ return /* @__PURE__ */ jsx(
1260
+ "span",
1261
+ {
1262
+ className: ["player-content__artwork", className].filter(Boolean).join(" "),
1263
+ children: artwork === void 0 ? /* @__PURE__ */ jsx("span", { "aria-label": `${alt} artwork unavailable`, role: "img" }) : /* @__PURE__ */ jsx("img", { alt: artwork.alt, src: artwork.url })
1264
+ }
1265
+ );
1266
+ }
1267
+ function availabilityLabel(state) {
1268
+ switch (state) {
1269
+ case "ready":
1270
+ return "Ready";
1271
+ case "processing":
1272
+ return "Processing";
1273
+ case "locked":
1274
+ return "Locked";
1275
+ case "failed":
1276
+ return "Unavailable";
1277
+ case "unavailable":
1278
+ return "Unavailable";
1279
+ }
1280
+ }
1281
+ function IntrinsicTransport({
1282
+ active,
1283
+ canStart,
1284
+ commands,
1285
+ onStart,
1286
+ state
1287
+ }) {
1288
+ const isPlaying = active && state.status === "playing";
1289
+ return /* @__PURE__ */ jsxs(
1290
+ "div",
1291
+ {
1292
+ "aria-label": "Content playback controls",
1293
+ className: "player-content__transport",
1294
+ role: "group",
1295
+ children: [
1296
+ /* @__PURE__ */ jsx(
1297
+ IconButton,
1298
+ {
1299
+ disabled: !active || !state.canGoPrevious,
1300
+ label: "Previous item",
1301
+ onClick: () => {
1302
+ void commands.previous();
1303
+ },
1304
+ children: /* @__PURE__ */ jsx(PreviousIcon, {})
1305
+ }
1306
+ ),
1307
+ /* @__PURE__ */ jsx(
1308
+ IconButton,
1309
+ {
1310
+ className: [
1311
+ "player-play-button",
1312
+ isPlaying ? "player-play-button--active" : ""
1313
+ ].filter(Boolean).join(" "),
1314
+ disabled: !canStart || active && state.status === "loading",
1315
+ label: isPlaying ? "Pause" : "Play",
1316
+ onClick: () => {
1317
+ if (active) {
1318
+ void commands.togglePlayback();
1319
+ } else {
1320
+ onStart();
1321
+ }
1322
+ },
1323
+ children: isPlaying ? /* @__PURE__ */ jsx(PauseIcon, { size: 22 }) : /* @__PURE__ */ jsx(PlayIcon, { size: 22 })
1324
+ }
1325
+ ),
1326
+ /* @__PURE__ */ jsx(
1327
+ IconButton,
1328
+ {
1329
+ disabled: !active || !state.canGoNext,
1330
+ label: "Next item",
1331
+ onClick: () => {
1332
+ void commands.next();
1333
+ },
1334
+ children: /* @__PURE__ */ jsx(NextIcon, {})
1335
+ }
1336
+ )
1337
+ ]
1338
+ }
1339
+ );
1340
+ }
1341
+ function activateIntrinsicItem(commands, context, queueItemId) {
1342
+ commands.setShuffle(false);
1343
+ commands.setRepeatMode("off");
1344
+ return commands.activateContext({
1345
+ contextId: context.id,
1346
+ mode: "ordered-queue",
1347
+ ...context.label === void 0 ? {} : { label: context.label },
1348
+ queue: context.queue,
1349
+ startQueueItemId: queueItemId,
1350
+ autoplay: true
1351
+ });
1352
+ }
1353
+ function useIntrinsicContextGuard(commands, context, state) {
1354
+ const nextQueueKey = context.queue.map((entry) => entry.queueItemId).join("\0");
1355
+ const activeQueueKey = state.activeContext?.queue.map((entry) => entry.queueItemId).join("\0");
1356
+ useEffect(() => {
1357
+ if (state.activeContext?.id !== context.id || state.currentQueueItemId === null) {
1358
+ return;
1359
+ }
1360
+ if (!context.queue.some(
1361
+ (entry) => entry.queueItemId === state.currentQueueItemId
1362
+ )) {
1363
+ void commands.clear();
1364
+ return;
1365
+ }
1366
+ if (activeQueueKey !== nextQueueKey) {
1367
+ commands.setShuffle(false);
1368
+ commands.setRepeatMode("off");
1369
+ void commands.activateContext({
1370
+ contextId: context.id,
1371
+ mode: "ordered-queue",
1372
+ ...context.label === void 0 ? {} : { label: context.label },
1373
+ queue: context.queue,
1374
+ startQueueItemId: state.currentQueueItemId,
1375
+ autoplay: state.status === "playing"
1376
+ });
1377
+ }
1378
+ }, [
1379
+ activeQueueKey,
1380
+ commands,
1381
+ context,
1382
+ nextQueueKey,
1383
+ state.activeContext?.id,
1384
+ state.currentQueueItemId,
1385
+ state.status
1386
+ ]);
1387
+ }
1388
+ function ReleasePlayer({
1389
+ appearance,
1390
+ className,
1391
+ commands,
1392
+ release,
1393
+ state
1394
+ }) {
1395
+ const build = useMemo(
1396
+ () => buildReleasePlaybackContext(release),
1397
+ [release]
1398
+ );
1399
+ const playableIds = new Set(
1400
+ build.context.queue.map((entry) => entry.queueItemId)
1401
+ );
1402
+ const active = state.activeContext?.id === build.context.id;
1403
+ useIntrinsicContextGuard(commands, build.context, state);
1404
+ const currentTrackId = active && state.currentItem?.contentContext?.sourceType === "release-track" ? state.currentItem.contentContext.trackId : null;
1405
+ const first = build.context.queue[0];
1406
+ const currentTrack = release.tracks.find((track) => track.id === currentTrackId) ?? release.tracks.find(
1407
+ (track) => track.queueItem?.queueItemId === first?.queueItemId
1408
+ ) ?? null;
1409
+ const displayItem = active ? state.currentItem : first?.item ?? null;
1410
+ const displayCurrentTimeSec = active ? state.currentTimeSec : 0;
1411
+ const displayDurationSec = active ? state.durationSec : displayItem?.durationHintSec ?? null;
1412
+ const waveformProgress = displayDurationSec === null || displayDurationSec <= 0 ? 0 : displayCurrentTimeSec / displayDurationSec;
1413
+ const playTrack = (track) => {
1414
+ if (track.queueItem === void 0 || !playableIds.has(track.queueItem.queueItemId)) {
1415
+ return;
1416
+ }
1417
+ void activateIntrinsicItem(
1418
+ commands,
1419
+ build.context,
1420
+ track.queueItem.queueItemId
1421
+ );
1422
+ };
1423
+ return /* @__PURE__ */ jsxs(
1424
+ PlayerSurface,
1425
+ {
1426
+ appearance,
1427
+ className: ["player-release", "player-content", className].filter(Boolean).join(" "),
1428
+ label: "Release player",
1429
+ children: [
1430
+ /* @__PURE__ */ jsxs("div", { className: "player-release__hero", children: [
1431
+ /* @__PURE__ */ jsx(
1432
+ ContentArtwork,
1433
+ {
1434
+ alt: release.title,
1435
+ artwork: release.artwork,
1436
+ className: "player-release__artwork"
1437
+ }
1438
+ ),
1439
+ /* @__PURE__ */ jsxs("div", { className: "player-release__detail", children: [
1440
+ /* @__PURE__ */ jsxs("header", { className: "player-release__identity", children: [
1441
+ /* @__PURE__ */ jsx("span", { className: "player-kicker", children: "Release" }),
1442
+ /* @__PURE__ */ jsx("h2", { children: release.title }),
1443
+ /* @__PURE__ */ jsx("p", { className: "player-release__artist", children: release.artistName }),
1444
+ release.releaseDate === void 0 && release.releaseType === void 0 ? null : /* @__PURE__ */ jsxs("dl", { className: "player-release__facts", children: [
1445
+ release.releaseDate === void 0 ? null : /* @__PURE__ */ jsxs("div", { children: [
1446
+ /* @__PURE__ */ jsx("dt", { children: "Released" }),
1447
+ /* @__PURE__ */ jsx("dd", { children: release.releaseDate })
1448
+ ] }),
1449
+ release.releaseType === void 0 ? null : /* @__PURE__ */ jsxs("div", { children: [
1450
+ /* @__PURE__ */ jsx("dt", { children: "Format" }),
1451
+ /* @__PURE__ */ jsx("dd", { children: release.releaseType })
1452
+ ] })
1453
+ ] })
1454
+ ] }),
1455
+ release.notes === void 0 ? null : /* @__PURE__ */ jsx("p", { className: "player-release__notes", children: release.notes }),
1456
+ release.credits === void 0 || release.credits.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "player-release__credits", children: [
1457
+ /* @__PURE__ */ jsx("span", { children: "Credits" }),
1458
+ /* @__PURE__ */ jsx("ul", { children: release.credits.map((credit) => /* @__PURE__ */ jsx("li", { children: credit }, credit)) })
1459
+ ] }),
1460
+ /* @__PURE__ */ jsxs(
1461
+ "section",
1462
+ {
1463
+ "aria-label": "Current Release Track",
1464
+ className: "player-release__playback",
1465
+ children: [
1466
+ /* @__PURE__ */ jsxs("div", { className: "player-release__now-playing", children: [
1467
+ /* @__PURE__ */ jsx("span", { children: "Current Track" }),
1468
+ /* @__PURE__ */ jsx("strong", { children: currentTrack === null ? "No playable Tracks" : currentTrack.title }),
1469
+ active ? /* @__PURE__ */ jsx(PlayerStatus, { state }) : null
1470
+ ] }),
1471
+ /* @__PURE__ */ jsxs("div", { className: "player-release__signal", children: [
1472
+ /* @__PURE__ */ jsx(
1473
+ Waveform,
1474
+ {
1475
+ opacity: state.experienceConfig.waveformOpacity,
1476
+ playedStateEnabled: state.experienceConfig.waveformPlayedStateEnabled,
1477
+ progress: waveformProgress,
1478
+ waveform: displayItem?.waveform
1479
+ }
1480
+ ),
1481
+ /* @__PURE__ */ jsx(
1482
+ SeekBar,
1483
+ {
1484
+ canSeek: active && state.canSeek,
1485
+ commitMode: state.experienceConfig.seekCommitMode,
1486
+ currentTimeSec: displayCurrentTimeSec,
1487
+ durationSec: displayDurationSec,
1488
+ label: "Seek in current Release Track",
1489
+ onSeek: (seconds) => {
1490
+ commands.seekTo(seconds);
1491
+ },
1492
+ status: active ? state.status : "paused"
1493
+ }
1494
+ ),
1495
+ /* @__PURE__ */ jsx(
1496
+ TimeReadout,
1497
+ {
1498
+ currentTimeSec: displayCurrentTimeSec,
1499
+ durationSec: displayDurationSec
1500
+ }
1501
+ )
1502
+ ] }),
1503
+ /* @__PURE__ */ jsx(
1504
+ IntrinsicTransport,
1505
+ {
1506
+ active,
1507
+ canStart: first !== void 0,
1508
+ commands,
1509
+ onStart: () => {
1510
+ if (first !== void 0) {
1511
+ void activateIntrinsicItem(
1512
+ commands,
1513
+ build.context,
1514
+ first.queueItemId
1515
+ );
1516
+ }
1517
+ },
1518
+ state
1519
+ }
1520
+ )
1521
+ ]
1522
+ }
1523
+ )
1524
+ ] })
1525
+ ] }),
1526
+ /* @__PURE__ */ jsxs(
1527
+ "section",
1528
+ {
1529
+ "aria-labelledby": `release-tracklist-${release.id}`,
1530
+ className: "player-release__tracklist",
1531
+ children: [
1532
+ /* @__PURE__ */ jsxs("div", { className: "player-section-heading", children: [
1533
+ /* @__PURE__ */ jsx("span", { id: `release-tracklist-${release.id}`, children: "Tracklist" }),
1534
+ /* @__PURE__ */ jsxs("span", { className: "player-queue-count", children: [
1535
+ String(release.tracks.length),
1536
+ " tracks"
1537
+ ] })
1538
+ ] }),
1539
+ release.tracks.length === 0 ? /* @__PURE__ */ jsx("p", { className: "player-empty", children: "Tracklist is empty." }) : /* @__PURE__ */ jsx("ol", { className: "player-content__list", children: release.tracks.map((track, index) => {
1540
+ const playable = track.queueItem !== void 0 && playableIds.has(track.queueItem.queueItemId);
1541
+ const isCurrent = track.id === currentTrackId;
1542
+ return /* @__PURE__ */ jsx(
1543
+ "li",
1544
+ {
1545
+ "data-availability": track.availability.state,
1546
+ "data-current": isCurrent ? "true" : "false",
1547
+ children: /* @__PURE__ */ jsxs(
1548
+ "button",
1549
+ {
1550
+ "aria-current": isCurrent ? "true" : void 0,
1551
+ "aria-label": playable ? `Play track ${track.title}` : `${track.title}: ${availabilityLabel(track.availability.state)}`,
1552
+ disabled: !playable,
1553
+ onClick: () => {
1554
+ playTrack(track);
1555
+ },
1556
+ type: "button",
1557
+ children: [
1558
+ /* @__PURE__ */ jsx("span", { className: "player-queue-number", children: String(track.trackNumber ?? index + 1).padStart(2, "0") }),
1559
+ /* @__PURE__ */ jsxs("span", { className: "player-content__entry-copy", children: [
1560
+ /* @__PURE__ */ jsx("strong", { children: track.title }),
1561
+ /* @__PURE__ */ jsx("small", { children: availabilityLabel(track.availability.state) })
1562
+ ] }),
1563
+ /* @__PURE__ */ jsx("time", { children: formatTime(track.durationHintSec ?? null) })
1564
+ ]
1565
+ }
1566
+ )
1567
+ },
1568
+ `${track.id}:${String(index)}`
1569
+ );
1570
+ }) })
1571
+ ]
1572
+ }
1573
+ ),
1574
+ /* @__PURE__ */ jsx(PlayerErrorNotice, { commands, state })
1575
+ ]
1576
+ }
1577
+ );
1578
+ }
1579
+ function PodcastProgramPlayer({
1580
+ appearance,
1581
+ className,
1582
+ commands,
1583
+ program,
1584
+ state
1585
+ }) {
1586
+ const build = useMemo(
1587
+ () => buildPodcastPlaybackContext(program),
1588
+ [program]
1589
+ );
1590
+ const playableIds = new Set(
1591
+ build.context.queue.map((entry) => entry.queueItemId)
1592
+ );
1593
+ const active = state.activeContext?.id === build.context.id;
1594
+ useIntrinsicContextGuard(commands, build.context, state);
1595
+ const currentEpisodeId = active && state.currentItem?.contentContext?.sourceType === "podcast-episode" ? state.currentItem.contentContext.episodeId : null;
1596
+ const currentEpisode = program.episodes.find((episode) => episode.id === currentEpisodeId) ?? null;
1597
+ const first = build.context.queue[0];
1598
+ const displayEpisode = currentEpisode ?? program.episodes.find(
1599
+ (episode) => episode.queueItem?.queueItemId === first?.queueItemId
1600
+ ) ?? null;
1601
+ const selectedEpisodeId = displayEpisode?.id ?? null;
1602
+ const displayItem = active ? state.currentItem : first?.item ?? null;
1603
+ const displayCurrentTimeSec = active ? state.currentTimeSec : 0;
1604
+ const displayDurationSec = active ? state.durationSec : displayEpisode?.durationHintSec ?? displayItem?.durationHintSec ?? null;
1605
+ const waveformProgress = displayDurationSec === null || displayDurationSec <= 0 ? 0 : displayCurrentTimeSec / displayDurationSec;
1606
+ const activeChapter = findActivePodcastChapter(
1607
+ displayEpisode?.chapters,
1608
+ displayCurrentTimeSec
1609
+ );
1610
+ const [pendingChapter, setPendingChapter] = useState(null);
1611
+ const [chaptersExpanded, setChaptersExpanded] = useState(false);
1612
+ useEffect(() => {
1613
+ setChaptersExpanded(false);
1614
+ }, [displayEpisode?.id]);
1615
+ useEffect(() => {
1616
+ if (pendingChapter !== null && state.currentQueueItemId === pendingChapter.queueItemId && state.canSeek) {
1617
+ commands.seekTo(pendingChapter.seconds);
1618
+ setPendingChapter(null);
1619
+ }
1620
+ }, [commands, pendingChapter, state.canSeek, state.currentQueueItemId]);
1621
+ const playEpisode = (episode) => {
1622
+ if (episode.queueItem === void 0 || !playableIds.has(episode.queueItem.queueItemId)) {
1623
+ return null;
1624
+ }
1625
+ return activateIntrinsicItem(
1626
+ commands,
1627
+ build.context,
1628
+ episode.queueItem.queueItemId
1629
+ );
1630
+ };
1631
+ const selectChapter = (episode, chapter) => {
1632
+ if (episode.queueItem === void 0 || !playableIds.has(episode.queueItem.queueItemId)) {
1633
+ return;
1634
+ }
1635
+ if (active && state.currentQueueItemId === episode.queueItem.queueItemId && state.canSeek) {
1636
+ commands.seekTo(chapter.startTimeSec);
1637
+ return;
1638
+ }
1639
+ setPendingChapter({
1640
+ queueItemId: episode.queueItem.queueItemId,
1641
+ seconds: chapter.startTimeSec
1642
+ });
1643
+ void playEpisode(episode)?.then((result) => {
1644
+ if (!result.ok) {
1645
+ setPendingChapter(null);
1646
+ }
1647
+ });
1648
+ };
1649
+ const backSeconds = state.experienceConfig.podcastBackSec;
1650
+ const forwardSeconds = state.experienceConfig.podcastForwardSec;
1651
+ const chapters = displayEpisode?.chapters ?? [];
1652
+ const visibleChapters = chaptersExpanded ? chapters : chapters.slice(0, 4);
1653
+ return /* @__PURE__ */ jsxs(
1654
+ PlayerSurface,
1655
+ {
1656
+ appearance,
1657
+ className: ["player-program", "player-content", className].filter(Boolean).join(" "),
1658
+ label: "Podcast program player",
1659
+ children: [
1660
+ /* @__PURE__ */ jsxs("header", { className: "player-program__hero", children: [
1661
+ /* @__PURE__ */ jsx(
1662
+ ContentArtwork,
1663
+ {
1664
+ alt: program.title,
1665
+ artwork: program.artwork,
1666
+ className: "player-program__artwork"
1667
+ }
1668
+ ),
1669
+ /* @__PURE__ */ jsxs("div", { className: "player-program__detail", children: [
1670
+ /* @__PURE__ */ jsx("span", { className: "player-kicker", children: "Podcast Program" }),
1671
+ /* @__PURE__ */ jsx("h2", { children: program.title }),
1672
+ program.description === void 0 ? null : /* @__PURE__ */ jsx("p", { className: "player-program__description", children: program.description }),
1673
+ program.hosts === void 0 && program.credits === void 0 ? null : /* @__PURE__ */ jsxs("dl", { className: "player-program__credits", children: [
1674
+ program.hosts === void 0 || program.hosts.length === 0 ? null : /* @__PURE__ */ jsxs("div", { children: [
1675
+ /* @__PURE__ */ jsx("dt", { children: "Host" }),
1676
+ /* @__PURE__ */ jsx("dd", { children: program.hosts.join(" / ") })
1677
+ ] }),
1678
+ program.credits === void 0 || program.credits.length === 0 ? null : /* @__PURE__ */ jsxs("div", { children: [
1679
+ /* @__PURE__ */ jsx("dt", { children: "Credits" }),
1680
+ /* @__PURE__ */ jsx("dd", { children: program.credits.join(" / ") })
1681
+ ] })
1682
+ ] })
1683
+ ] })
1684
+ ] }),
1685
+ /* @__PURE__ */ jsxs(
1686
+ "section",
1687
+ {
1688
+ "aria-label": "Current Podcast Episode",
1689
+ className: "player-program__episode",
1690
+ children: [
1691
+ /* @__PURE__ */ jsxs("header", { className: "player-program__episode-heading", children: [
1692
+ /* @__PURE__ */ jsx("span", { className: "player-kicker", children: "Current Episode" }),
1693
+ /* @__PURE__ */ jsx("h3", { children: displayEpisode === null ? "No playable Episodes" : displayEpisode.title }),
1694
+ displayEpisode === null ? null : /* @__PURE__ */ jsxs("dl", { className: "player-program__episode-facts", children: [
1695
+ displayEpisode.publishedAt === void 0 ? null : /* @__PURE__ */ jsxs("div", { children: [
1696
+ /* @__PURE__ */ jsx("dt", { children: "Published" }),
1697
+ /* @__PURE__ */ jsx("dd", { children: displayEpisode.publishedAt })
1698
+ ] }),
1699
+ /* @__PURE__ */ jsxs("div", { children: [
1700
+ /* @__PURE__ */ jsx("dt", { children: "Duration" }),
1701
+ /* @__PURE__ */ jsx("dd", { children: formatTime(displayDurationSec) })
1702
+ ] })
1703
+ ] }),
1704
+ displayEpisode?.description === void 0 ? null : /* @__PURE__ */ jsx("p", { children: displayEpisode.description }),
1705
+ active ? /* @__PURE__ */ jsx(PlayerStatus, { state }) : null
1706
+ ] }),
1707
+ /* @__PURE__ */ jsxs("div", { className: "player-program__playback", children: [
1708
+ /* @__PURE__ */ jsxs("div", { className: "player-program__signal", children: [
1709
+ /* @__PURE__ */ jsx(
1710
+ Waveform,
1711
+ {
1712
+ opacity: state.experienceConfig.waveformOpacity,
1713
+ playedStateEnabled: state.experienceConfig.waveformPlayedStateEnabled,
1714
+ progress: waveformProgress,
1715
+ waveform: displayItem?.waveform
1716
+ }
1717
+ ),
1718
+ /* @__PURE__ */ jsx(
1719
+ SeekBar,
1720
+ {
1721
+ canSeek: active && state.canSeek,
1722
+ commitMode: state.experienceConfig.seekCommitMode,
1723
+ currentTimeSec: displayCurrentTimeSec,
1724
+ durationSec: displayDurationSec,
1725
+ label: "Seek in current Podcast Episode",
1726
+ onSeek: (seconds) => {
1727
+ commands.seekTo(seconds);
1728
+ },
1729
+ status: active ? state.status : "paused"
1730
+ }
1731
+ ),
1732
+ /* @__PURE__ */ jsx(
1733
+ TimeReadout,
1734
+ {
1735
+ currentTimeSec: displayCurrentTimeSec,
1736
+ durationSec: displayDurationSec
1737
+ }
1738
+ )
1739
+ ] }),
1740
+ /* @__PURE__ */ jsxs("div", { className: "player-program__transport-row", children: [
1741
+ /* @__PURE__ */ jsx(
1742
+ IntrinsicTransport,
1743
+ {
1744
+ active,
1745
+ canStart: first !== void 0,
1746
+ commands,
1747
+ onStart: () => {
1748
+ if (first !== void 0) {
1749
+ void activateIntrinsicItem(
1750
+ commands,
1751
+ build.context,
1752
+ first.queueItemId
1753
+ );
1754
+ }
1755
+ },
1756
+ state
1757
+ }
1758
+ ),
1759
+ /* @__PURE__ */ jsxs(
1760
+ "div",
1761
+ {
1762
+ "aria-label": "Podcast seek controls",
1763
+ className: "player-program__skip-controls",
1764
+ role: "group",
1765
+ children: [
1766
+ /* @__PURE__ */ jsx(
1767
+ IconButton,
1768
+ {
1769
+ disabled: !active || !state.canSeek,
1770
+ label: `Skip back ${String(backSeconds)} seconds`,
1771
+ onClick: () => {
1772
+ commands.seekBy(-backSeconds);
1773
+ },
1774
+ children: /* @__PURE__ */ jsx(PodcastSkipBackIcon, {})
1775
+ }
1776
+ ),
1777
+ /* @__PURE__ */ jsx(
1778
+ IconButton,
1779
+ {
1780
+ disabled: !active || !state.canSeek,
1781
+ label: `Skip forward ${String(forwardSeconds)} seconds`,
1782
+ onClick: () => {
1783
+ commands.seekBy(forwardSeconds);
1784
+ },
1785
+ children: /* @__PURE__ */ jsx(PodcastSkipForwardIcon, {})
1786
+ }
1787
+ )
1788
+ ]
1789
+ }
1790
+ )
1791
+ ] })
1792
+ ] }),
1793
+ chapters.length === 0 || displayEpisode === null ? null : /* @__PURE__ */ jsxs("div", { className: "player-program__chapters", children: [
1794
+ /* @__PURE__ */ jsxs("div", { className: "player-section-heading", children: [
1795
+ /* @__PURE__ */ jsx("span", { id: `program-chapters-${program.id}`, children: "Chapters" }),
1796
+ /* @__PURE__ */ jsxs("span", { className: "player-queue-count", children: [
1797
+ String(chapters.length),
1798
+ " chapters"
1799
+ ] })
1800
+ ] }),
1801
+ /* @__PURE__ */ jsx("ol", { "aria-labelledby": `program-chapters-${program.id}`, children: visibleChapters.map((chapter) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
1802
+ "button",
1803
+ {
1804
+ "aria-current": chapter.id === activeChapter?.id ? "true" : void 0,
1805
+ onClick: () => {
1806
+ selectChapter(displayEpisode, chapter);
1807
+ },
1808
+ type: "button",
1809
+ children: [
1810
+ /* @__PURE__ */ jsx("time", { children: formatTime(chapter.startTimeSec) }),
1811
+ /* @__PURE__ */ jsx("span", { children: chapter.title })
1812
+ ]
1813
+ }
1814
+ ) }, chapter.id)) }),
1815
+ chapters.length <= 4 ? null : /* @__PURE__ */ jsx(
1816
+ "button",
1817
+ {
1818
+ className: "player-program__chapters-toggle",
1819
+ onClick: () => {
1820
+ setChaptersExpanded((expanded) => !expanded);
1821
+ },
1822
+ type: "button",
1823
+ children: chaptersExpanded ? "Show fewer chapters" : `Show all ${String(chapters.length)} chapters`
1824
+ }
1825
+ )
1826
+ ] })
1827
+ ]
1828
+ }
1829
+ ),
1830
+ /* @__PURE__ */ jsxs(
1831
+ "section",
1832
+ {
1833
+ "aria-labelledby": `program-episodes-${program.id}`,
1834
+ className: "player-program__episode-list",
1835
+ children: [
1836
+ /* @__PURE__ */ jsxs("div", { className: "player-section-heading", children: [
1837
+ /* @__PURE__ */ jsx("span", { id: `program-episodes-${program.id}`, children: "Episodes" }),
1838
+ /* @__PURE__ */ jsxs("span", { className: "player-queue-count", children: [
1839
+ String(program.episodes.length),
1840
+ " episodes"
1841
+ ] })
1842
+ ] }),
1843
+ program.episodes.length === 0 ? /* @__PURE__ */ jsx("p", { className: "player-empty", children: "No Episodes are available." }) : /* @__PURE__ */ jsx("ol", { className: "player-content__list player-content__episodes", children: program.episodes.map((episode, index) => {
1844
+ const playable = episode.queueItem !== void 0 && playableIds.has(episode.queueItem.queueItemId);
1845
+ const isCurrent = episode.id === selectedEpisodeId;
1846
+ return /* @__PURE__ */ jsx(
1847
+ "li",
1848
+ {
1849
+ "data-availability": episode.availability.state,
1850
+ "data-current": isCurrent ? "true" : "false",
1851
+ children: /* @__PURE__ */ jsxs(
1852
+ "button",
1853
+ {
1854
+ "aria-current": isCurrent ? "true" : void 0,
1855
+ "aria-label": playable ? `Play episode ${episode.title}` : `${episode.title}: ${availabilityLabel(episode.availability.state)}`,
1856
+ disabled: !playable,
1857
+ onClick: () => {
1858
+ void playEpisode(episode);
1859
+ },
1860
+ type: "button",
1861
+ children: [
1862
+ /* @__PURE__ */ jsx("span", { className: "player-queue-number", children: String(index + 1).padStart(2, "0") }),
1863
+ /* @__PURE__ */ jsxs("span", { className: "player-content__entry-copy", children: [
1864
+ /* @__PURE__ */ jsx("strong", { children: episode.title }),
1865
+ /* @__PURE__ */ jsx("small", { children: availabilityLabel(episode.availability.state) })
1866
+ ] }),
1867
+ /* @__PURE__ */ jsx("time", { children: formatTime(episode.durationHintSec ?? null) })
1868
+ ]
1869
+ }
1870
+ )
1871
+ },
1872
+ `${episode.id}:${String(index)}`
1873
+ );
1874
+ }) })
1875
+ ]
1876
+ }
1877
+ ),
1878
+ /* @__PURE__ */ jsx(PlayerErrorNotice, { commands, state })
1879
+ ]
1880
+ }
1881
+ );
1882
+ }
1137
1883
  function MiniPlayer({
1138
1884
  appearance,
1139
1885
  className,
@@ -1143,17 +1889,23 @@ function MiniPlayer({
1143
1889
  if (state.currentItem === null) {
1144
1890
  return null;
1145
1891
  }
1892
+ const contentContext = state.currentItem.contentContext;
1146
1893
  return /* @__PURE__ */ jsxs(
1147
1894
  PlayerSurface,
1148
1895
  {
1149
1896
  appearance,
1150
1897
  className: ["player-mini", className].filter(Boolean).join(" "),
1151
- label: "Mini music player",
1898
+ label: contentContext?.sourceType === "podcast-episode" ? "Podcast mini player" : "Mini music player",
1152
1899
  children: [
1153
1900
  /* @__PURE__ */ jsxs(
1154
1901
  "div",
1155
1902
  {
1156
1903
  className: "player-mini__main",
1904
+ "data-content-source": contentContext?.sourceType,
1905
+ "data-episode-id": contentContext?.sourceType === "podcast-episode" ? contentContext.episodeId : void 0,
1906
+ "data-program-id": contentContext?.sourceType === "podcast-episode" ? contentContext.programId : void 0,
1907
+ "data-release-id": contentContext?.sourceType === "release-track" ? contentContext.releaseId : void 0,
1908
+ "data-track-id": contentContext?.sourceType === "release-track" ? contentContext.trackId : void 0,
1157
1909
  "data-playback-context-id": state.activeContext?.id,
1158
1910
  "data-playback-context-mode": state.activeContext?.mode,
1159
1911
  children: [
@@ -1476,6 +2228,6 @@ function WidePlayer({
1476
2228
  );
1477
2229
  }
1478
2230
 
1479
- export { Artwork, BoundCompactPlayerView, CompactPlayer, DetailedPlayer, IconButton, MiniPlayer, PlayerErrorNotice, PlayerStatus, PlayerSurface, PodcastPlayer, PopoverVolumeControl, PrimaryPlaybackButton, QueuePanel, QueueToggle, SeekBar, TimeReadout, TrackMetadata, TransportControls, VolumeControl, Waveform, WidePlayer, clamp, formatTime, formatTimeAria, getErrorMessage, getNextRepeatMode, getRepeatLabel, getStatusLabel };
2231
+ export { Artwork, BoundCompactPlayerView, CompactPlayer, DetailedPlayer, IconButton, MiniPlayer, PlayerErrorNotice, PlayerStatus, PlayerSurface, PodcastPlayer, PodcastProgramPlayer, PopoverVolumeControl, PrimaryPlaybackButton, QueuePanel, QueueToggle, ReleasePlayer, SeekBar, TimeReadout, TrackMetadata, TransportControls, VolumeControl, Waveform, WidePlayer, clamp, formatTime, formatTimeAria, getErrorMessage, getNextRepeatMode, getRepeatLabel, getStatusLabel };
1480
2232
  //# sourceMappingURL=index.js.map
1481
2233
  //# sourceMappingURL=index.js.map