@livo-tv/sdk 1.4.0 → 1.5.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.
@@ -316,6 +316,9 @@ declare function studioRoomLabels(t: (key: string, values?: Record<string, strin
316
316
  pipTitle: string;
317
317
  pipError: string;
318
318
  screenUnsupportedDevice: string;
319
+ screenDownloadAppTitle: string;
320
+ screenDownloadAppAction: string;
321
+ screenDownloadAppDismiss: string;
319
322
  screenNotAllowed: string;
320
323
  askToShare: string;
321
324
  shareRequested: string;
@@ -393,7 +396,7 @@ type StudioDeviceSelection = {
393
396
  speaker: string;
394
397
  };
395
398
 
396
- declare function StudioRoom({ session, meeting, control, onRefreshStatus, onRejoin, labels, reducedMotion, embed, chrome, playerOrigin, windowGate, initialCameraOn, initialMuted, initialDevices, }: {
399
+ declare function StudioRoom({ session, meeting, control, onRefreshStatus, onRejoin, labels, reducedMotion, embed, chrome, playerOrigin, windowGate, initialCameraOn, initialMuted, initialDevices, appDownloadUrl, }: {
397
400
  session: StudioSession;
398
401
  meeting: StudioMeetingHandle;
399
402
  control: StudioControlActions;
@@ -410,6 +413,7 @@ declare function StudioRoom({ session, meeting, control, onRefreshStatus, onRejo
410
413
  initialCameraOn?: boolean;
411
414
  initialMuted?: boolean;
412
415
  initialDevices?: StudioDeviceSelection;
416
+ appDownloadUrl?: string;
413
417
  }): react.JSX.Element;
414
418
 
415
419
  declare function StudioSkeleton(): react.JSX.Element;
@@ -465,7 +469,7 @@ declare function StudioWindowGate({ lockKey, allowTakeover, labels, children, }:
465
469
  children: ReactNode;
466
470
  }): react.JSX.Element;
467
471
 
468
- declare function LivoHostStudio({ token, apiUrl, labels, redeemError, embed, chrome, playerOrigin, theme, }: {
472
+ declare function LivoHostStudio({ token, apiUrl, labels, redeemError, embed, chrome, playerOrigin, theme, appDownloadUrl, }: {
469
473
  token: string;
470
474
  apiUrl?: string;
471
475
  labels: StudioRoomLabels;
@@ -474,6 +478,7 @@ declare function LivoHostStudio({ token, apiUrl, labels, redeemError, embed, chr
474
478
  chrome?: ReactNode;
475
479
  playerOrigin?: string;
476
480
  theme?: string | null;
481
+ appDownloadUrl?: string;
477
482
  }): react.JSX.Element;
478
483
 
479
484
  type StudioLobbyLabels = {
@@ -494,7 +499,7 @@ type StudioLobbyLabels = {
494
499
  joinBusy: string;
495
500
  endedTitle: string;
496
501
  };
497
- declare function LivoGuestStudio({ token, apiUrl, labels, lobbyLabels, embed, playerOrigin, theme, }: {
502
+ declare function LivoGuestStudio({ token, apiUrl, labels, lobbyLabels, embed, playerOrigin, theme, appDownloadUrl, }: {
498
503
  token: string;
499
504
  apiUrl?: string;
500
505
  labels: StudioRoomLabels;
@@ -502,6 +507,7 @@ declare function LivoGuestStudio({ token, apiUrl, labels, lobbyLabels, embed, pl
502
507
  embed?: boolean;
503
508
  playerOrigin?: string;
504
509
  theme?: string | null;
510
+ appDownloadUrl?: string;
505
511
  }): react.JSX.Element;
506
512
 
507
513
  declare function useRealtimeKitMeeting(authToken: string | null, generation?: number): {
@@ -316,6 +316,9 @@ declare function studioRoomLabels(t: (key: string, values?: Record<string, strin
316
316
  pipTitle: string;
317
317
  pipError: string;
318
318
  screenUnsupportedDevice: string;
319
+ screenDownloadAppTitle: string;
320
+ screenDownloadAppAction: string;
321
+ screenDownloadAppDismiss: string;
319
322
  screenNotAllowed: string;
320
323
  askToShare: string;
321
324
  shareRequested: string;
@@ -393,7 +396,7 @@ type StudioDeviceSelection = {
393
396
  speaker: string;
394
397
  };
395
398
 
396
- declare function StudioRoom({ session, meeting, control, onRefreshStatus, onRejoin, labels, reducedMotion, embed, chrome, playerOrigin, windowGate, initialCameraOn, initialMuted, initialDevices, }: {
399
+ declare function StudioRoom({ session, meeting, control, onRefreshStatus, onRejoin, labels, reducedMotion, embed, chrome, playerOrigin, windowGate, initialCameraOn, initialMuted, initialDevices, appDownloadUrl, }: {
397
400
  session: StudioSession;
398
401
  meeting: StudioMeetingHandle;
399
402
  control: StudioControlActions;
@@ -410,6 +413,7 @@ declare function StudioRoom({ session, meeting, control, onRefreshStatus, onRejo
410
413
  initialCameraOn?: boolean;
411
414
  initialMuted?: boolean;
412
415
  initialDevices?: StudioDeviceSelection;
416
+ appDownloadUrl?: string;
413
417
  }): react.JSX.Element;
414
418
 
415
419
  declare function StudioSkeleton(): react.JSX.Element;
@@ -465,7 +469,7 @@ declare function StudioWindowGate({ lockKey, allowTakeover, labels, children, }:
465
469
  children: ReactNode;
466
470
  }): react.JSX.Element;
467
471
 
468
- declare function LivoHostStudio({ token, apiUrl, labels, redeemError, embed, chrome, playerOrigin, theme, }: {
472
+ declare function LivoHostStudio({ token, apiUrl, labels, redeemError, embed, chrome, playerOrigin, theme, appDownloadUrl, }: {
469
473
  token: string;
470
474
  apiUrl?: string;
471
475
  labels: StudioRoomLabels;
@@ -474,6 +478,7 @@ declare function LivoHostStudio({ token, apiUrl, labels, redeemError, embed, chr
474
478
  chrome?: ReactNode;
475
479
  playerOrigin?: string;
476
480
  theme?: string | null;
481
+ appDownloadUrl?: string;
477
482
  }): react.JSX.Element;
478
483
 
479
484
  type StudioLobbyLabels = {
@@ -494,7 +499,7 @@ type StudioLobbyLabels = {
494
499
  joinBusy: string;
495
500
  endedTitle: string;
496
501
  };
497
- declare function LivoGuestStudio({ token, apiUrl, labels, lobbyLabels, embed, playerOrigin, theme, }: {
502
+ declare function LivoGuestStudio({ token, apiUrl, labels, lobbyLabels, embed, playerOrigin, theme, appDownloadUrl, }: {
498
503
  token: string;
499
504
  apiUrl?: string;
500
505
  labels: StudioRoomLabels;
@@ -502,6 +507,7 @@ declare function LivoGuestStudio({ token, apiUrl, labels, lobbyLabels, embed, pl
502
507
  embed?: boolean;
503
508
  playerOrigin?: string;
504
509
  theme?: string | null;
510
+ appDownloadUrl?: string;
505
511
  }): react.JSX.Element;
506
512
 
507
513
  declare function useRealtimeKitMeeting(authToken: string | null, generation?: number): {
@@ -518,52 +518,38 @@ function canTakeOffAir(tiles, participantId, onAir) {
518
518
  (tile) => tile.id !== participantId && isOnStage(tile.stageStatus)
519
519
  );
520
520
  }
521
+ function pipSpotlightFrom(tile, kind) {
522
+ return {
523
+ participantId: tile.id,
524
+ name: tile.name,
525
+ kind,
526
+ picture: tile.picture
527
+ };
528
+ }
521
529
  function resolveStudioPipSpotlight(tiles, activeSpeakerId) {
522
530
  const visible = tiles.filter((tile) => isOnStage(tile.stageStatus));
523
531
  const pinned = visible.find((tile) => tile.pinned === true);
524
532
  if (pinned) {
525
- return {
526
- participantId: pinned.id,
527
- name: pinned.name,
528
- kind: pinned.screenShareEnabled ? "screen" : pinned.videoEnabled ? "camera" : "idle",
529
- picture: pinned.picture
530
- };
533
+ return pipSpotlightFrom(
534
+ pinned,
535
+ pinned.screenShareEnabled ? "screen" : pinned.videoEnabled ? "camera" : "idle"
536
+ );
537
+ }
538
+ const speaker = activeSpeakerId ? visible.find((tile) => tile.id === activeSpeakerId) : void 0;
539
+ if (speaker) {
540
+ return pipSpotlightFrom(speaker, speaker.videoEnabled ? "camera" : "idle");
531
541
  }
532
542
  const sharing = visible.find((tile) => tile.screenShareEnabled);
533
543
  if (sharing) {
534
- return {
535
- participantId: sharing.id,
536
- name: sharing.name,
537
- kind: "screen",
538
- picture: sharing.picture
539
- };
540
- }
541
- const speaker = activeSpeakerId ? visible.find((tile) => tile.id === activeSpeakerId && tile.videoEnabled) : void 0;
542
- if (speaker) {
543
- return {
544
- participantId: speaker.id,
545
- name: speaker.name,
546
- kind: "camera",
547
- picture: speaker.picture
548
- };
544
+ return pipSpotlightFrom(sharing, "screen");
549
545
  }
550
546
  const remoteCam = visible.find((tile) => !tile.isSelf && tile.videoEnabled);
551
547
  if (remoteCam) {
552
- return {
553
- participantId: remoteCam.id,
554
- name: remoteCam.name,
555
- kind: "camera",
556
- picture: remoteCam.picture
557
- };
548
+ return pipSpotlightFrom(remoteCam, "camera");
558
549
  }
559
550
  const self = visible.find((tile) => tile.isSelf);
560
551
  if (!self) return null;
561
- return {
562
- participantId: self.id,
563
- name: self.name,
564
- kind: self.videoEnabled ? "camera" : "idle",
565
- picture: self.picture
566
- };
552
+ return pipSpotlightFrom(self, self.videoEnabled ? "camera" : "idle");
567
553
  }
568
554
  function resolveStudioPipSelfView(tiles, spotlight) {
569
555
  const self = tiles.find((tile) => tile.isSelf);
@@ -682,6 +668,9 @@ function screenShareBlock(captureSupported, permission) {
682
668
  if (permission === "NOT_ALLOWED") return "permission";
683
669
  return null;
684
670
  }
671
+ function showScreenShareControl(block) {
672
+ return block !== "permission";
673
+ }
685
674
 
686
675
  // src/studio/lib/parse.ts
687
676
  function isRecord(value) {
@@ -1464,6 +1453,19 @@ function DialogHeader({ className, ...props }) {
1464
1453
  }
1465
1454
  );
1466
1455
  }
1456
+ function DialogFooter({ className, ...props }) {
1457
+ return /* @__PURE__ */ jsx(
1458
+ "div",
1459
+ {
1460
+ "data-slot": "dialog-footer",
1461
+ className: cn(
1462
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1463
+ className
1464
+ ),
1465
+ ...props
1466
+ }
1467
+ );
1468
+ }
1467
1469
  function DialogTitle({
1468
1470
  className,
1469
1471
  ...props
@@ -2023,6 +2025,19 @@ var badgeVariants = cva(
2023
2025
  function Badge({ className, variant, ...props }) {
2024
2026
  return /* @__PURE__ */ jsx("span", { className: cn(badgeVariants({ variant }), className), ...props });
2025
2027
  }
2028
+
2029
+ // src/studio/lib/studio-video-style.ts
2030
+ var STUDIO_VIDEO_MEDIA_STYLE = `
2031
+ .livo-studio-tile-video::-webkit-media-controls-start-playback-button,
2032
+ .livo-studio-inline-video::-webkit-media-controls-start-playback-button {
2033
+ display: none !important;
2034
+ -webkit-appearance: none;
2035
+ }
2036
+ .livo-studio-tile-video::-webkit-media-controls,
2037
+ .livo-studio-inline-video::-webkit-media-controls {
2038
+ display: none !important;
2039
+ }
2040
+ `;
2026
2041
  function StudioPipPanel({
2027
2042
  meeting,
2028
2043
  labels,
@@ -2077,7 +2092,7 @@ function StudioPipPanel({
2077
2092
  /* @__PURE__ */ jsx(StudioLiveBadge, { children: labels.live }),
2078
2093
  elapsed ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground tabular-nums text-xs", children: elapsed }) : null
2079
2094
  ] }) : null,
2080
- selfView ? /* @__PURE__ */ jsx(
2095
+ selfView && meeting.localVideoTrack() ? /* @__PURE__ */ jsx(
2081
2096
  "div",
2082
2097
  {
2083
2098
  "data-testid": "pip-self-view",
@@ -2134,7 +2149,8 @@ function PipSpotlight({
2134
2149
  spotlight,
2135
2150
  cameraOffLabel
2136
2151
  }) {
2137
- if (!spotlight || spotlight.kind === "idle") {
2152
+ const track = spotlight && spotlight.kind !== "idle" ? spotlight.kind === "screen" ? meeting.participantScreenTrack(spotlight.participantId) : meeting.participantVideoTrack(spotlight.participantId) : null;
2153
+ if (!spotlight || spotlight.kind === "idle" || !track) {
2138
2154
  return /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-2", children: [
2139
2155
  spotlight?.picture ? /* @__PURE__ */ jsx(
2140
2156
  "img",
@@ -2147,7 +2163,6 @@ function PipSpotlight({
2147
2163
  /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: spotlight?.name ?? cameraOffLabel })
2148
2164
  ] });
2149
2165
  }
2150
- const track = spotlight.kind === "screen" ? meeting.participantScreenTrack(spotlight.participantId) : meeting.participantVideoTrack(spotlight.participantId);
2151
2166
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2152
2167
  /* @__PURE__ */ jsx(
2153
2168
  PipTrackVideo,
@@ -2166,11 +2181,7 @@ function PipTrackVideo({
2166
2181
  const ref = useRef(null);
2167
2182
  useEffect(() => {
2168
2183
  const el = ref.current;
2169
- if (!el) return;
2170
- if (!track) {
2171
- el.srcObject = null;
2172
- return;
2173
- }
2184
+ if (!el || !track) return;
2174
2185
  el.setAttribute("webkit-playsinline", "");
2175
2186
  el.srcObject = new MediaStream([track]);
2176
2187
  void el.play().catch(() => void 0);
@@ -2178,19 +2189,24 @@ function PipTrackVideo({
2178
2189
  el.srcObject = null;
2179
2190
  };
2180
2191
  }, [track]);
2181
- return /* @__PURE__ */ jsx(
2182
- "video",
2183
- {
2184
- ref,
2185
- autoPlay: true,
2186
- playsInline: true,
2187
- muted: true,
2188
- className: cn(
2189
- "livo-studio-inline-video absolute inset-0 size-full",
2190
- fit === "contain" ? "object-contain" : "object-cover"
2191
- )
2192
- }
2193
- );
2192
+ if (!track) return null;
2193
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2194
+ /* @__PURE__ */ jsx("style", { children: STUDIO_VIDEO_MEDIA_STYLE }),
2195
+ /* @__PURE__ */ jsx(
2196
+ "video",
2197
+ {
2198
+ ref,
2199
+ autoPlay: true,
2200
+ playsInline: true,
2201
+ muted: true,
2202
+ disableRemotePlayback: true,
2203
+ className: cn(
2204
+ "livo-studio-inline-video absolute inset-0 size-full",
2205
+ fit === "contain" ? "object-contain" : "object-cover"
2206
+ )
2207
+ }
2208
+ )
2209
+ ] });
2194
2210
  }
2195
2211
  function pipScreenControls({
2196
2212
  labels,
@@ -3217,6 +3233,7 @@ function StudioTile({
3217
3233
  const onStage = isOnStage(tile.stageStatus);
3218
3234
  const [confirm, setConfirm] = useState(null);
3219
3235
  const showMenu = (canModerate || canManageStage) && !tile.isSelf;
3236
+ const hasVideoTrack = tileHasVideoTrack(meeting, tile);
3220
3237
  const glowRef = useSpeakingGlow({
3221
3238
  track: tile.kind === "screen" || !tile.audioEnabled ? null : meeting.participantAudioTrack(tile.id),
3222
3239
  enabled: onStage,
@@ -3232,23 +3249,14 @@ function StudioTile({
3232
3249
  compact ? "aspect-video w-40 shrink-0 md:w-full" : tile.kind === "screen" ? "min-h-0 flex-1 md:min-h-48" : fill ? "size-full min-h-0 md:aspect-video md:h-auto" : "aspect-video"
3233
3250
  ),
3234
3251
  children: [
3235
- tile.kind === "idle" ? /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-3", children: [
3236
- tile.picture ? /* @__PURE__ */ jsx(
3237
- "img",
3238
- {
3239
- src: tile.picture,
3240
- alt: "",
3241
- className: "size-16 rounded-full object-cover"
3242
- }
3243
- ) : /* @__PURE__ */ jsx("div", { className: "bg-background flex size-16 items-center justify-center rounded-full text-xl", children: tile.name.slice(0, 1).toUpperCase() }),
3244
- /* @__PURE__ */ jsx(
3245
- AudioWaveform,
3246
- {
3247
- track: tile.isSelf ? meeting.localAudioTrack() : null,
3248
- reducedMotion
3249
- }
3250
- )
3251
- ] }) : /* @__PURE__ */ jsx(
3252
+ tile.kind === "idle" || !hasVideoTrack ? /* @__PURE__ */ jsx(
3253
+ IdlePersonTile,
3254
+ {
3255
+ tile,
3256
+ meeting,
3257
+ reducedMotion
3258
+ }
3259
+ ) : /* @__PURE__ */ jsx(
3252
3260
  StudioTileVideo,
3253
3261
  {
3254
3262
  meeting,
@@ -3347,6 +3355,37 @@ function TileStatusIcons({
3347
3355
  tile.chatCanSend === false ? /* @__PURE__ */ jsx(StatusIcon, { label: labels.muteChat, children: /* @__PURE__ */ jsx(MessageSquareOff, {}) }) : null
3348
3356
  ] });
3349
3357
  }
3358
+ function tileHasVideoTrack(meeting, tile) {
3359
+ if (tile.kind === "screen") {
3360
+ return meeting.participantScreenTrack(tile.id) != null;
3361
+ }
3362
+ if (tile.kind === "idle") return false;
3363
+ if (tile.isSelf) return meeting.localVideoTrack() != null;
3364
+ return meeting.participantVideoTrack(tile.id) != null;
3365
+ }
3366
+ function IdlePersonTile({
3367
+ tile,
3368
+ meeting,
3369
+ reducedMotion
3370
+ }) {
3371
+ return /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-3", children: [
3372
+ tile.picture ? /* @__PURE__ */ jsx(
3373
+ "img",
3374
+ {
3375
+ src: tile.picture,
3376
+ alt: "",
3377
+ className: "size-16 rounded-full object-cover"
3378
+ }
3379
+ ) : /* @__PURE__ */ jsx("div", { className: "bg-background flex size-16 items-center justify-center rounded-full text-xl", children: tile.name.slice(0, 1).toUpperCase() }),
3380
+ /* @__PURE__ */ jsx(
3381
+ AudioWaveform,
3382
+ {
3383
+ track: tile.isSelf ? meeting.localAudioTrack() : null,
3384
+ reducedMotion
3385
+ }
3386
+ )
3387
+ ] });
3388
+ }
3350
3389
  function StatusIcon({
3351
3390
  label,
3352
3391
  children
@@ -3370,23 +3409,27 @@ function StudioTileVideo({
3370
3409
  meeting.attachVideo(participantId, el, kind);
3371
3410
  return () => meeting.attachVideo(participantId, null, kind);
3372
3411
  }, [kind, meeting, participantId]);
3373
- return /* @__PURE__ */ jsx(
3374
- "video",
3375
- {
3376
- ref,
3377
- autoPlay: true,
3378
- playsInline: true,
3379
- muted: true,
3380
- disablePictureInPicture: true,
3381
- controlsList: "nodownload noplaybackrate noremoteplayback",
3382
- className: cn(
3383
- "livo-studio-tile-video absolute inset-0 size-full",
3384
- "pointer-events-none",
3385
- contain ? "object-contain" : "object-cover",
3386
- mirror && "-scale-x-100"
3387
- )
3388
- }
3389
- );
3412
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3413
+ /* @__PURE__ */ jsx("style", { children: STUDIO_VIDEO_MEDIA_STYLE }),
3414
+ /* @__PURE__ */ jsx(
3415
+ "video",
3416
+ {
3417
+ ref,
3418
+ autoPlay: true,
3419
+ playsInline: true,
3420
+ muted: true,
3421
+ disablePictureInPicture: true,
3422
+ disableRemotePlayback: true,
3423
+ controlsList: "nodownload noplaybackrate noremoteplayback",
3424
+ className: cn(
3425
+ "livo-studio-tile-video absolute inset-0 size-full",
3426
+ "pointer-events-none",
3427
+ contain ? "object-contain" : "object-cover",
3428
+ mirror && "-scale-x-100"
3429
+ )
3430
+ }
3431
+ )
3432
+ ] });
3390
3433
  }
3391
3434
  function StudioStage({
3392
3435
  participants,
@@ -3478,6 +3521,32 @@ function StudioStage({
3478
3521
  }
3479
3522
  );
3480
3523
  }
3524
+ function StudioDownloadAppDialog({
3525
+ open,
3526
+ onOpenChange,
3527
+ downloadUrl,
3528
+ labels
3529
+ }) {
3530
+ const href = downloadUrl?.trim() ?? "";
3531
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { children: [
3532
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
3533
+ /* @__PURE__ */ jsx(DialogTitle, { children: labels.screenDownloadAppTitle }),
3534
+ /* @__PURE__ */ jsx(DialogDescription, { children: labels.screenUnsupportedDevice })
3535
+ ] }),
3536
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
3537
+ /* @__PURE__ */ jsx(
3538
+ Button,
3539
+ {
3540
+ type: "button",
3541
+ variant: "outline",
3542
+ onClick: () => onOpenChange(false),
3543
+ children: labels.screenDownloadAppDismiss
3544
+ }
3545
+ ),
3546
+ href ? /* @__PURE__ */ jsx(Button, { render: /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noreferrer" }), children: labels.screenDownloadAppAction }) : /* @__PURE__ */ jsx(Button, { type: "button", disabled: true, children: labels.screenDownloadAppAction })
3547
+ ] })
3548
+ ] }) });
3549
+ }
3481
3550
  function StudioToolbar({
3482
3551
  session,
3483
3552
  meeting,
@@ -3732,7 +3801,8 @@ function StudioRoom({
3732
3801
  windowGate,
3733
3802
  initialCameraOn = true,
3734
3803
  initialMuted = false,
3735
- initialDevices
3804
+ initialDevices,
3805
+ appDownloadUrl
3736
3806
  }) {
3737
3807
  const [cameraOn, setCameraOn] = useState(initialCameraOn);
3738
3808
  const [screenOn, setScreenOn] = useState(false);
@@ -3753,6 +3823,7 @@ function StudioRoom({
3753
3823
  );
3754
3824
  const [egressError, setEgressError] = useState(null);
3755
3825
  const [shareGrant, setShareGrant] = useState("idle");
3826
+ const [downloadAppOpen, setDownloadAppOpen] = useState(false);
3756
3827
  const [connectTimedOut, setConnectTimedOut] = useState(false);
3757
3828
  const [screenPermission, setScreenPermission] = useState(
3758
3829
  () => meeting.screenSharePermission()
@@ -3794,6 +3865,11 @@ function StudioRoom({
3794
3865
  const roomState = leaveState ?? meeting.roomState();
3795
3866
  const sessionOver = isStudioSessionOver(session.stream.status, roomState);
3796
3867
  const rejected = isStudioRejected(roomState);
3868
+ const connectionState = useMemo(
3869
+ () => meeting.connectionState(),
3870
+ [meeting, tick2]
3871
+ );
3872
+ const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
3797
3873
  useBeforeUnload(active && !sessionOver);
3798
3874
  useWakeLock(active && !sessionOver);
3799
3875
  useEffect(() => {
@@ -3957,12 +4033,13 @@ function StudioRoom({
3957
4033
  void meeting.setScreenShareEnabled(false);
3958
4034
  }, [guestOffStage, meeting]);
3959
4035
  useEffect(() => {
3960
- if (!sessionOver && !rejected) return;
4036
+ if (!sessionOver && !rejected && !guestConnectFailed) return;
4037
+ pip.close();
3961
4038
  setShareGrant("idle");
3962
4039
  void meeting.setAudioEnabled(false);
3963
4040
  void meeting.setVideoEnabled(false);
3964
4041
  void meeting.setScreenShareEnabled(false);
3965
- }, [meeting, rejected, sessionOver]);
4042
+ }, [guestConnectFailed, meeting, pip.close, rejected, sessionOver]);
3966
4043
  useEffect(() => {
3967
4044
  return meeting.subscribeMedia(() => {
3968
4045
  const self2 = meeting.participants().find((row) => row.isSelf);
@@ -4000,10 +4077,6 @@ function StudioRoom({
4000
4077
  [participants]
4001
4078
  );
4002
4079
  const waitlisted = useMemo(() => meeting.waitlisted(), [meeting, tick2]);
4003
- const connectionState = useMemo(
4004
- () => meeting.connectionState(),
4005
- [meeting, tick2]
4006
- );
4007
4080
  const lastMeetingError = useMemo(
4008
4081
  () => meeting.lastMeetingError(),
4009
4082
  [meeting, tick2]
@@ -4021,7 +4094,7 @@ function StudioRoom({
4021
4094
  captureSupported,
4022
4095
  screenPermission
4023
4096
  );
4024
- const showScreenControl = durableScreenBlock === null;
4097
+ const showScreenControl = showScreenShareControl(durableScreenBlock);
4025
4098
  const canGoLive = session.role === "moderator" && canPublish(session.stream.status);
4026
4099
  const showPublishButton = session.role === "moderator" && !onAir && session.stream.status !== "ended" && session.stream.status !== "error";
4027
4100
  const self = participants.find((row) => row.isSelf);
@@ -4117,17 +4190,10 @@ function StudioRoom({
4117
4190
  }, [connectionState, session.role]);
4118
4191
  useEffect(() => {
4119
4192
  if (inWaitingRoom || screenNoticePrimed.current) return;
4120
- if (!durableScreenBlock) return;
4193
+ if (durableScreenBlock !== "permission") return;
4121
4194
  screenNoticePrimed.current = true;
4122
- toast.message(
4123
- durableScreenBlock === "device" ? labels.screenUnsupportedDevice : labels.screenNotAllowed
4124
- );
4125
- }, [
4126
- durableScreenBlock,
4127
- inWaitingRoom,
4128
- labels.screenNotAllowed,
4129
- labels.screenUnsupportedDevice
4130
- ]);
4195
+ toast.message(labels.screenNotAllowed);
4196
+ }, [durableScreenBlock, inWaitingRoom, labels.screenNotAllowed]);
4131
4197
  useEffect(() => {
4132
4198
  return meeting.subscribeBroadcast((message) => {
4133
4199
  if (message.type === "host-media") {
@@ -4236,6 +4302,10 @@ function StudioRoom({
4236
4302
  setCameraOn((current) => !current);
4237
4303
  }, []);
4238
4304
  const onToggleScreen = useCallback(() => {
4305
+ if (durableScreenBlock === "device") {
4306
+ setDownloadAppOpen(true);
4307
+ return;
4308
+ }
4239
4309
  if (!canShareFreely || screenRequestPending) return;
4240
4310
  if (!screenOn && !canStartScreenShare(meeting.participants())) {
4241
4311
  toast.message(labels.screenBusy);
@@ -4245,12 +4315,17 @@ function StudioRoom({
4245
4315
  setScreenOn((current) => !current);
4246
4316
  }, [
4247
4317
  canShareFreely,
4318
+ durableScreenBlock,
4248
4319
  labels.screenBusy,
4249
4320
  meeting,
4250
4321
  screenOn,
4251
4322
  screenRequestPending
4252
4323
  ]);
4253
4324
  const onAskToShare = useCallback(async () => {
4325
+ if (durableScreenBlock === "device") {
4326
+ setDownloadAppOpen(true);
4327
+ return;
4328
+ }
4254
4329
  const local = meeting.participants().find((row) => row.isSelf);
4255
4330
  if (!local) return;
4256
4331
  try {
@@ -4267,7 +4342,12 @@ function StudioRoom({
4267
4342
  } catch {
4268
4343
  toast.error(labels.shareRequestError);
4269
4344
  }
4270
- }, [labels.shareRequestError, labels.shareRequestSent, meeting]);
4345
+ }, [
4346
+ durableScreenBlock,
4347
+ labels.shareRequestError,
4348
+ labels.shareRequestSent,
4349
+ meeting
4350
+ ]);
4271
4351
  const verifyGrantTookEffect = useCallback(
4272
4352
  (userId, retried = false) => {
4273
4353
  const id = window.setTimeout(() => {
@@ -4430,7 +4510,6 @@ function StudioRoom({
4430
4510
  }
4431
4511
  ) });
4432
4512
  }
4433
- const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
4434
4513
  if (guestConnectFailed) {
4435
4514
  const detail = [meeting.roomState(), lastMeetingError].filter((value) => Boolean(value)).join(" \u2014 ");
4436
4515
  return /* @__PURE__ */ jsx("div", { className: "flex h-dvh flex-col items-center justify-center p-6", children: /* @__PURE__ */ jsx(
@@ -4764,7 +4843,21 @@ function StudioRoom({
4764
4843
  }
4765
4844
  ),
4766
4845
  pip.pipWindow.document.body
4767
- ) : null
4846
+ ) : null,
4847
+ /* @__PURE__ */ jsx(
4848
+ StudioDownloadAppDialog,
4849
+ {
4850
+ open: downloadAppOpen,
4851
+ onOpenChange: setDownloadAppOpen,
4852
+ downloadUrl: appDownloadUrl,
4853
+ labels: {
4854
+ screenDownloadAppTitle: labels.screenDownloadAppTitle,
4855
+ screenUnsupportedDevice: labels.screenUnsupportedDevice,
4856
+ screenDownloadAppAction: labels.screenDownloadAppAction,
4857
+ screenDownloadAppDismiss: labels.screenDownloadAppDismiss
4858
+ }
4859
+ }
4860
+ )
4768
4861
  ] });
4769
4862
  }
4770
4863
  function isOverconstrained(error) {
@@ -6307,7 +6400,8 @@ function LivoHostStudio({
6307
6400
  embed = false,
6308
6401
  chrome,
6309
6402
  playerOrigin,
6310
- theme
6403
+ theme,
6404
+ appDownloadUrl
6311
6405
  }) {
6312
6406
  const reducedMotion = usePrefersReducedMotion();
6313
6407
  const [session, setSession] = useState(null);
@@ -6387,7 +6481,8 @@ function LivoHostStudio({
6387
6481
  labels,
6388
6482
  embed,
6389
6483
  chrome,
6390
- playerOrigin
6484
+ playerOrigin,
6485
+ appDownloadUrl
6391
6486
  }
6392
6487
  );
6393
6488
  }
@@ -6400,7 +6495,8 @@ function LivoGuestStudio({
6400
6495
  lobbyLabels,
6401
6496
  embed = false,
6402
6497
  playerOrigin,
6403
- theme
6498
+ theme,
6499
+ appDownloadUrl
6404
6500
  }) {
6405
6501
  const reducedMotion = usePrefersReducedMotion();
6406
6502
  const [session, setSession] = useState(null);
@@ -6586,7 +6682,8 @@ function LivoGuestStudio({
6586
6682
  playerOrigin,
6587
6683
  initialCameraOn: cameraOn,
6588
6684
  initialMuted: muted,
6589
- initialDevices: deviceSelection
6685
+ initialDevices: deviceSelection,
6686
+ appDownloadUrl
6590
6687
  }
6591
6688
  );
6592
6689
  }
@@ -6691,6 +6788,9 @@ function studioRoomLabels(t) {
6691
6788
  pipTitle: t("pipTitle"),
6692
6789
  pipError: t("pipError"),
6693
6790
  screenUnsupportedDevice: t("screenUnsupportedDevice"),
6791
+ screenDownloadAppTitle: t("screenDownloadAppTitle"),
6792
+ screenDownloadAppAction: t("screenDownloadAppAction"),
6793
+ screenDownloadAppDismiss: t("screenDownloadAppDismiss"),
6694
6794
  screenNotAllowed: t("screenNotAllowed"),
6695
6795
  askToShare: t("askToShare"),
6696
6796
  shareRequested: t("shareRequested"),