@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.
@@ -542,52 +542,38 @@ function canTakeOffAir(tiles, participantId, onAir) {
542
542
  (tile) => tile.id !== participantId && isOnStage(tile.stageStatus)
543
543
  );
544
544
  }
545
+ function pipSpotlightFrom(tile, kind) {
546
+ return {
547
+ participantId: tile.id,
548
+ name: tile.name,
549
+ kind,
550
+ picture: tile.picture
551
+ };
552
+ }
545
553
  function resolveStudioPipSpotlight(tiles, activeSpeakerId) {
546
554
  const visible = tiles.filter((tile) => isOnStage(tile.stageStatus));
547
555
  const pinned = visible.find((tile) => tile.pinned === true);
548
556
  if (pinned) {
549
- return {
550
- participantId: pinned.id,
551
- name: pinned.name,
552
- kind: pinned.screenShareEnabled ? "screen" : pinned.videoEnabled ? "camera" : "idle",
553
- picture: pinned.picture
554
- };
557
+ return pipSpotlightFrom(
558
+ pinned,
559
+ pinned.screenShareEnabled ? "screen" : pinned.videoEnabled ? "camera" : "idle"
560
+ );
561
+ }
562
+ const speaker = activeSpeakerId ? visible.find((tile) => tile.id === activeSpeakerId) : void 0;
563
+ if (speaker) {
564
+ return pipSpotlightFrom(speaker, speaker.videoEnabled ? "camera" : "idle");
555
565
  }
556
566
  const sharing = visible.find((tile) => tile.screenShareEnabled);
557
567
  if (sharing) {
558
- return {
559
- participantId: sharing.id,
560
- name: sharing.name,
561
- kind: "screen",
562
- picture: sharing.picture
563
- };
564
- }
565
- const speaker = activeSpeakerId ? visible.find((tile) => tile.id === activeSpeakerId && tile.videoEnabled) : void 0;
566
- if (speaker) {
567
- return {
568
- participantId: speaker.id,
569
- name: speaker.name,
570
- kind: "camera",
571
- picture: speaker.picture
572
- };
568
+ return pipSpotlightFrom(sharing, "screen");
573
569
  }
574
570
  const remoteCam = visible.find((tile) => !tile.isSelf && tile.videoEnabled);
575
571
  if (remoteCam) {
576
- return {
577
- participantId: remoteCam.id,
578
- name: remoteCam.name,
579
- kind: "camera",
580
- picture: remoteCam.picture
581
- };
572
+ return pipSpotlightFrom(remoteCam, "camera");
582
573
  }
583
574
  const self = visible.find((tile) => tile.isSelf);
584
575
  if (!self) return null;
585
- return {
586
- participantId: self.id,
587
- name: self.name,
588
- kind: self.videoEnabled ? "camera" : "idle",
589
- picture: self.picture
590
- };
576
+ return pipSpotlightFrom(self, self.videoEnabled ? "camera" : "idle");
591
577
  }
592
578
  function resolveStudioPipSelfView(tiles, spotlight) {
593
579
  const self = tiles.find((tile) => tile.isSelf);
@@ -706,6 +692,9 @@ function screenShareBlock(captureSupported, permission) {
706
692
  if (permission === "NOT_ALLOWED") return "permission";
707
693
  return null;
708
694
  }
695
+ function showScreenShareControl(block) {
696
+ return block !== "permission";
697
+ }
709
698
 
710
699
  // src/studio/lib/parse.ts
711
700
  function isRecord(value) {
@@ -1488,6 +1477,19 @@ function DialogHeader({ className, ...props }) {
1488
1477
  }
1489
1478
  );
1490
1479
  }
