@livo-tv/sdk 1.1.0 → 1.1.2

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
@@ -11,7 +11,7 @@ import "@livo-tv/sdk/styles.css";
11
11
  <LivoStudio guestToken="..." theme={{ primary: "#2563eb" }} />
12
12
  ```
13
13
 
14
- `@livo-tv/sdk/studio` is the native room. Tailwind hosts must `@source` `node_modules/@livo-tv/sdk/dist/*.{js,cjs,mjs}` like `@livo-tv/blocks`. RealtimeKit is an optional peer (`@cloudflare/realtimekit`). Root `<LivoStudio>` remains the iframe fallback.
14
+ `@livo-tv/sdk` and `@livo-tv/sdk/studio` are client entries (the published bundles start with `"use client"`). Import them from `"use client"` files in Next.js App Router — do not import `@livo-tv/sdk/studio` from a Server Component page. Tailwind hosts must `@source` `node_modules/@livo-tv/sdk/dist/*.{js,cjs,mjs}` like `@livo-tv/blocks`. RealtimeKit is an optional peer (`@cloudflare/realtimekit`). Root `<LivoStudio>` remains the iframe fallback.
15
15
 
16
16
  Server (Node / Workers):
17
17
 
package/dist/index.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  'use strict';
2
3
 
3
4
  var blocks = require('@livo-tv/blocks');
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { VideoPlayer, createPlaybackBeaconClient, CurtainStage } from '@livo-tv/blocks';
2
3
  import { useState, useRef, useEffect, useMemo } from 'react';
3
4
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  'use strict';
2
3
 
3
4
  var react = require('react');
@@ -10,6 +11,8 @@ var tailwindMerge = require('tailwind-merge');
10
11
  var button = require('@base-ui/react/button');
11
12
  var classVarianceAuthority = require('class-variance-authority');
12
13
  var jsxRuntime = require('react/jsx-runtime');
14
+ var mergeProps = require('@base-ui/react/merge-props');
15
+ var useRender = require('@base-ui/react/use-render');
13
16
  var menu = require('@base-ui/react/menu');
14
17
  var tooltip = require('@base-ui/react/tooltip');
15
18
  var DialogPrimitive = require('@radix-ui/react-dialog');
@@ -775,6 +778,58 @@ function postLivoEvent(type, payload) {
775
778
  window.parent.postMessage({ source: "livo", type, payload }, "*");
776
779
  }
777
780
 
781
+ // src/studio/lib/embed-urls.ts
782
+ function playerWatchUrl(streamId, origin = "https://player.livo.tv") {
783
+ return `${origin.replace(/\/$/, "")}/${streamId}`;
784
+ }
785
+
786
+ // src/studio/lib/clipboard.ts
787
+ async function copyText(value, clipboard = globalThis.navigator?.clipboard ?? null) {
788
+ if (!clipboard) return false;
789
+ try {
790
+ await clipboard.writeText(value);
791
+ return true;
792
+ } catch {
793
+ return false;
794
+ }
795
+ }
796
+
797
+ // src/studio/lib/share.ts
798
+ function isAbort(error) {
799
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
800
+ }
801
+ function resolveShareApi(share = typeof navigator === "undefined" ? null : typeof navigator.share === "function" ? navigator : null) {
802
+ return share ?? null;
803
+ }
804
+ function canNativeShare(data, share = resolveShareApi()) {
805
+ if (!share) return false;
806
+ if (typeof share.canShare !== "function") return true;
807
+ try {
808
+ return share.canShare(data);
809
+ } catch {
810
+ return false;
811
+ }
812
+ }
813
+ async function shareOrCopyLink(options, deps) {
814
+ const data = {
815
+ url: options.url,
816
+ ...options.title ? { title: options.title } : {},
817
+ ...options.text ? { text: options.text } : {}
818
+ };
819
+ const share = resolveShareApi(deps?.share);
820
+ if (canNativeShare(data, share) && share) {
821
+ try {
822
+ await share.share(data);
823
+ return "shared";
824
+ } catch (error) {
825
+ if (isAbort(error)) return "cancelled";
826
+ }
827
+ }
828
+ const copy = copyText;
829
+ const ok = await copy(options.url);
830
+ return ok ? "copied" : "failed";
831
+ }
832
+
778
833
  // src/studio/lib/status.ts
779
834
  function canPublish(status) {
780
835
  return status === "preview";
@@ -1448,9 +1503,124 @@ function StudioRemoteAudio({
1448
1503
  }, [meeting, participantId]);
1449
1504
  return /* @__PURE__ */ jsxRuntime.jsx("audio", { ref, autoPlay: true, className: "sr-only" });
