-
Notifications
You must be signed in to change notification settings - Fork 99
Decompose webSessionSetup into reusable parts (Phase 3) #788
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { describe, it, expect, vi } from "vitest"; | ||
|
|
||
| vi.mock("livekit-client", () => ({ | ||
| Room: vi.fn(), | ||
| RoomEvent: {}, | ||
| Track: { Kind: { Audio: "audio" }, Source: { Microphone: "microphone" } }, | ||
| ConnectionState: {}, | ||
| createLocalAudioTrack: vi.fn(), | ||
| })); | ||
|
|
||
| import { setupWebRTCSession } from "./VoiceSessionSetup.js"; | ||
| import { WebRTCConnection } from "../utils/WebRTCConnection.js"; | ||
| import { WebSocketConnection } from "../utils/WebSocketConnection.js"; | ||
|
|
||
| describe("setupWebRTCSession", () => { | ||
| it("returns input/output from a WebRTCConnection", () => { | ||
| const mockInput = { close: vi.fn(), setMuted: vi.fn() }; | ||
| const mockOutput = { close: vi.fn(), setVolume: vi.fn() }; | ||
|
|
||
| // Create a minimal object that passes the instanceof check | ||
| const connection = Object.create(WebRTCConnection.prototype, { | ||
| input: { value: mockInput }, | ||
| output: { value: mockOutput }, | ||
| }); | ||
|
|
||
| const result = setupWebRTCSession(connection); | ||
|
|
||
| expect(result.connection).toBe(connection); | ||
| expect(result.input).toBe(mockInput); | ||
| expect(result.output).toBe(mockOutput); | ||
| expect(result.playbackEventTarget).toBeNull(); | ||
| expect(result.detach).toBeTypeOf("function"); | ||
| }); | ||
|
|
||
| it("detach is a no-op", () => { | ||
| const connection = Object.create(WebRTCConnection.prototype, { | ||
| input: { value: {} }, | ||
| output: { value: {} }, | ||
| }); | ||
|
|
||
| const result = setupWebRTCSession(connection); | ||
| expect(() => result.detach()).not.toThrow(); | ||
| }); | ||
|
|
||
| it("throws when given a WebSocketConnection", () => { | ||
| const connection = Object.create(WebSocketConnection.prototype); | ||
|
|
||
| expect(() => setupWebRTCSession(connection)).toThrow( | ||
| "setupWebRTCSession requires a WebRTCConnection" | ||
| ); | ||
| }); | ||
|
|
||
| it("throws when given a plain object", () => { | ||
| const connection = { input: {}, output: {} } as any; | ||
|
|
||
| expect(() => setupWebRTCSession(connection)).toThrow( | ||
| "setupWebRTCSession requires a WebRTCConnection" | ||
| ); | ||
| }); | ||
|
|
||
| it("throws with a descriptive message when given null", () => { | ||
| expect(() => setupWebRTCSession(null as any)).toThrow( | ||
| "Received: object" | ||
| ); | ||
| }); | ||
|
|
||
| it("throws with a descriptive message when given undefined", () => { | ||
| expect(() => setupWebRTCSession(undefined as any)).toThrow( | ||
| "Received: undefined" | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import type { Options } from "../../BaseConversation.js"; | |
| import type { BaseConnection } from "../../utils/BaseConnection.js"; | ||
| import { | ||
| setSetupStrategy, | ||
| setupWebRTCSession, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused
|
||
| type VoiceSessionSetupResult, | ||
| } from "../VoiceSessionSetup.js"; | ||
| import { MediaDeviceOutput } from "./output.js"; | ||
|
|
@@ -13,53 +14,40 @@ import { attachConnectionToOutput } from "../../utils/attachConnectionToOutput.j | |
| import { createConnection } from "../../utils/ConnectionFactory.js"; | ||
|
|
||
| /** | ||
| * Sets up input and output controllers for an existing connection. | ||
| * Shared helper used by platform-specific setup strategies. | ||
| * Sets up WebSocket-specific input and output controllers using | ||
| * web MediaDevice APIs (AudioContext, AudioWorklet, etc.). | ||
| */ | ||
| export async function setupInputOutput( | ||
| async function setupWebSocketIO( | ||
| options: Options, | ||
| connection: BaseConnection | ||
| connection: WebSocketConnection | ||
| ): Promise<Omit<VoiceSessionSetupResult, "connection">> { | ||
| if (connection instanceof WebRTCConnection) { | ||
| return { | ||
| input: connection.input, | ||
| output: connection.output, | ||
| playbackEventTarget: null, | ||
| detach: () => {}, | ||
| }; | ||
| } else if (connection instanceof WebSocketConnection) { | ||
| const [input, output] = await Promise.all([ | ||
| MediaDeviceInput.create({ | ||
| ...connection.inputFormat, | ||
| preferHeadphonesForIosDevices: options.preferHeadphonesForIosDevices, | ||
| inputDeviceId: options.inputDeviceId, | ||
| workletPaths: options.workletPaths, | ||
| libsampleratePath: options.libsampleratePath, | ||
| }), | ||
| MediaDeviceOutput.create({ | ||
| ...connection.outputFormat, | ||
| outputDeviceId: options.outputDeviceId, | ||
| workletPaths: options.workletPaths, | ||
| }), | ||
| ]); | ||
| const [input, output] = await Promise.all([ | ||
| MediaDeviceInput.create({ | ||
| ...connection.inputFormat, | ||
| preferHeadphonesForIosDevices: options.preferHeadphonesForIosDevices, | ||
| inputDeviceId: options.inputDeviceId, | ||
| workletPaths: options.workletPaths, | ||
| libsampleratePath: options.libsampleratePath, | ||
| }), | ||
| MediaDeviceOutput.create({ | ||
| ...connection.outputFormat, | ||
| outputDeviceId: options.outputDeviceId, | ||
| workletPaths: options.workletPaths, | ||
| }), | ||
| ]); | ||
|
|
||
| const detachInput = attachInputToConnection(input, connection); | ||
| const detachOutput = attachConnectionToOutput(connection, output); | ||
| const detachInput = attachInputToConnection(input, connection); | ||
| const detachOutput = attachConnectionToOutput(connection, output); | ||
|
|
||
| return { | ||
| input, | ||
| output, | ||
| playbackEventTarget: output, | ||
| detach: () => { | ||
| detachInput(); | ||
| detachOutput(); | ||
| }, | ||
| }; | ||
| } else { | ||
| throw new Error( | ||
| `Unsupported connection type: ${connection.constructor.name}` | ||
| ); | ||
| } | ||
| return { | ||
| input, | ||
| output, | ||
| playbackEventTarget: output, | ||
| detach: () => { | ||
| detachInput(); | ||
| detachOutput(); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -70,8 +58,14 @@ export async function webSessionSetup( | |
| options: Options | ||
| ): Promise<VoiceSessionSetupResult> { | ||
| const connection = await createConnection(options); | ||
| const io = await setupInputOutput(options, connection); | ||
| return { connection, ...io }; | ||
|
|
||
| if (connection instanceof WebSocketConnection) { | ||
| const io = await setupWebSocketIO(options, connection); | ||
| return { connection, ...io }; | ||
| } | ||
|
|
||
| // WebRTC — platform-agnostic setup | ||
| return setupWebRTCSession(connection); | ||
| } | ||
|
|
||
| // Register the web strategy as the default | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,8 @@ import { | |
| import type { Options } from "@elevenlabs/client"; | ||
| import { | ||
| setSetupStrategy, | ||
| webSessionSetup, | ||
| createConnection, | ||
| setupWebRTCSession, | ||
| type VoiceSessionSetupResult, | ||
| } from "@elevenlabs/client/internal"; | ||
| import { attachNativeVolume } from "./nativeVolume.js"; | ||
|
|
@@ -18,13 +19,25 @@ registerGlobals(); | |
| * React Native voice session setup strategy. | ||
| * | ||
| * 1. Configures and starts the native AudioSession | ||
| * 2. Delegates connection + input/output setup to the web strategy | ||
| * 2. Creates a WebRTC connection and extracts its I/O controllers | ||
| * 3. Wraps input/output controllers with native volume processors | ||
| * 4. Wraps detach to stop the native AudioSession on cleanup | ||
| * | ||
| * Only WebRTC connections are supported on React Native. | ||
| * WebSocket connections require Web Audio APIs (AudioContext, | ||
| * AudioWorkletNode) that are not available in React Native. | ||
| */ | ||
| async function reactNativeSessionSetup( | ||
| options: Options | ||
| ): Promise<VoiceSessionSetupResult> { | ||
| if (options.connectionType === "websocket" || options.signedUrl) { | ||
| throw new Error( | ||
| "WebSocket connections are not supported on React Native. " + | ||
| "Only WebRTC connections are available. " + | ||
| "Remove the connectionType/signedUrl option or use connectionType: 'webrtc'." | ||
| ); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. React Native guard misses
|
||
|
|
||
| await AudioSession.configureAudio({ | ||
| android: { | ||
| preferredOutputList: ["speaker"], | ||
|
|
@@ -36,7 +49,8 @@ async function reactNativeSessionSetup( | |
| }); | ||
| await AudioSession.startAudioSession(); | ||
|
|
||
| const result = attachNativeVolume(await webSessionSetup(options)); | ||
| const connection = await createConnection(options); | ||
| const result = attachNativeVolume(setupWebRTCSession(connection)); | ||
|
|
||
|
Comment on lines
+52
to
54
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in b4f4512: added an early guard that checks for |
||
| const originalDetach = result.detach; | ||
| return { | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in Phase 4 (b4f4512 → 88eaf14): the web setup file was rewritten and
BaseConnectionis no longer imported.WebRTCConnectionis still used for theinstanceofcheck in the WebRTC branch.