1480
+ function DialogFooter({ className, ...props }) {
1481
+ return /* @__PURE__ */ jsxRuntime.jsx(
1482
+ "div",
1483
+ {
1484
+ "data-slot": "dialog-footer",
1485
+ className: cn(
1486
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1487
+ className
1488
+ ),
1489
+ ...props
1490
+ }
1491
+ );
1492
+ }
1491
1493
  function DialogTitle({
1492
1494
  className,
1493
1495
  ...props
@@ -2047,6 +2049,19 @@ var badgeVariants = classVarianceAuthority.cva(
2047
2049
  function Badge({ className, variant, ...props }) {
2048
2050
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(badgeVariants({ variant }), className), ...props });
2049
2051
  }
2052
+
2053
+ // src/studio/lib/studio-video-style.ts
2054
+ var STUDIO_VIDEO_MEDIA_STYLE = `
2055
+ .livo-studio-tile-video::-webkit-media-controls-start-playback-button,
2056
+ .livo-studio-inline-video::-webkit-media-controls-start-playback-button {
2057
+ display: none !important;
2058
+ -webkit-appearance: none;
2059
+ }
2060
+ .livo-studio-tile-video::-webkit-media-controls,
2061
+ .livo-studio-inline-video::-webkit-media-controls {
2062
+ display: none !important;
2063
+ }
2064
+ `;
2050
2065
  function StudioPipPanel({
2051
2066
  meeting,
2052
2067
  labels,
@@ -2101,7 +2116,7 @@ function StudioPipPanel({
2101
2116
  /* @__PURE__ */ jsxRuntime.jsx(StudioLiveBadge, { children: labels.live }),
2102
2117
  elapsed ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground tabular-nums text-xs", children: elapsed }) : null
2103
2118
  ] }) : null,
2104
- selfView ? /* @__PURE__ */ jsxRuntime.jsx(
2119
+ selfView && meeting.localVideoTrack() ? /* @__PURE__ */ jsxRuntime.jsx(
2105
2120
  "div",
2106
2121
  {
2107
2122
  "data-testid": "pip-self-view",
@@ -2158,7 +2173,8 @@ function PipSpotlight({
2158
2173
  spotlight,
2159
2174
  cameraOffLabel
2160
2175
  }) {
2161
- if (!spotlight || spotlight.kind === "idle") {
2176
+ const track = spotlight && spotlight.kind !== "idle" ? spotlight.kind === "screen" ? meeting.participantScreenTrack(spotlight.participantId) : meeting.participantVideoTrack(spotlight.participantId) : null;
2177
+ if (!spotlight || spotlight.kind === "idle" || !track) {
2162
2178
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-2", children: [
2163
2179
  spotlight?.picture ? /* @__PURE__ */ jsxRuntime.jsx(
2164
2180
  "img",
@@ -2171,7 +2187,6 @@ function PipSpotlight({
2171
2187
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-xs", children: spotlight?.name ?? cameraOffLabel })
2172
2188
  ] });
2173
2189
  }
2174
- const track = spotlight.kind === "screen" ? meeting.participantScreenTrack(spotlight.participantId) : meeting.participantVideoTrack(spotlight.participantId);
2175
2190
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2176
2191
  /* @__PURE__ */ jsxRuntime.jsx(
2177
2192
  PipTrackVideo,
@@ -2190,11 +2205,7 @@ function PipTrackVideo({
2190
2205
  const ref = react.useRef(null);
2191
2206
  react.useEffect(() => {
2192
2207
  const el = ref.current;
2193
- if (!el) return;
2194
- if (!track) {
2195
- el.srcObject = null;
2196
- return;
2197
- }
2208
+ if (!el || !track) return;
2198
2209
  el.setAttribute("webkit-playsinline", "");
2199
2210
  el.srcObject = new MediaStream([track]);
2200
2211
  void el.play().catch(() => void 0);
@@ -2202,19 +2213,24 @@ function PipTrackVideo({
2202
2213
  el.srcObject = null;
2203
2214
  };
2204
2215
  }, [track]);
2205
- return /* @__PURE__ */ jsxRuntime.jsx(
2206
- "video",
2207
- {
2208
- ref,
2209
- autoPlay: true,
2210
- playsInline: true,
2211
- muted: true,
2212
- className: cn(
2213
- "livo-studio-inline-video absolute inset-0 size-full",
2214
- fit === "contain" ? "object-contain" : "object-cover"
2215
- )
2216
- }
2217
- );
2216
+ if (!track) return null;
2217
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2218
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: STUDIO_VIDEO_MEDIA_STYLE }),
2219
+ /* @__PURE__ */ jsxRuntime.jsx(
2220
+ "video",
2221
+ {
2222
+ ref,
2223
+ autoPlay: true,
2224
+ playsInline: true,
2225
+ muted: true,
2226
+ disableRemotePlayback: true,
2227
+ className: cn(
2228
+ "livo-studio-inline-video absolute inset-0 size-full",
2229
+ fit === "contain" ? "object-contain" : "object-cover"
2230
+ )
2231
+ }
2232
+ )
2233
+ ] });
2218
2234
  }
2219
2235
  function pipScreenControls({
2220
2236
  labels,
@@ -3241,6 +3257,7 @@ function StudioTile({
3241
3257
  const onStage = isOnStage(tile.stageStatus);
3242
3258
  const [confirm, setConfirm] = react.useState(null);
3243
3259
  const showMenu = (canModerate || canManageStage) && !tile.isSelf;
3260
+ const hasVideoTrack = tileHasVideoTrack(meeting, tile);
3244
3261
  const glowRef = useSpeakingGlow({
3245
3262
  track: tile.kind === "screen" || !tile.audioEnabled ? null : meeting.participantAudioTrack(tile.id),
3246
3263
  enabled: onStage,
@@ -3256,23 +3273,14 @@ function StudioTile({
3256
3273
  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"
3257
3274
  ),
3258
3275
  children: [
3259
- tile.kind === "idle" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-3", children: [
3260
- tile.picture ? /* @__PURE__ */ jsxRuntime.jsx(
3261
- "img",
3262
- {
3263
- src: tile.picture,
3264
- alt: "",
3265
- className: "size-16 rounded-full object-cover"
3266
- }
3267
- ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-background flex size-16 items-center justify-center rounded-full text-xl", children: tile.name.slice(0, 1).toUpperCase() }),
3268
- /* @__PURE__ */ jsxRuntime.jsx(
3269
- AudioWaveform,
3270
- {
3271
- track: tile.isSelf ? meeting.localAudioTrack() : null,
3272
- reducedMotion
3273
- }
3274
- )
3275
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(
3276
+ tile.kind === "idle" || !hasVideoTrack ? /* @__PURE__ */ jsxRuntime.jsx(
3277
+ IdlePersonTile,
3278
+ {
3279
+ tile,
3280
+ meeting,
3281
+ reducedMotion
3282
+ }
3283
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
3276
3284
  StudioTileVideo,
3277
3285
  {
3278
3286
  meeting,
@@ -3371,6 +3379,37 @@ function TileStatusIcons({
3371
3379
  tile.chatCanSend === false ? /* @__PURE__ */ jsxRuntime.jsx(StatusIcon, { label: labels.muteChat, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquareOff, {}) }) : null
3372
3380
  ] });
3373
3381
  }
3382
+ function tileHasVideoTrack(meeting, tile) {
3383
+ if (tile.kind === "screen") {
3384
+ return meeting.participantScreenTrack(tile.id) != null;
3385
+ }
3386
+ if (tile.kind === "idle") return false;
3387
+ if (tile.isSelf) return meeting.localVideoTrack() != null;
3388
+ return meeting.participantVideoTrack(tile.id) != null;
3389
+ }
3390
+ function IdlePersonTile({
3391
+ tile,
3392
+ meeting,
3393
+ reducedMotion
3394
+ }) {
3395
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-3", children: [
3396
+ tile.picture ? /* @__PURE__ */ jsxRuntime.jsx(
3397
+ "img",
3398
+ {
3399
+ src: tile.picture,
3400
+ alt: "",
3401
+ className: "size-16 rounded-full object-cover"
3402
+ }
3403
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-background flex size-16 items-center justify-center rounded-full text-xl", children: tile.name.slice(0, 1).toUpperCase() }),
3404
+ /* @__PURE__ */ jsxRuntime.jsx(
3405
+ AudioWaveform,
3406
+ {
3407
+ track: tile.isSelf ? meeting.localAudioTrack() : null,
3408
+ reducedMotion
3409
+ }
3410
+ )
3411
+ ] });
3412
+ }
3374
3413
  function StatusIcon({
3375
3414
  label,
3376
3415
  children
@@ -3394,23 +3433,27 @@ function StudioTileVideo({
3394
3433
  meeting.attachVideo(participantId, el, kind);
3395
3434
  return () => meeting.attachVideo(participantId, null, kind);
3396
3435
  }, [kind, meeting, participantId]);
3397
- return /* @__PURE__ */ jsxRuntime.jsx(
3398
- "video",
3399
- {
3400
- ref,
3401
- autoPlay: true,
3402
- playsInline: true,
3403
- muted: true,
3404
- disablePictureInPicture: true,
3405
- controlsList: "nodownload noplaybackrate noremoteplayback",
3406
- className: cn(
3407
- "livo-studio-tile-video absolute inset-0 size-full",
3408
- "pointer-events-none",
3409
- contain ? "object-contain" : "object-cover",
3410
- mirror && "-scale-x-100"
3411
- )
3412
- }
3413
- );
3436
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3437
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: STUDIO_VIDEO_MEDIA_STYLE }),
3438
+ /* @__PURE__ */ jsxRuntime.jsx(
3439
+ "video",
3440
+ {
3441
+ ref,
3442
+ autoPlay: true,
3443
+ playsInline: true,
3444
+ muted: true,
3445
+ disablePictureInPicture: true,
3446
+ disableRemotePlayback: true,
3447
+ controlsList: "nodownload noplaybackrate noremoteplayback",
3448
+ className: cn(
3449
+ "livo-studio-tile-video absolute inset-0 size-full",
3450
+ "pointer-events-none",
3451
+ contain ? "object-contain" : "object-cover",
3452
+ mirror && "-scale-x-100"
3453
+ )
3454
+ }
3455
+ )
3456
+ ] });
3414
3457
  }
3415
3458
  function StudioStage({
3416
3459
  participants,
@@ -3502,6 +3545,32 @@ function StudioStage({
3502
3545
  }
3503
3546
  );
3504
3547
  }
3548
+ function StudioDownloadAppDialog({
3549
+ open,
3550
+ onOpenChange,
3551
+ downloadUrl,
3552
+ labels
3553
+ }) {
3554
+ const href = downloadUrl?.trim() ?? "";
3555
+ return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(DialogContent, { children: [
3556
+ /* @__PURE__ */ jsxRuntime.jsxs(DialogHeader, { children: [
3557
+ /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { children: labels.screenDownloadAppTitle }),
3558
+ /* @__PURE__ */ jsxRuntime.jsx(DialogDescription, { children: labels.screenUnsupportedDevice })
3559
+ ] }),
3560
+ /* @__PURE__ */ jsxRuntime.jsxs(DialogFooter, { children: [
3561
+ /* @__PURE__ */ jsxRuntime.jsx(
3562
+ Button,
3563
+ {
3564
+ type: "button",
3565
+ variant: "outline",
3566
+ onClick: () => onOpenChange(false),
3567
+ children: labels.screenDownloadAppDismiss
3568
+ }
3569
+ ),
3570
+ href ? /* @__PURE__ */ jsxRuntime.jsx(Button, { render: /* @__PURE__ */ jsxRuntime.jsx("a", { href, target: "_blank", rel: "noreferrer" }), children: labels.screenDownloadAppAction }) : /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", disabled: true, children: labels.screenDownloadAppAction })
3571
+ ] })
3572
+ ] }) });
3573
+ }
3505
3574
  function StudioToolbar({
3506
3575
  session,
3507
3576
  meeting,
@@ -3756,7 +3825,8 @@ function StudioRoom({
3756
3825
  windowGate,
3757
3826
  initialCameraOn = true,
3758
3827
  initialMuted = false,
3759
- initialDevices
3828
+ initialDevices,
3829
+ appDownloadUrl
3760
3830
  }) {
3761
3831
  const [cameraOn, setCameraOn] = react.useState(initialCameraOn);
3762
3832
  const [screenOn, setScreenOn] = react.useState(false);
@@ -3777,6 +3847,7 @@ function StudioRoom({
3777
3847
  );
3778
3848
  const [egressError, setEgressError] = react.useState(null);
3779
3849
  const [shareGrant, setShareGrant] = react.useState("idle");
3850
+ const [downloadAppOpen, setDownloadAppOpen] = react.useState(false);
3780
3851
  const [connectTimedOut, setConnectTimedOut] = react.useState(false);
3781
3852
  const [screenPermission, setScreenPermission] = react.useState(
3782
3853
  () => meeting.screenSharePermission()
@@ -3818,6 +3889,11 @@ function StudioRoom({
3818
3889
  const roomState = leaveState ?? meeting.roomState();
3819
3890
  const sessionOver = isStudioSessionOver(session.stream.status, roomState);
3820
3891
  const rejected = isStudioRejected(roomState);
3892
+ const connectionState = react.useMemo(
3893
+ () => meeting.connectionState(),
3894
+ [meeting, tick2]
3895
+ );
3896
+ const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
3821
3897
  useBeforeUnload(active && !sessionOver);
3822
3898
  useWakeLock(active && !sessionOver);
3823
3899
  react.useEffect(() => {
@@ -3981,12 +4057,13 @@ function StudioRoom({
3981
4057
  void meeting.setScreenShareEnabled(false);
3982
4058
  }, [guestOffStage, meeting]);
3983
4059
  react.useEffect(() => {
3984
- if (!sessionOver && !rejected) return;
4060
+ if (!sessionOver && !rejected && !guestConnectFailed) return;
4061
+ pip.close();
3985
4062
  setShareGrant("idle");
3986
4063
  void meeting.setAudioEnabled(false);
3987
4064
  void meeting.setVideoEnabled(false);
3988
4065
  void meeting.setScreenShareEnabled(false);
3989
- }, [meeting, rejected, sessionOver]);
4066
+ }, [guestConnectFailed, meeting, pip.close, rejected, sessionOver]);
3990
4067
  react.useEffect(() => {
3991
4068
  return meeting.subscribeMedia(() => {
3992
4069
  const self2 = meeting.participants().find((row) => row.isSelf);
@@ -4024,10 +4101,6 @@ function StudioRoom({
4024
4101
  [participants]
4025
4102
  );
4026
4103
  const waitlisted = react.useMemo(() => meeting.waitlisted(), [meeting, tick2]);
4027
- const connectionState = react.useMemo(
4028
- () => meeting.connectionState(),
4029
- [meeting, tick2]
4030
- );
4031
4104
  const lastMeetingError = react.useMemo(
4032
4105
  () => meeting.lastMeetingError(),
4033
4106
  [meeting, tick2]
@@ -4045,7 +4118,7 @@ function StudioRoom({
4045
4118
  captureSupported,
4046
4119
  screenPermission
4047
4120
  );
4048
- const showScreenControl = durableScreenBlock === null;
4121
+ const showScreenControl = showScreenShareControl(durableScreenBlock);
4049
4122
  const canGoLive = session.role === "moderator" && canPublish(session.stream.status);
4050
4123
  const showPublishButton = session.role === "moderator" && !onAir && session.stream.status !== "ended" && session.stream.status !== "error";
4051
4124
  const self = participants.find((row) => row.isSelf);
@@ -4141,17 +4214,10 @@ function StudioRoom({
4141
4214
  }, [connectionState, session.role]);
4142
4215
  react.useEffect(() => {
4143
4216
  if (inWaitingRoom || screenNoticePrimed.current) return;
4144
- if (!durableScreenBlock) return;
4217
+ if (durableScreenBlock !== "permission") return;
4145
4218
  screenNoticePrimed.current = true;
4146
- sonner.toast.message(
4147
- durableScreenBlock === "device" ? labels.screenUnsupportedDevice : labels.screenNotAllowed
4148
- );
4149
- }, [
4150
- durableScreenBlock,
4151
- inWaitingRoom,
4152
- labels.screenNotAllowed,
4153
- labels.screenUnsupportedDevice
4154
- ]);
4219
+ sonner.toast.message(labels.screenNotAllowed);
4220
+ }, [durableScreenBlock, inWaitingRoom, labels.screenNotAllowed]);
4155
4221
  react.useEffect(() => {
4156
4222
  return meeting.subscribeBroadcast((message) => {
4157
4223
  if (message.type === "host-media") {
@@ -4260,6 +4326,10 @@ function StudioRoom({
4260
4326
  setCameraOn((current) => !current);
4261
4327
  }, []);
4262
4328
  const onToggleScreen = react.useCallback(() => {
4329
+ if (durableScreenBlock === "device") {
4330
+ setDownloadAppOpen(true);
4331
+ return;
4332
+ }
4263
4333
  if (!canShareFreely || screenRequestPending) return;
4264
4334
  if (!screenOn && !canStartScreenShare(meeting.participants())) {
4265
4335
  sonner.toast.message(labels.screenBusy);
@@ -4269,12 +4339,17 @@ function StudioRoom({
4269
4339
  setScreenOn((current) => !current);
4270
4340
  }, [
4271
4341
  canShareFreely,
4342
+ durableScreenBlock,
4272
4343
  labels.screenBusy,
4273
4344
  meeting,
4274
4345
  screenOn,
4275
4346
  screenRequestPending
4276
4347
  ]);
4277
4348
  const onAskToShare = react.useCallback(async () => {
4349
+ if (durableScreenBlock === "device") {
4350
+ setDownloadAppOpen(true);
4351
+ return;
4352
+ }
4278
4353
  const local = meeting.participants().find((row) => row.isSelf);
4279
4354
  if (!local) return;
4280
4355
  try {
@@ -4291,7 +4366,12 @@ function StudioRoom({
4291
4366
  } catch {
4292
4367
  sonner.toast.error(labels.shareRequestError);
4293
4368
  }
4294
- }, [labels.shareRequestError, labels.shareRequestSent, meeting]);
4369
+ }, [
4370
+ durableScreenBlock,
4371
+ labels.shareRequestError,
4372
+ labels.shareRequestSent,
4373
+ meeting
4374
+ ]);
4295
4375
  const verifyGrantTookEffect = react.useCallback(
4296
4376
  (userId, retried = false) => {
4297
4377
  const id = window.setTimeout(() => {
@@ -4454,7 +4534,6 @@ function StudioRoom({
4454
4534
  }
4455
4535
  ) });
4456
4536
  }
4457
- const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
4458
4537
  if (guestConnectFailed) {
4459
4538
  const detail = [meeting.roomState(), lastMeetingError].filter((value) => Boolean(value)).join(" \u2014 ");
4460
4539
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-dvh flex-col items-center justify-center p-6", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -4788,7 +4867,21 @@ function StudioRoom({
4788
4867
  }
4789
4868
  ),
4790
4869
  pip.pipWindow.document.body
4791
- ) : null
4870
+ ) : null,
4871
+ /* @__PURE__ */ jsxRuntime.jsx(
4872
+ StudioDownloadAppDialog,
4873
+ {
4874
+ open: downloadAppOpen,
4875
+ onOpenChange: setDownloadAppOpen,
4876
+ downloadUrl: appDownloadUrl,
4877
+ labels: {
4878
+ screenDownloadAppTitle: labels.screenDownloadAppTitle,
4879
+ screenUnsupportedDevice: labels.screenUnsupportedDevice,
4880
+ screenDownloadAppAction: labels.screenDownloadAppAction,
4881
+ screenDownloadAppDismiss: labels.screenDownloadAppDismiss
4882
+ }
4883
+ }
4884
+ )
4792
4885
  ] });
4793
4886
  }
4794
4887
  function isOverconstrained(error) {
@@ -6331,7 +6424,8 @@ function LivoHostStudio({
6331
6424
  embed = false,
6332
6425
  chrome,
6333
6426
  playerOrigin,
6334
- theme
6427
+ theme,
6428
+ appDownloadUrl
6335
6429
  }) {
6336
6430
  const reducedMotion = usePrefersReducedMotion();
6337
6431
  const [session, setSession] = react.useState(null);
@@ -6411,7 +6505,8 @@ function LivoHostStudio({
6411
6505
  labels,
6412
6506
  embed,
6413
6507
  chrome,
6414
- playerOrigin
6508
+ playerOrigin,
6509
+ appDownloadUrl
6415
6510
  }
6416
6511
  );
6417
6512
  }
@@ -6424,7 +6519,8 @@ function LivoGuestStudio({
6424
6519
  lobbyLabels,
6425
6520
  embed = false,
6426
6521
  playerOrigin,
6427
- theme
6522
+ theme,
6523
+ appDownloadUrl
6428
6524
  }) {
6429
6525
  const reducedMotion = usePrefersReducedMotion();
6430
6526
  const [session, setSession] = react.useState(null);
@@ -6610,7 +6706,8 @@ function LivoGuestStudio({
6610
6706
  playerOrigin,
6611
6707
  initialCameraOn: cameraOn,
6612
6708
  initialMuted: muted,
6613
- initialDevices: deviceSelection
6709
+ initialDevices: deviceSelection,
6710
+ appDownloadUrl
6614
6711
  }
6615
6712
  );
6616
6713
  }
@@ -6715,6 +6812,9 @@ function studioRoomLabels(t) {
6715
6812
  pipTitle: t("pipTitle"),
6716
6813
  pipError: t("pipError"),
6717
6814
  screenUnsupportedDevice: t("screenUnsupportedDevice"),
6815
+ screenDownloadAppTitle: t("screenDownloadAppTitle"),
6816
+ screenDownloadAppAction: t("screenDownloadAppAction"),
6817
+ screenDownloadAppDismiss: t("screenDownloadAppDismiss"),
6718
6818
  screenNotAllowed: t("screenNotAllowed"),
6719
6819
  askToShare: t("askToShare"),
6720
6820
  shareRequested: t("shareRequested"),