1450
1505
  }
1506
+ function BubbleGroup({ className, ...props }) {
1507
+ return /* @__PURE__ */ jsxRuntime.jsx(
1508
+ "div",
1509
+ {
1510
+ "data-slot": "bubble-group",
1511
+ className: cn("flex min-w-0 flex-col gap-2", className),
1512
+ ...props
1513
+ }
1514
+ );
1515
+ }
1516
+ var bubbleVariants = classVarianceAuthority.cva(
1517
+ "group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full",
1518
+ {
1519
+ variants: {
1520
+ variant: {
1521
+ default: "*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",
1522
+ secondary: "*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]",
1523
+ muted: "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]",
1524
+ tinted: "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]",
1525
+ outline: "*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30",
1526
+ ghost: "border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50",
1527
+ destructive: "*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30"
1528
+ }
1529
+ },
1530
+ defaultVariants: {
1531
+ variant: "default"
1532
+ }
1533
+ }
1534
+ );
1535
+ function Bubble({
1536
+ variant = "default",
1537
+ align = "start",
1538
+ className,
1539
+ ...props
1540
+ }) {
1541
+ return /* @__PURE__ */ jsxRuntime.jsx(
1542
+ "div",
1543
+ {
1544
+ "data-slot": "bubble",
1545
+ "data-variant": variant,
1546
+ "data-align": align,
1547
+ className: cn(bubbleVariants({ variant }), className),
1548
+ ...props
1549
+ }
1550
+ );
1551
+ }
1552
+ function BubbleContent({
1553
+ className,
1554
+ render,
1555
+ ...props
1556
+ }) {
1557
+ return useRender.useRender({
1558
+ defaultTagName: "div",
1559
+ props: mergeProps.mergeProps(
1560
+ {
1561
+ className: cn(
1562
+ "w-fit max-w-full min-w-0 overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50",
1563
+ className
1564
+ )
1565
+ },
1566
+ props
1567
+ ),
1568
+ render,
1569
+ state: {
1570
+ slot: "bubble-content"
1571
+ }
1572
+ });
1573
+ }
1574
+ classVarianceAuthority.cva(
1575
+ "absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0",
1576
+ {
1577
+ variants: {
1578
+ side: {
1579
+ top: "top-0 -translate-y-3/4",
1580
+ bottom: "bottom-0 translate-y-3/4"
1581
+ },
1582
+ align: {
1583
+ start: "left-3",
1584
+ end: "right-3"
1585
+ }
1586
+ },
1587
+ defaultVariants: {
1588
+ side: "bottom",
1589
+ align: "end"
1590
+ }
1591
+ }
1592
+ );
1593
+
1594
+ // src/studio/lib/chat-groups.ts
1595
+ function groupChatMessages(messages, selfId) {
1596
+ const groups = [];
1597
+ for (const row of messages) {
1598
+ const mine = Boolean(selfId) && row.userId === selfId;
1599
+ const last = groups[groups.length - 1];
1600
+ if (last && last.userId === row.userId && last.mine === mine) {
1601
+ last.rows.push(row);
1602
+ continue;
1603
+ }
1604
+ groups.push({
1605
+ userId: row.userId,
1606
+ name: row.name,
1607
+ mine,
1608
+ rows: [row]
1609
+ });
1610
+ }
1611
+ return groups;
1612
+ }
1613
+ function senderInitials(name) {
1614
+ const parts = name.trim().split(/\s+/).filter((part) => part.length > 0);
1615
+ if (parts.length === 0) return "?";
1616
+ const first = parts[0];
1617
+ if (parts.length === 1) return first.slice(0, 2).toUpperCase();
1618
+ return `${first[0]}${parts[1][0]}`.toUpperCase();
1619
+ }
1451
1620
  function StudioChatPanel({
1452
1621
  meeting,
1453
1622
  selfId,
1623
+ participants,
1454
1624
  isModerator: isModerator2,
1455
1625
  labels
1456
1626
  }) {
@@ -1468,6 +1638,17 @@ function StudioChatPanel({
1468
1638
  );
1469
1639
  const pinned = react.useMemo(() => meeting.pinnedChatMessages(), [meeting, tick]);
1470
1640
  const limit = meeting.chatMaxTextLimit();
1641
+ const pictures = react.useMemo(() => {
1642
+ const map = /* @__PURE__ */ new Map();
1643
+ for (const row of participants) {
1644
+ if (row.userId) map.set(row.userId, row.picture);
1645
+ }
1646
+ return map;
1647
+ }, [participants]);
1648
+ const groups = react.useMemo(
1649
+ () => groupChatMessages(messages, selfId),
1650
+ [messages, selfId]
1651
+ );
1471
1652
  react.useEffect(() => {
1472
1653
  const list = listRef.current;
1473
1654
  if (!list) return;
@@ -1491,6 +1672,44 @@ function StudioChatPanel({
1491
1672
  );
1492
1673
  }
1493
1674
  }
1675
+ function messageActions(row, mine) {
1676
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1677
+ "p",
1678
+ {
1679
+ className: mine ? "text-muted-foreground flex flex-wrap justify-end gap-2 text-xs" : "text-muted-foreground flex flex-wrap gap-2 text-xs",
1680
+ children: [
1681
+ row.isEdited ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.chatEdited }) : null,
1682
+ mine ? /* @__PURE__ */ jsxRuntime.jsx(
1683
+ "button",
1684
+ {
1685
+ type: "button",
1686
+ onClick: () => {
1687
+ setEditingId(row.id);
1688
+ setDraft(row.text);
1689
+ },
1690
+ children: labels.chatEdit
1691
+ }
1692
+ ) : null,
1693
+ isModerator2 || mine ? /* @__PURE__ */ jsxRuntime.jsx(
1694
+ "button",
1695
+ {
1696
+ type: "button",
1697
+ onClick: () => void meeting.deleteChatMessage(row.id).catch(() => sonner.toast.error(labels.chatError)),
1698
+ children: labels.chatDelete
1699
+ }
1700
+ ) : null,
1701
+ isModerator2 ? /* @__PURE__ */ jsxRuntime.jsx(
1702
+ "button",
1703
+ {
1704
+ type: "button",
1705
+ onClick: () => void meeting.pinChatMessage(row.id, row.pinned !== true).catch(() => sonner.toast.error(labels.chatError)),
1706
+ children: row.pinned ? labels.chatUnpin : labels.chatPin
1707
+ }
1708
+ ) : null
1709
+ ]
1710
+ }
1711
+ );
1712
+ }
1494
1713
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [
1495
1714
  pinned[0] ? /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "status", className: "bg-muted border-b px-3 py-2 text-sm", children: [
1496
1715
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-xs", children: labels.chatPinned }),
@@ -1502,7 +1721,7 @@ function StudioChatPanel({
1502
1721
  ref: listRef,
1503
1722
  role: "log",
1504
1723
  "aria-live": "polite",
1505
- className: "flex min-h-0 flex-1 flex-col gap-2 overflow-auto p-3",
1724
+ className: "flex min-h-0 flex-1 flex-col gap-3 overflow-auto p-3",
1506
1725
  children: [
1507
1726
  /* @__PURE__ */ jsxRuntime.jsx(
1508
1727
  Button,
@@ -1520,48 +1739,42 @@ function StudioChatPanel({
1520
1739
  children: labels.chatLoadOlder
1521
1740
  }
1522
1741
  ),
1523
- messages.map((row) => {
1524
- const mine = Boolean(selfId) && row.userId === selfId;
1742
+ groups.map((group) => {
1743
+ const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
1744
+ /* @__PURE__ */ jsxRuntime.jsx(
1745
+ Bubble,
1746
+ {
1747
+ align: group.mine ? "end" : "start",
1748
+ variant: group.mine ? "default" : "secondary",
1749
+ children: /* @__PURE__ */ jsxRuntime.jsx(BubbleContent, { children: row.text })
1750
+ }
1751
+ ),
1752
+ messageActions(row, group.mine)
1753
+ ] }, row.id));
1754
+ if (group.mine) {
1755
+ return /* @__PURE__ */ jsxRuntime.jsx(BubbleGroup, { className: "items-end", children: bubbles }, group.rows[0].id);
1756
+ }
1757
+ const picture = pictures.get(group.userId) ?? null;
1525
1758
  return /* @__PURE__ */ jsxRuntime.jsxs(
1526
1759
  "div",
1527
1760
  {
1528
- className: mine ? "self-end text-right" : "self-start",
1761
+ className: "flex max-w-full gap-2 self-start",
1529
1762
  children: [
1530
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-xs", children: row.name }),
1531
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bg-muted max-w-64 rounded-lg px-2 py-1 text-sm", children: row.text }),
1532
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-muted-foreground flex flex-wrap justify-end gap-2 text-xs", children: [
1533
- row.isEdited ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.chatEdited }) : null,
1534
- mine ? /* @__PURE__ */ jsxRuntime.jsx(
1535
- "button",
1536
- {
1537
- type: "button",
1538
- onClick: () => {
1539
- setEditingId(row.id);
1540
- setDraft(row.text);
1541
- },
1542
- children: labels.chatEdit
1543
- }
1544
- ) : null,
1545
- isModerator2 || mine ? /* @__PURE__ */ jsxRuntime.jsx(
1546
- "button",
1547
- {
1548
- type: "button",
1549
- onClick: () => void meeting.deleteChatMessage(row.id).catch(() => sonner.toast.error(labels.chatError)),
1550
- children: labels.chatDelete
1551
- }
1552
- ) : null,
1553
- isModerator2 ? /* @__PURE__ */ jsxRuntime.jsx(
1554
- "button",
1555
- {
1556
- type: "button",
1557
- onClick: () => void meeting.pinChatMessage(row.id, row.pinned !== true).catch(() => sonner.toast.error(labels.chatError)),
1558
- children: row.pinned ? labels.chatUnpin : labels.chatPin
1559
- }
1560
- ) : null
1763
+ picture ? /* @__PURE__ */ jsxRuntime.jsx(
1764
+ "img",
1765
+ {
1766
+ src: picture,
1767
+ alt: "",
1768
+ className: "mt-0.5 size-7 shrink-0 rounded-full object-cover"
1769
+ }
1770
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bg-muted text-muted-foreground mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full text-xs", children: senderInitials(group.name) }),
1771
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
1772
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground mb-1 text-xs", children: group.name }),
1773
+ /* @__PURE__ */ jsxRuntime.jsx(BubbleGroup, { children: bubbles })
1561
1774
  ] })
1562
1775
  ]
1563
1776
  },
1564
- row.id
1777
+ group.rows[0].id
1565
1778
  );
1566
1779
  })
