@livo-tv/sdk 1.16.0 → 1.18.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/README.md CHANGED
@@ -79,7 +79,7 @@ Wrap the studio in `StudioWindowGate` with a **stable** `lockKey` (stream id or
79
79
 
80
80
  Omit `playerOrigin` in native embeds. The watch-share control only appears when you pass a player origin; native partners should share their own watch URL.
81
81
 
82
- `endedHref` is the link on the host's ended-session action. When it is omitted or blank, that action goes to `/streams/:id`.
82
+ `endedHref` is the link on the host's ended-session action. When it is omitted or blank, that action goes to `/streams/:id`. Optional `endedExtra` renders under that action. Omit it and the card stays the same.
83
83
 
84
84
  Regression page: `pnpm example:embed` serves `examples/partner-embed` on `http://localhost:3003`.
85
85
 
@@ -11595,6 +11595,7 @@ function EmptyState({
11595
11595
  actionLabel,
11596
11596
  onAction,
11597
11597
  actionHref,
11598
+ extra,
11598
11599
  role = "status"
11599
11600
  }) {
11600
11601
  const action = actionLabel && actionHref ? /* @__PURE__ */ jsxRuntime.jsx(Button3, { render: /* @__PURE__ */ jsxRuntime.jsx("a", { href: actionHref }), children: actionLabel }) : actionLabel ? /* @__PURE__ */ jsxRuntime.jsx(Button3, { type: "button", onClick: onAction, children: actionLabel }) : null;
@@ -11606,7 +11607,8 @@ function EmptyState({
11606
11607
  children: [
11607
11608
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold", children: title }),
11608
11609
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground max-w-md text-sm", children: description }),
11609
- action
11610
+ action,
11611
+ extra
11610
11612
  ]
11611
11613
  }
11612
11614
  );
@@ -11677,21 +11679,18 @@ function pipSpotlightFrom(tile, kind) {
11677
11679
  }
11678
11680
  function resolveStudioPipSpotlight(tiles, activeSpeakerId) {
11679
11681
  const visible = tiles.filter((tile) => isOnStage(tile.stageStatus));
11682
+ const sharing = visible.find((tile) => tile.screenShareEnabled);
11683
+ if (sharing) {
11684
+ return pipSpotlightFrom(sharing, "screen");
11685
+ }
11680
11686
  const pinned = visible.find((tile) => tile.pinned === true);
11681
11687
  if (pinned) {
11682
- return pipSpotlightFrom(
11683
- pinned,
11684
- pinned.screenShareEnabled ? "screen" : pinned.videoEnabled ? "camera" : "idle"
11685
- );
11688
+ return pipSpotlightFrom(pinned, pinned.videoEnabled ? "camera" : "idle");
11686
11689
  }
11687
11690
  const speaker = activeSpeakerId ? visible.find((tile) => tile.id === activeSpeakerId) : void 0;
11688
11691
  if (speaker) {
11689
11692
  return pipSpotlightFrom(speaker, speaker.videoEnabled ? "camera" : "idle");
11690
11693
  }
11691
- const sharing = visible.find((tile) => tile.screenShareEnabled);
11692
- if (sharing) {
11693
- return pipSpotlightFrom(sharing, "screen");
11694
- }
11695
11694
  const remoteCam = visible.find((tile) => !tile.isSelf && tile.videoEnabled);
11696
11695
  if (remoteCam) {
11697
11696
  return pipSpotlightFrom(remoteCam, "camera");
@@ -22856,6 +22855,7 @@ function StudioDeviceSelectGroup({
22856
22855
  devices,
22857
22856
  selected,
22858
22857
  labels,
22858
+ disabled: disabled2 = false,
22859
22859
  onChange,
22860
22860
  speakerAction
22861
22861
  }) {
@@ -22867,6 +22867,7 @@ function StudioDeviceSelectGroup({
22867
22867
  label: labels.cameraDevice,
22868
22868
  options: devices.video,
22869
22869
  value: selected.video,
22870
+ disabled: disabled2,
22870
22871
  onChange: (id) => onChange("video", id)
22871
22872
  }
22872
22873
  ),
@@ -22877,6 +22878,7 @@ function StudioDeviceSelectGroup({
22877
22878
  label: labels.micDevice,
22878
22879
  options: devices.audio,
22879
22880
  value: selected.audio,
22881
+ disabled: disabled2,
22880
22882
  onChange: (id) => onChange("audio", id)
22881
22883
  }
22882
22884
  ),
@@ -22887,6 +22889,7 @@ function StudioDeviceSelectGroup({
22887
22889
  label: labels.speakerDevice,
22888
22890
  options: devices.speaker,
22889
22891
  value: selected.speaker,
22892
+ disabled: disabled2,
22890
22893
  onChange: (id) => onChange("speaker", id),
22891
22894
  action: speakerAction
22892
22895
  }
@@ -22898,6 +22901,7 @@ function DeviceRow({
22898
22901
  label,
22899
22902
  options,
22900
22903
  value,
22904
+ disabled: disabled2 = false,
22901
22905
  onChange,
22902
22906
  action
22903
22907
  }) {
@@ -22916,6 +22920,7 @@ function DeviceRow({
22916
22920
  "border-input bg-background h-8 min-w-0 flex-1 rounded-md border px-2 text-base md:text-sm"
22917
22921
  ),
22918
22922
  value: display,
22923
+ disabled: disabled2,
22919
22924
  onChange: (event) => onChange(event.target.value),
22920
22925
  children: options.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.deviceId, children: option.label }, option.deviceId))
22921
22926
  }
@@ -28566,24 +28571,35 @@ function StudioToolbar({
28566
28571
  children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(Eye, {})
28567
28572
  }
28568
28573
  ) : null,
28569
- showStageGuest ? /* @__PURE__ */ jsxRuntime.jsx(
28574
+ showStageGuest ? selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(
28575
+ ConfirmAction,
28576
+ {
28577
+ triggerLabel: labels.moveToAudience,
28578
+ title: labels.moveToAudienceTitle,
28579
+ description: labels.moveToAudienceDescription,
28580
+ confirmLabel: labels.moveToAudienceConfirm,
28581
+ pending,
28582
+ onConfirm: onLeaveStage,
28583
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(
28584
+ Button3,
28585
+ {
28586
+ type: "button",
28587
+ size: "icon-lg",
28588
+ variant: "secondary",
28589
+ disabled: pending || !canLeaveStage,
28590
+ "aria-label": labels.moveToAudience,
28591
+ children: /* @__PURE__ */ jsxRuntime.jsx(Users, {})
28592
+ }
28593
+ )
28594
+ }
28595
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
28570
28596
  ControlIcon,
28571
28597
  {
28572
- label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
28598
+ label: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
28573
28599
  pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
28574
- disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
28575
- onClick: () => {
28576
- if (selfStageStatus === "ON_STAGE") {
28577
- if (!canLeaveStage) {
28578
- toast.message(labels.offAirLastVisible);
28579
- return;
28580
- }
28581
- onLeaveStage();
28582
- return;
28583
- }
28584
- onAskStage();
28585
- },
28586
- children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(Eye, {})
28600
+ disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
28601
+ onClick: onAskStage,
28602
+ children: /* @__PURE__ */ jsxRuntime.jsx(Eye, {})
28587
28603
  }
28588
28604
  ) : null,
28589
28605
  showScreenControl ? canShareFreely ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -28915,7 +28931,8 @@ function StudioRoom({
28915
28931
  captionsBaseUrl,
28916
28932
  apiUrl,
28917
28933
  cdnBase,
28918
- endedHref
28934
+ endedHref,
28935
+ endedExtra
28919
28936
  }) {
28920
28937
  const [cameraOn, setCameraOn] = React18.useState(initialCameraOn);
28921
28938
  const [screenOn, setScreenOn] = React18.useState(false);
@@ -29627,6 +29644,7 @@ function StudioRoom({
29627
29644
  try {
29628
29645
  await meeting.leave();
29629
29646
  setLeaveState("left");
29647
+ } catch {
29630
29648
  } finally {
29631
29649
  setPending(false);
29632
29650
  }
@@ -29647,6 +29665,7 @@ function StudioRoom({
29647
29665
  }
29648
29666
  if (sessionOver) {
29649
29667
  const canRejoin = session.role === "moderator" && onRejoin != null && isStudioActiveStatus(session.stream.status);
29668
+ const returnHref = canRejoin || session.role !== "moderator" ? void 0 : endedHref?.trim() || `/streams/${session.stream.id}`;
29650
29669
  return withStudioToaster(
29651
29670
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-dvh flex-col items-center justify-center p-6", children: /* @__PURE__ */ jsxRuntime.jsx(
29652
29671
  EmptyState,
@@ -29655,7 +29674,8 @@ function StudioRoom({
29655
29674
  description: labels.endedDescription,
29656
29675
  actionLabel: canRejoin ? labels.rejoin : session.role === "moderator" ? labels.endedAction : void 0,
29657
29676
  onAction: canRejoin ? onRejoin : void 0,
29658
- actionHref: canRejoin || session.role !== "moderator" ? void 0 : endedHref?.trim() || `/streams/${session.stream.id}`
29677
+ actionHref: returnHref,
29678
+ extra: returnHref ? endedExtra : void 0
29659
29679
  }
29660
29680
  ) })
29661
29681
  );
@@ -30263,7 +30283,8 @@ var LOBBY_DEVICE_LABEL_DEFAULTS = {
30263
30283
  speakerDevice: "Speaker",
30264
30284
  testSpeaker: "Test",
30265
30285
  testSpeakerError: "Could not play the speaker test",
30266
- cameraOff: "Camera is off"
30286
+ cameraOff: "Camera is off",
30287
+ joining: "Joining\u2026"
30267
30288
  };
30268
30289
  function StudioGuestLobby({
30269
30290
  reducedMotion,
@@ -30298,7 +30319,9 @@ function StudioGuestLobby({
30298
30319
  testSpeakerError: labels.testSpeakerError ?? LOBBY_DEVICE_LABEL_DEFAULTS.testSpeakerError,
30299
30320
  cameraOff: labels.cameraOff ?? LOBBY_DEVICE_LABEL_DEFAULTS.cameraOff
30300
30321
  };
30322
+ const joiningLabel = labels.joining?.trim() || LOBBY_DEVICE_LABEL_DEFAULTS.joining;
30301
30323
  function changeDevice(kind, deviceId) {
30324
+ if (joining) return;
30302
30325
  persistStudioDeviceSelection(kind, deviceId);
30303
30326
  onDeviceChange?.(kind, deviceId);
30304
30327
  if (!savedDevices) {
@@ -30306,7 +30329,7 @@ function StudioGuestLobby({
30306
30329
  }
30307
30330
  }
30308
30331
  async function onTestSpeaker() {
30309
- if (testingSpeaker) return;
30332
+ if (testingSpeaker || joining) return;
30310
30333
  const speakerId = effective.speaker || devices.speaker[0]?.deviceId || void 0;
30311
30334
  setTestingSpeaker(true);
30312
30335
  try {
@@ -30345,6 +30368,7 @@ function StudioGuestLobby({
30345
30368
  size: "icon-lg",
30346
30369
  "aria-pressed": muted,
30347
30370
  "aria-label": labels.mic,
30371
+ disabled: joining,
30348
30372
  onClick: () => onMutedChange(!muted),
30349
30373
  children: muted ? /* @__PURE__ */ jsxRuntime.jsx(MicOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(Mic, {})
30350
30374
  }
@@ -30357,6 +30381,7 @@ function StudioGuestLobby({
30357
30381
  size: "icon-lg",
30358
30382
  "aria-pressed": cameraOn,
30359
30383
  "aria-label": labels.camera,
30384
+ disabled: joining,
30360
30385
  onClick: () => onCameraChange(!cameraOn),
30361
30386
  children: cameraOn ? /* @__PURE__ */ jsxRuntime.jsx(Video, {}) : /* @__PURE__ */ jsxRuntime.jsx(VideoOff, {})
30362
30387
  }
@@ -30368,6 +30393,7 @@ function StudioGuestLobby({
30368
30393
  devices,
30369
30394
  selected: effective,
30370
30395
  labels: deviceLabels,
30396
+ disabled: joining,
30371
30397
  onChange: changeDevice,
30372
30398
  speakerAction: /* @__PURE__ */ jsxRuntime.jsx(
30373
30399
  Button3,
@@ -30375,7 +30401,7 @@ function StudioGuestLobby({
30375
30401
  type: "button",
30376
30402
  size: "sm",
30377
30403
  variant: "outline",
30378
- disabled: testingSpeaker,
30404
+ disabled: testingSpeaker || joining,
30379
30405
  onClick: () => void onTestSpeaker(),
30380
30406
  children: deviceLabels.testSpeaker
30381
30407
  }
@@ -30393,6 +30419,7 @@ function StudioGuestLobby({
30393
30419
  onChange: (event) => onDisplayNameChange(event.target.value),
30394
30420
  maxLength: 80,
30395
30421
  required: true,
30422
+ disabled: joining,
30396
30423
  autoComplete: "name"
30397
30424
  }
30398
30425
  )
@@ -30406,8 +30433,18 @@ function StudioGuestLobby({
30406
30433
  size: "lg",
30407
30434
  className: "w-full",
30408
30435
  disabled: joining || showName && !displayName.trim(),
30436
+ "aria-busy": joining,
30409
30437
  onClick: onJoin,
30410
- children: waitingForHost ? labels.waitingForHost : labels.join
30438
+ children: joining ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
30439
+ /* @__PURE__ */ jsxRuntime.jsx(
30440
+ LoaderCircle,
30441
+ {
30442
+ "aria-hidden": true,
30443
+ className: "animate-spin motion-reduce:animate-none"
30444
+ }
30445
+ ),
30446
+ joiningLabel
30447
+ ] }) : waitingForHost ? labels.waitingForHost : labels.join
30411
30448
  }
30412
30449
  )
30413
30450
  ] });
@@ -30722,6 +30759,7 @@ function studioMeetingFromRtk(raw) {
30722
30759
  const chatSend = /* @__PURE__ */ new Map();
30723
30760
  const nameOverrides = /* @__PURE__ */ new Map();
30724
30761
  let joinStageInFlight = null;
30762
+ let leaveInProgress = false;
30725
30763
  let joinedStageLocally = false;
30726
30764
  let stageGrantPending = false;
30727
30765
  let stageGrantJoinFailures = 0;
@@ -30835,6 +30873,7 @@ function studioMeetingFromRtk(raw) {
30835
30873
  for (const listener of stageGrantListeners) listener(userId);
30836
30874
  }
30837
30875
  function markStageGranted() {
30876
+ if (leaveInProgress) return;
30838
30877
  stageGrantPending = true;
30839
30878
  void ensureStageJoined();
30840
30879
  }
@@ -30882,6 +30921,7 @@ function studioMeetingFromRtk(raw) {
30882
30921
  async function runJoinStageAttempts() {
30883
30922
  let lastError;
30884
30923
  for (let attempt = 0; attempt < STAGE_JOIN_MAX_ATTEMPTS; attempt++) {
30924
+ if (leaveInProgress) return;
30885
30925
  if (currentStageStatus() === "ON_STAGE") {
30886
30926
  notifyStage();
30887
30927
  return;
@@ -30930,6 +30970,7 @@ function studioMeetingFromRtk(raw) {
30930
30970
  }
30931
30971
  }
30932
30972
  async function ensureStageJoined() {
30973
+ if (leaveInProgress) return;
30933
30974
  if (currentStageStatus() === "ON_STAGE") {
30934
30975
  clearStageGrantPending();
30935
30976
  return;
@@ -31066,10 +31107,29 @@ function studioMeetingFromRtk(raw) {
31066
31107
  await applyCurrentVideoEffects();
31067
31108
  },
31068
31109
  async leave() {
31069
- if (typeof window !== "undefined") {
31070
- window.removeEventListener("unhandledrejection", onUnhandledRejection);
31110
+ leaveInProgress = true;
31111
+ clearStageGrantPending();
31112
+ try {
31113
+ if (typeof window !== "undefined") {
31114
+ window.removeEventListener(
31115
+ "unhandledrejection",
31116
+ onUnhandledRejection
31117
+ );
31118
+ }
31119
+ let firstFailed = false;
31120
+ try {
31121
+ await meeting.leave?.();
31122
+ } catch {
31123
+ firstFailed = true;
31124
+ }
31125
+ const roomState = meeting.self?.roomState ?? null;
31126
+ if (firstFailed || roomState === "joined" || roomState === "stageLeft") {
31127
+ await meeting.leave?.();
31128
+ }
31129
+ } finally {
31130
+ clearStageGrantPending();
31131
+ leaveInProgress = false;
31071
31132
  }
31072
- await meeting.leave?.();
31073
31133
  },
31074
31134
  async setAudioEnabled(enabled) {
31075
31135
  if (enabled) await meeting.self?.enableAudio?.();
@@ -31682,7 +31742,8 @@ function LivoHostStudio({
31682
31742
  theme,
31683
31743
  appDownloadUrl,
31684
31744
  guestUrl,
31685
- endedHref
31745
+ endedHref,
31746
+ endedExtra
31686
31747
  }) {
31687
31748
  const reducedMotion = usePrefersReducedMotion();
31688
31749
  const [session, setSession] = React18.useState(null);
@@ -31804,7 +31865,8 @@ function LivoHostStudio({
31804
31865
  playerOrigin,
31805
31866
  appDownloadUrl,
31806
31867
  apiUrl,
31807
- endedHref
31868
+ endedHref,
31869
+ endedExtra
31808
31870
  }
31809
31871
  ) });
31810
31872
  }
@@ -31844,6 +31906,9 @@ function LivoGuestStudio({
31844
31906
  session?.authToken ?? null,
31845
31907
  sessionGen
31846
31908
  );
31909
+ React18.useEffect(() => {
31910
+ if (meeting) setJoining(false);
31911
+ }, [meeting]);
31847
31912
  React18.useEffect(() => {
31848
31913
  const identity = readStudioGuestIdentity();
31849
31914
  setDisplayName(identity.displayName);
@@ -31876,6 +31941,7 @@ function LivoGuestStudio({
31876
31941
  setStatus(null);
31877
31942
  setSession(next);
31878
31943
  } catch (err) {
31944
+ setJoining(false);
31879
31945
  const code = studioErrorCode(err);
31880
31946
  if (code === "studio_not_started") {
31881
31947
  setWaitingForHost(true);
@@ -31892,7 +31958,6 @@ function LivoGuestStudio({
31892
31958
  }
31893
31959
  } finally {
31894
31960
  joinInFlight.current = false;
31895
- setJoining(false);
31896
31961
  }
31897
31962
  }, [apiUrl, displayName, guestId, lobbyLabels, token]);
31898
31963
  React18.useEffect(() => {
@@ -32154,6 +32219,10 @@ function studioRoomLabels(t) {
32154
32219
  shareRequestTimeout: t("shareRequestTimeout"),
32155
32220
  shareRespondError: t("shareRespondError"),
32156
32221
  goOffAir: t("goOffAir"),
32222
+ moveToAudience: t("moveToAudience"),
32223
+ moveToAudienceTitle: t("moveToAudienceTitle"),
32224
+ moveToAudienceDescription: t("moveToAudienceDescription"),
32225
+ moveToAudienceConfirm: t("moveToAudienceConfirm"),
32157
32226
  goOnAir: t("goOnAir"),
32158
32227
  takeOffAir: t("takeOffAir"),
32159
32228
  bringOnAir: t("bringOnAir"),