Skip to content
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

Add custom delay #31

Merged
merged 7 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ jobs:
- name: Install dependencies
run: pnpm i

- name: Install pulseaudio
run: |
apt-get update
apt-get install -y pulseaudio
apt-get install sudo
sudo pulseaudio --start

- name: Run tests
run: pnpm run test
env:
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@
"fs-extra": "^11.2.0",
"husky": "^9.1.3",
"prettier": "^3.3.3"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"msw"
]
}
}
16 changes: 16 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,22 @@ const conversation = await Conversation.startSession({
});
```

#### Connection delay

You can configure additional delay between when the microphone is activated and when the connection is established.
On Android, the delay is set to 3 seconds by default to make sure the device has time to switch to the correct audio mode.
Without it, you may experience issues with the beginning of the first message being cut off.

```ts
const conversation = await Conversation.startSession({
connectionDelay: {
android: 3_000,
ios: 0,
default: 0,
},
});
```

#### Return value

`startSession` returns a `Conversation` instance that can be used to control the session. The method will throw an error if the session cannot be established. This can happen if the user denies microphone access, or if the websocket connection
Expand Down
6 changes: 3 additions & 3 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@11labs/client",
"version": "0.0.6",
"version": "0.0.7-beta.1",
"description": "ElevenLabs JavaScript Client Library",
"main": "./dist/lib.umd.js",
"module": "./dist/lib.module.js",
Expand Down Expand Up @@ -31,14 +31,14 @@
"license": "MIT",
"devDependencies": {
"@types/node-wav": "^0.0.3",
"@vitest/browser": "^2.0.5",
"@vitest/browser": "^3.0.5",
"eslint": "^9.8.0",
"microbundle": "^0.15.1",
"mock-socket": "^9.3.1",
"node-wav": "^0.0.2",
"playwright": "^1.46.1",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
"vitest": "^3.0.5"
},
"repository": {
"type": "git",
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe("Conversation", () => {
status = value.status;
},
onUnhandledClientToolCall,
connectionDelay: { default: 0 },
});
const client = await clientPromise;

Expand Down Expand Up @@ -190,6 +191,7 @@ describe("Conversation", () => {
await expect(async () => {
await Conversation.startSession({
signedUrl: "wss://api.elevenlabs.io/2",
connectionDelay: { default: 0 },
});
await clientPromise;
}).rejects.toThrowError(
Expand All @@ -212,6 +214,7 @@ describe("Conversation", () => {
Conversation.startSession({
signedUrl: "wss://api.elevenlabs.io/3",
onDisconnect: resolve,
connectionDelay: { default: 0 },
});
setTimeout(() => reject(new Error("timeout")), 5000);
});
Expand Down
25 changes: 23 additions & 2 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SessionConfig,
} from "./utils/connection";
import { ClientToolCallEvent, IncomingSocketEvent } from "./utils/events";
import { isAndroidDevice, isIosDevice } from "./utils/compatibility";

export type { IncomingSocketEvent } from "./utils/events";
export type { SessionConfig, DisconnectionDetails } from "./utils/connection";
Expand Down Expand Up @@ -78,14 +79,30 @@ export class Conversation {
let input: Input | null = null;
let connection: Connection | null = null;
let output: Output | null = null;
let preliminaryInputStream: MediaStream | null = null;

try {
// some browsers won't allow calling getSupportedConstraints or enumerateDevices
// before getting approval for microphone access
const preliminaryInputStream = await navigator.mediaDevices.getUserMedia({
preliminaryInputStream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
preliminaryInputStream?.getTracks().forEach(track => track.stop());

const delayConfig = options.connectionDelay ?? {
default: 0,
// Give the Android AudioManager enough time to switch to the correct audio mode
android: 3_000,
};
let delay = delayConfig.default;
if (isAndroidDevice()) {
delay = delayConfig.android ?? delay;
} else if (isIosDevice()) {
delay = delayConfig.ios ?? delay;
}

if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}

connection = await Connection.create(options);
[input, output] = await Promise.all([
Expand All @@ -96,9 +113,13 @@ export class Conversation {
Output.create(connection.outputFormat),
]);

preliminaryInputStream?.getTracks().forEach(track => track.stop());
preliminaryInputStream = null;

return new Conversation(fullOptions, connection, input, output);
} catch (error) {
fullOptions.onStatusChange({ status: "disconnected" });
preliminaryInputStream?.getTracks().forEach(track => track.stop());
connection?.close();
await input?.close();
await output?.close();
Expand Down
18 changes: 18 additions & 0 deletions packages/client/src/utils/compatibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function isIosDevice() {
return (
[
"iPad Simulator",
"iPhone Simulator",
"iPod Simulator",
"iPad",
"iPhone",
"iPod",
].includes(navigator.platform) ||
// iPad on iOS 13 detection
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
);
}

export function isAndroidDevice() {
return /android/i.test(navigator.userAgent);
}
5 changes: 5 additions & 0 deletions packages/client/src/utils/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export type SessionConfig = {
};
customLlmExtraBody?: any;
dynamicVariables?: Record<string, string | number | boolean>;
connectionDelay?: {
default: number;
android?: number;
ios?: number;
};
} & (
| { signedUrl: string; agentId?: undefined }
| { agentId: string; signedUrl?: undefined }
Expand Down
18 changes: 3 additions & 15 deletions packages/client/src/utils/input.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { rawAudioProcessor } from "./rawAudioProcessor";
import { FormatConfig } from "./connection";
import { isIosDevice } from "./compatibility";

export type InputConfig = {
preferHeadphonesForIosDevices?: boolean;
Expand All @@ -8,21 +9,6 @@ export type InputConfig = {
const LIBSAMPLERATE_JS =
"https://cdn.jsdelivr.net/npm/@alexanderolsen/libsamplerate-js@2.1.2/dist/libsamplerate.worklet.js";

function isIosDevice() {
return (
[
"iPad Simulator",
"iPhone Simulator",
"iPod Simulator",
"iPad",
"iPhone",
"iPod",
].includes(navigator.platform) ||
// iPad on iOS 13 detection
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
);
}

export class Input {
public static async create({
sampleRate,
Expand Down Expand Up @@ -79,6 +65,8 @@ export class Input {
source.connect(analyser);
analyser.connect(worklet);

await context.resume();

return new Input(context, analyser, worklet, inputStream);
} catch (error) {
inputStream?.getTracks().forEach(track => track.stop());
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/utils/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export class Output {
worklet.port.postMessage({ type: "setFormat", format });
worklet.connect(gain);

await context.resume();

return new Output(context, analyser, gain, worklet);
} catch (error) {
context?.close();
Expand Down
54 changes: 25 additions & 29 deletions packages/client/vitest.workspace.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,39 @@
/// <reference types="@vitest/browser/providers/playwright" />

import { defineWorkspace } from "vitest/config";

export default defineWorkspace([
{
test: {
name: "Chromium",
name: "Browser tests",
browser: {
provider: "playwright",
enabled: true,
name: "chromium",
providerOptions: {
launch: {
args: [
"--use-fake-device-for-media-stream",
"--use-fake-ui-for-media-stream",
],
},
context: {
permissions: ["microphone"],
instances: [
{
browser: "chromium",
launch: {
args: [
"--use-fake-device-for-media-stream",
"--use-fake-ui-for-media-stream",
],
},
context: {
permissions: ["microphone"],
},
},
},
},
},
},
{
test: {
name: "Firefox",
browser: {
provider: "playwright",
enabled: true,
name: "firefox",
providerOptions: {
launch: {
firefoxUserPrefs: {
"permissions.default.microphone": 1,
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true,
{
browser: "firefox",
headless: true,
launch: {
firefoxUserPrefs: {
"permissions.default.microphone": 1,
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true,
},
},
},
},
],
},
},
},
Expand Down
16 changes: 16 additions & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@ const conversation = useConversation({
});
```

#### Connection delay

You can configure additional delay between when the microphone is activated and when the connection is established.
On Android, the delay is set to 3 seconds by default to make sure the device has time to switch to the correct audio mode.
Without it, you may experience issues with the beginning of the first message being cut off.

```ts
const conversation = useConversation({
connectionDelay: {
android: 3_000,
ios: 0,
default: 0,
},
});
```

#### Methods

##### startConversation
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@11labs/react",
"version": "0.0.6",
"version": "0.0.7-beta.1",
"description": "ElevenLabs React Library",
"main": "./dist/lib.umd.js",
"module": "./dist/lib.module.js",
Expand Down
Loading