1567
1780
  ]
@@ -1691,7 +1904,9 @@ function StudioSidePanel({
1691
1904
  onDeny,
1692
1905
  onGrantStage,
1693
1906
  onDenyStage,
1694
- onTakeOffAir
1907
+ onTakeOffAir,
1908
+ guestUrl,
1909
+ onShareGuestLink
1695
1910
  }) {
1696
1911
  const panelists = participants.filter((row) => isOnStage(row.stageStatus));
1697
1912
  const audience = participants.filter((row) => !isOnStage(row.stageStatus));
@@ -1748,6 +1963,19 @@ function StudioSidePanel({
1748
1963
  labels
1749
1964
  }
1750
1965
  ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4 overflow-auto p-3", children: [
1966
+ guestUrl ? /* @__PURE__ */ jsxRuntime.jsxs(
1967
+ Button,
1968
+ {
1969
+ type: "button",
1970
+ variant: "outline",
1971
+ className: "w-full",
1972
+ onClick: onShareGuestLink,
1973
+ children: [
1974
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, {}),
1975
+ labels.shareLink
1976
+ ]
1977
+ }
1978
+ ) : null,
1751
1979
  /* @__PURE__ */ jsxRuntime.jsx(
1752
1980
  StudioWaitingRoom,
1753
1981
  {
@@ -2299,58 +2527,6 @@ function TooltipContent({
2299
2527
  }
2300
2528
  ) });
2301
2529
  }
