feat(react): Embeddable/pre-built video components#2117
Conversation
- add feedback form
- add copy invite link
- small improvements
- add theme support
- simplify skip lobby
- re-create client when name is changed
- hide buttons when video is disabled in dashboard
- delete comments
- improve livestream waiting room - remove option for name change in Lobby
- add configuration context
- improve state transitions
- improve state transitions
- add layout as initial prop to EmbeddedStreamClient.tsx
- improve config context
- improve error handling
- submit feedback
- submit feedback improvements
- submit feedback improvements
- submit feedback improvements
- add MicCaptureErrorNotification
- cleanup Livestream
- lift check up for call started_at
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
packages/react-sdk/src/embedded/livestream/viewer/ViewerLayout.tsx (2)
144-155: Consider managing focus when the participants sidebar opens.The toggle button correctly sets
aria-pressed, but when the sidebar opens, focus remains on the toggle. Keyboard and screen-reader users would benefit from focus being moved into theCallParticipantsList(or its close button) when it appears, and returned to the toggle when it closes. This is a minor accessibility improvement that can be deferred.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-sdk/src/embedded/livestream/viewer/ViewerLayout.tsx` around lines 144 - 155, When the participants sidebar opens/ closes, move keyboard focus so screen-reader and keyboard users aren't left on the toggle: add a ref to the CompositeButton used for the toggle (reference the existing CompositeButton and handleToggleParticipants) and add a ref inside the CallParticipantsList (e.g., to its first focusable element or its close button). Use an effect (watching the showParticipants boolean) to focus the CallParticipantsList element when showParticipants becomes true and to return focus to the CompositeButton ref when it becomes false; ensure the CallParticipantsList component exposes a focusable element via a forwarded ref or a prop so the effect can call .focus().
89-101: Extract the duplicated live-info block into a shared sub-component.The live badge +
ViewersCount+ elapsed time JSX (lines 51–60) is duplicated verbatim here (lines 90–99). Extracting it into a smallLiveInfocomponent (or a local render helper) would eliminate the duplication and make future changes safer.♻️ Example extraction
+const LiveInfo = ({ elapsed, participantCount, t }: { + elapsed: string | undefined; + participantCount: number; + t: (key: string) => string; +}) => ( + <div className="str-video__embedded-livestream-duration"> + <span className="str-video__embedded-livestream-duration__live-badge"> + {t('Live')} + </span> + <ViewersCount count={participantCount} /> + {elapsed && ( + <span className="str-video__embedded-livestream-duration__elapsed"> + {elapsed} + </span> + )} + </div> +);Then use
<LiveInfo ... />in both the header and the desktop controls section.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-sdk/src/embedded/livestream/viewer/ViewerLayout.tsx` around lines 89 - 101, The live-info JSX (Live badge + ViewersCount + elapsed) duplicated in ViewerLayout.tsx should be extracted into a small shared component (e.g., function LiveInfo) that accepts props like count (participantCount) and elapsed; implement LiveInfo to render the same structure (including calling t('Live') or accepting a label prop) and replace both inline blocks (the header and the .str-video__embedded-desktop section) with <LiveInfo count={participantCount} elapsed={elapsed} /> so future changes are made in one place and prop names match existing symbols ViewersCount, participantCount, elapsed and the i18n usage.packages/react-sdk/package.json (1)
8-12: Move"types"before"import"in the"."export entry.The TypeScript 4.7 docs explicitly state: "The 'types' condition should always come first in 'exports'." In the
"."entry,"types"is last, whereas"./embedded"already has it first. The"types"field must come before other conditions for TypeScript to pick it up correctly — if it's placed after, TypeScript will ignore it when consumers usemoduleResolution: "bundler","node16", or"nodenext"(which evaluate conditions in key order). The top-level"types"field provides a fallback for older tooling but not for these modern resolution modes.♻️ Proposed fix
".": { + "types": "./dist/index.d.ts", "import": "./dist/index.es.js", "require": "./dist/index.cjs.js", - "types": "./dist/index.d.ts" },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-sdk/package.json` around lines 8 - 12, Reorder the exports entry for the "." export in package.json so that the "types" condition appears before "import" and "require"; specifically update the object keys within the "." export entry (currently listing "import", "require", "types") to place "types" first (e.g., "types", "import", "require") so TypeScript's condition evaluation picks up the declaration file for modern moduleResolution modes; mirror the same ordering used in the "./embedded" export entry.packages/react-sdk/src/embedded/call/CallControls.tsx (1)
51-61: Duration display is duplicated betweenCallHeaderandCallControls.Both components independently call
useCallSession()anduseCallDuration(startedAt)to render nearly identical duration markup (icon + elapsed span). This appears intentional for responsive layouts (header vs. control bar), but the repeated hook calls and JSX could be extracted into a small shared<CallDuration />presentational component to keep things DRY.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-sdk/src/embedded/call/CallControls.tsx` around lines 51 - 61, Extract the duplicated duration UI and hook usage into a small presentational component (e.g., CallDuration) and replace the inline markup in both CallControls and CallHeader with that component; specifically, move the useCallSession() / useCallDuration(startedAt) logic and the Icon + span JSX into CallDuration, export it from the same folder, and import/use <CallDuration /> in place of the existing duplicated blocks in CallControls.tsx and CallHeader so both components render the same duration UI without repeating hook calls or JSX.packages/react-sdk/src/embedded/livestream/host/HostLayout.tsx (1)
9-9: Consolidate the two imports from../../hooksinto one.
useLayout(Line 9) anduseCallDuration(Line 26) are both imported from the same path.♻️ Proposed fix
-import { useLayout } from '../../hooks'; import { ... } from '../../../components'; -import { useCallDuration } from '../../hooks'; +import { useCallDuration, useLayout } from '../../hooks';Also applies to: 26-26
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-sdk/src/embedded/livestream/host/HostLayout.tsx` at line 9, Import consolidation: combine the two separate imports from '../../hooks' into a single import statement that includes both useLayout and useCallDuration to avoid duplicate module resolutions; update the import at the top of HostLayout.tsx so the single import lists useLayout and useCallDuration together (referencing the existing useLayout and useCallDuration symbols) and remove the extra import line.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/react-sdk/src/embedded/livestream/host/HostLayout.tsx`:
- Around line 77-95: Wrap or update the livestreamStatus container (the JSX
constant livestreamStatus in HostLayout/HostLayout.tsx) to be an ARIA live
region so screen readers are notified when the status or elapsed time changes:
add an appropriate aria-live value (e.g., "polite" or "assertive" depending on
urgency) and aria-atomic="true" to the element that holds the status text (the
div containing the Live/Backstage span, ViewersCount, and elapsed span) so
dynamic changes to the Live/Backstage label and elapsed time are announced;
ensure you do this on the same element rendered as livestreamStatus so
ViewersCount remains unchanged.
- Around line 179-196: The Stop/Go Live buttons in HostLayout.tsx are missing
explicit types and may submit ancestor forms; update the two button elements
with classNames "str-video__embedded-end-stream-button" and
"str-video__embedded-go-live-button" to include type="button" (the buttons that
call onStopLive and onGoLive respectively) so they do not default to
type="submit".
- Around line 33-46: The SVG in the StartBroadcastIcon component is decorative
and should not be announced by screen readers; update the <svg> element inside
StartBroadcastIcon to include aria-hidden="true" (and optionally
focusable="false" for older IE) so only the button's visible text is announced,
keeping the component unchanged otherwise.
---
Nitpick comments:
In `@packages/react-sdk/package.json`:
- Around line 8-12: Reorder the exports entry for the "." export in package.json
so that the "types" condition appears before "import" and "require";
specifically update the object keys within the "." export entry (currently
listing "import", "require", "types") to place "types" first (e.g., "types",
"import", "require") so TypeScript's condition evaluation picks up the
declaration file for modern moduleResolution modes; mirror the same ordering
used in the "./embedded" export entry.
In `@packages/react-sdk/src/embedded/call/CallControls.tsx`:
- Around line 51-61: Extract the duplicated duration UI and hook usage into a
small presentational component (e.g., CallDuration) and replace the inline
markup in both CallControls and CallHeader with that component; specifically,
move the useCallSession() / useCallDuration(startedAt) logic and the Icon + span
JSX into CallDuration, export it from the same folder, and import/use
<CallDuration /> in place of the existing duplicated blocks in CallControls.tsx
and CallHeader so both components render the same duration UI without repeating
hook calls or JSX.
In `@packages/react-sdk/src/embedded/livestream/host/HostLayout.tsx`:
- Line 9: Import consolidation: combine the two separate imports from
'../../hooks' into a single import statement that includes both useLayout and
useCallDuration to avoid duplicate module resolutions; update the import at the
top of HostLayout.tsx so the single import lists useLayout and useCallDuration
together (referencing the existing useLayout and useCallDuration symbols) and
remove the extra import line.
In `@packages/react-sdk/src/embedded/livestream/viewer/ViewerLayout.tsx`:
- Around line 144-155: When the participants sidebar opens/ closes, move
keyboard focus so screen-reader and keyboard users aren't left on the toggle:
add a ref to the CompositeButton used for the toggle (reference the existing
CompositeButton and handleToggleParticipants) and add a ref inside the
CallParticipantsList (e.g., to its first focusable element or its close button).
Use an effect (watching the showParticipants boolean) to focus the
CallParticipantsList element when showParticipants becomes true and to return
focus to the CompositeButton ref when it becomes false; ensure the
CallParticipantsList component exposes a focusable element via a forwarded ref
or a prop so the effect can call .focus().
- Around line 89-101: The live-info JSX (Live badge + ViewersCount + elapsed)
duplicated in ViewerLayout.tsx should be extracted into a small shared component
(e.g., function LiveInfo) that accepts props like count (participantCount) and
elapsed; implement LiveInfo to render the same structure (including calling
t('Live') or accepting a label prop) and replace both inline blocks (the header
and the .str-video__embedded-desktop section) with <LiveInfo
count={participantCount} elapsed={elapsed} /> so future changes are made in one
place and prop names match existing symbols ViewersCount, participantCount,
elapsed and the i18n usage.
- rename style to theme
- coderabbit fixes
- coderabbit fixes
- add missing styles and fix types
- remove custom sorting and unused ref
- reorder mic and speaker in device controls
- lift state up for handling join errors and introduce seperate screen
- add '@stream-io/audio-filters-web' as external dept
- fix issue with undefined device list
- fix issue with undefined device list
- use ifInvisibleOrUnknownBy for livestream preset
- cleanup imports
- adjust client to new types
- adjust client to new types
- adjust client to new types
- adjust client to new types
💡 Overview
This PR adds two new components
<EmbeddedMeeting />and<EmbeddedLivestream />that embed a complete video experience from a single tag. The consumer provides configuration like apiKey, callId, user credentials, layout options and the component handles client and call initialization, provider wiring, and all state transitions from lobby to active call to post-call cleanup. Both components ship as a separate bundle entry point so existing SDK consumers are unaffected.📝 Implementation notes
🎫 Ticket: https://linear.app/stream/issue/REACT-753/stream-video-embedded
📑 Docs: https://github.com/GetStream/docs-content/pull/1007
Summary by CodeRabbit
New Features
Packaging
Style & Translations