2302
-
2303
- // src/studio/lib/clipboard.ts
2304
- async function copyText(value, clipboard = globalThis.navigator?.clipboard ?? null) {
2305
- if (!clipboard) return false;
2306
- try {
2307
- await clipboard.writeText(value);
2308
- return true;
2309
- } catch {
2310
- return false;
2311
- }
2312
- }
2313
-
2314
- // src/studio/lib/share.ts
2315
- function isAbort(error) {
2316
- return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
2317
- }
2318
- function resolveShareApi(share = typeof navigator === "undefined" ? null : typeof navigator.share === "function" ? navigator : null) {
2319
- return share ?? null;
2320
- }
2321
- function canNativeShare(data, share = resolveShareApi()) {
2322
- if (!share) return false;
2323
- if (typeof share.canShare !== "function") return true;
2324
- try {
2325
- return share.canShare(data);
2326
- } catch {
2327
- return false;
2328
- }
2329
- }
2330
- async function shareOrCopyLink(options, deps) {
2331
- const data = {
2332
- url: options.url,
2333
- ...options.title ? { title: options.title } : {},
2334
- ...options.text ? { text: options.text } : {}
2335
- };
2336
- const share = resolveShareApi(deps?.share);
2337
- if (canNativeShare(data, share) && share) {
2338
- try {
2339
- await share.share(data);
2340
- return "shared";
2341
- } catch (error) {
2342
- if (isAbort(error)) return "cancelled";
2343
- }
2344
- }
2345
- const copy = copyText;
2346
- const ok = await copy(options.url);
2347
- return ok ? "copied" : "failed";
2348
- }
2349
-
2350
- // src/studio/lib/embed-urls.ts
2351
- function playerWatchUrl(streamId, origin = "https://player.livo.tv") {
2352
- return `${origin.replace(/\/$/, "")}/${streamId}`;
2353
- }
2354
2530
  function Dialog({ ...props }) {
2355
2531
  return /* @__PURE__ */ jsxRuntime.jsx(DialogPrimitive__namespace.Root, { "data-slot": "dialog", ...props });
2356
2532
  }
@@ -2820,7 +2996,6 @@ function StudioToolbar({
2820
2996
  stageEnabled,
2821
2997
  canLeaveStage,
2822
2998
  reducedMotion,
2823
- playerOrigin,
2824
2999
  onToggleMute,
2825
3000
  onToggleCamera,
2826
3001
  onToggleScreen,
@@ -2944,41 +3119,6 @@ function StudioToolbar({
2944
3119
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Users, {})
2945
3120
  }
2946
3121
  ) : null,
2947
- /* @__PURE__ */ jsxRuntime.jsx(
2948
- ControlIcon,
2949
- {
2950
- label: labels.share,
2951
- onClick: () => {
2952
- void navigator.clipboard.writeText(
2953
- playerWatchUrl(session.stream.id, playerOrigin)
2954
- );
2955
- sonner.toast.success(labels.shareCopied);
2956
- },
2957
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, {})
2958
- }
2959
- ),
2960
- session.role === "moderator" && session.guestUrl ? /* @__PURE__ */ jsxRuntime.jsx(
2961
- ControlIcon,
2962
- {
2963
- label: labels.shareLink,
2964
- onClick: () => {
2965
- const url = session.guestUrl;
2966
- if (!url) return;
2967
- void shareOrCopyLink({
2968
- url,
2969
- title: session.stream.title,
2970
- text: labels.shareGuestText.replace(
2971
- "{title}",
2972
- session.stream.title
2973
- )
2974
- }).then((result) => {
2975
- if (result === "copied") sonner.toast.success(labels.copied);
2976
- if (result === "failed") sonner.toast.error(labels.shareFailed);
2977
- });
2978
- },
2979
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, {})
2980
- }
2981
- ) : null,
2982
3122
  /* @__PURE__ */ jsxRuntime.jsx(
2983
3123
  ControlIcon,
2984
3124
  {
@@ -3751,6 +3891,22 @@ function StudioRoom({
3751
3891
  trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", variant: "destructive", disabled: pending, children: labels.stop })
3752
3892
  }
3753
3893
  ) : null,
3894
+ session.role === "moderator" ? /* @__PURE__ */ jsxRuntime.jsx(
3895
+ Button,
3896
+ {
3897
+ type: "button",
3898
+ variant: "outline",
3899
+ size: "icon-sm",
3900
+ "aria-label": labels.share,
3901
+ onClick: () => {
3902
+ void navigator.clipboard.writeText(
3903
+ playerWatchUrl(session.stream.id, playerOrigin)
3904
+ );
3905
+ sonner.toast.success(labels.shareCopied);
3906
+ },
3907
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, {})
3908
+ }
3909
+ ) : null,
3754
3910
  embed ? null : chrome
3755
3911
  ] }),
3756
3912
  !stageEnabled && session.role === "guest" ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -3838,6 +3994,22 @@ function StudioRoom({
3838
3994
  onTakeOffAir: (userId) => {
3839
3995
  const tile = participants.find((row) => row.userId === userId);
3840
3996
  if (tile) void onSetStage(tile, false);
3997
+ },
3998
+ guestUrl: session.guestUrl,
3999
+ onShareGuestLink: () => {
4000
+ const url = session.guestUrl;
4001
+ if (!url) return;
4002
+ void shareOrCopyLink({
4003
+ url,
4004
+ title: session.stream.title,
4005
+ text: labels.shareGuestText.replace(
4006
+ "{title}",
4007
+ session.stream.title
4008
+ )
4009
+ }).then((result) => {
4010
+ if (result === "copied") sonner.toast.success(labels.copied);
4011
+ if (result === "failed") sonner.toast.error(labels.shareFailed);
4012
+ });
3841
4013
  }
3842
4014
  }
3843
4015
  ) : null
@@ -3856,7 +4028,6 @@ function StudioRoom({
3856
4028
  session,
3857
4029
  meeting,
3858
4030
  labels,
3859
- playerOrigin,
3860
4031
  muted,
3861
4032
  cameraOn,
3862
4033
  screenOn,
@@ -3886,9 +4057,9 @@ function StudioRoom({
3886
4057
  onCancelShareRequest: () => setShareGrant("idle"),
3887
4058
  onAskStage: () => {
3888
4059
  if (session.role === "moderator") {
3889
- const userId = self?.userId;
3890
- if (userId) void meeting.setStageAccess(userId, true);
3891
- else void meeting.requestStageAccess();
4060
+ void Promise.resolve(meeting.joinStage()).catch(
4061
+ () => sonner.toast.error(labels.stageError)
4062
+ );
3892
4063
  return;
3893
4064
  }
3894
4065
  void meeting.requestStageAccess();
@@ -4636,6 +4807,21 @@ function studioMeetingFromRtk(raw) {
4636
4807
  await meeting.stage?.requestAccess?.();
4637
4808
  notifyStage();
4638
4809
  },
4810
+ async joinStage() {
4811
+ if (currentStageStatus() === "ON_STAGE") {
4812
+ notifyStage();
4813
+ return;
4814
+ }
4815
+ if (meeting.self?.permissions?.stageAccess !== "ALLOWED") {
4816
+ await meeting.stage?.requestAccess?.();
4817
+ notifyStage();
4818
+ return;
4819
+ }
4820
+ await meeting.stage?.join?.();
4821
+ joinedStageLocally = true;
4822
+ await republishCameraIfOn();
4823
+ notifyStage();
4824
+ },
4639
4825
  async leaveStage() {
4640
4826
  await meeting.stage?.leave?.();
4641
4827
  notifyStage();