Skip to content
Open
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
52 changes: 52 additions & 0 deletions packages/call-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@call0/call-sdk",
"version": "0.1.0",
"description": "Video calling SDK for Call0 using MediaSoup",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"lint": "eslint \"src/**/*.ts*\"",
"lint:fix": "eslint \"src/**/*.ts*\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"typecheck": "tsc --noEmit"
},
"keywords": [
"webrtc",
"mediasoup",
"video",
"call",
"sdk"
],
"author": "Call0",
"license": "MIT",
"dependencies": {
"mediasoup-client": "^3.14.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify mediasoup-client version compatibility.

Ensure that mediasoup-client version 3.14.0 is compatible with the latest WebRTC standards and doesn't have known security vulnerabilities.


🌐 Web query:

What is the latest version of mediasoup-client and are there any security advisories for version 3.14.0?

💡 Result:

The latest version of mediasoup-client as of August 2025 is 3.18.0[10]. For mediasoup specifically (the server component), the most recent version is 3.16.6, according to the Snyk vulnerability database, but mediasoup-client 3.18.0 was released July 31, 2025[10][4].

Regarding security advisories for mediasoup-client version 3.14.0, there are no public reports of direct security vulnerabilities specifically affecting mediasoup-client 3.14.0 in current vulnerability databases as of August 2025[4]. The Snyk advisories page for mediasoup lists recent versions but does not show unresolved vulnerabilities for 3.14.0[4]. However, as mediasoup and mediasoup-client are tightly coordinated projects, and the changelog documents ongoing bug fixes and improvements, it is recommended you check the official mediasoup GitHub and relevant security trackers for any targeted advisories before deploying or maintaining version 3.14.0[2].

In summary:

  • Latest mediasoup-client: 3.18.0 (released July 31, 2025)[10].
  • No public, unresolved security advisories specifically for mediasoup-client 3.14.0[4].
  • Upgrading to the latest stable release is recommended, as upstream updates sometimes include security-related protocol and compatibility fixes[2][10].

Citations:


Upgrade mediasoup-client to the latest stable release (3.18.0)

Although there are no public security advisories for 3.14.0, the latest v3.18.0 (released July 31, 2025) includes compatibility and bug-fix updates against evolving WebRTC standards. Please update the dependency to benefit from these upstream improvements.

• packages/call-sdk/package.json (line 28)

  • Change
    "mediasoup-client": "^3.14.0",
  • To
    "mediasoup-client": "^3.18.0",
🤖 Prompt for AI Agents
In packages/call-sdk/package.json at line 28, update the mediasoup-client
dependency version from "^3.14.0" to "^3.18.0" to use the latest stable release
with improved compatibility and bug fixes. Simply replace the version string to
"^3.18.0" and save the file.

"react": "^19.1.1",
"react-dom": "^19.1.1"
},
Comment on lines +27 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Review React version consistency.

There's a potential version conflict: the dependencies specify React ^19.1.1 but peerDependencies allow >=18.0.0. This could cause issues for consumers using React 18.

Consider moving React to peerDependencies only or align the versions:

  "dependencies": {
-    "mediasoup-client": "^3.14.0",
-    "react": "^19.1.1",
-    "react-dom": "^19.1.1"
+    "mediasoup-client": "^3.14.0"
  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"dependencies": {
"mediasoup-client": "^3.14.0",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"dependencies": {
"mediasoup-client": "^3.14.0"
},
🤖 Prompt for AI Agents
In packages/call-sdk/package.json around lines 27 to 31, the React version in
dependencies (^19.1.1) conflicts with the peerDependencies allowing >=18.0.0,
which may cause version mismatches for consumers. To fix this, remove React and
react-dom from dependencies and instead specify them only in peerDependencies
with a consistent version range that includes both React 18 and 19, ensuring
compatibility and avoiding duplicate React installations.

"devDependencies": {
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3",
"jest": "^30.0.5",
"prettier": "^3.6.2",
"tsup": "^8.5.0",
"typescript": "^5.9.2"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"publishConfig": {
"access": "public"
}
}
151 changes: 151 additions & 0 deletions packages/call-sdk/src/hooks/useCall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { useCallback, useEffect, useState } from "react";
import { useCall as useCallContext } from "../providers/CallProvider";
import type { CallConfig, CallStats } from "../types/call";
import type { Participant, Self } from "../types/participant";

export interface UseCallReturn {
// Connection state
isConnected: boolean;
isConnecting: boolean;
isReconnecting: boolean;
connectionError?: Error;

// Participants
self: Self | null;
participants: Participant[];
participantCount: number;
dominantSpeaker?: Participant;
pinnedParticipant?: Participant;

// Media state
localStream: MediaStream | null;
isMicrophoneEnabled: boolean;
isCameraEnabled: boolean;
isScreenShareEnabled: boolean;

// Call actions
joinCall: (config: CallConfig) => Promise<void>;
leaveCall: () => Promise<void>;

// Media controls
toggleMicrophone: () => Promise<boolean>;
toggleCamera: () => Promise<boolean>;
startScreenShare: () => Promise<void>;
stopScreenShare: () => Promise<void>;

// Participant management
pinParticipant: (participantId: string) => void;
unpinParticipant: () => void;
getParticipantById: (participantId: string) => Participant | undefined;

// Device management
changeAudioDevice: (deviceId: string) => Promise<void>;
changeVideoDevice: (deviceId: string) => Promise<void>;

// Call statistics
stats?: CallStats;

// Advanced
callClient: any; // CallClient instance for advanced usage
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace 'any' type with proper CallClient type.

Using any breaks type safety and defeats the purpose of TypeScript. Import and use the actual CallClient type.

+import type { CallClient } from '../services/call-client';

  // Advanced
-  callClient: any; // CallClient instance for advanced usage
+  callClient: CallClient; // CallClient instance for advanced usage
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
callClient: any; // CallClient instance for advanced usage
import type { CallClient } from '../services/call-client';
// Advanced
callClient: CallClient; // CallClient instance for advanced usage
🤖 Prompt for AI Agents
In packages/call-sdk/src/hooks/useCall.ts at line 49, the callClient variable is
typed as 'any', which breaks type safety. Import the CallClient type from its
module and replace 'any' with this proper type annotation to ensure type safety
and better code clarity.

}

/**
* Main hook for call functionality
* Provides a comprehensive interface for managing video calls
*/
export function useCall(): UseCallReturn {
const context = useCallContext();
const [isReconnecting, setIsReconnecting] = useState(false);

// Derived state
const participantsArray = Array.from(context.participants.values());
const dominantSpeaker = context.dominantSpeakerId
? context.participants.get(context.dominantSpeakerId)
: undefined;
const pinnedParticipant = context.pinnedParticipantId
? context.participants.get(context.pinnedParticipantId)
: undefined;

// Media state derived from self
const isMicrophoneEnabled = context.self?.audioEnabled ?? false;
const isCameraEnabled = context.self?.videoEnabled ?? false;
const isScreenShareEnabled = context.self?.screenShareEnabled ?? false;

// Handle reconnection logic
useEffect(() => {
if (context.callState.connectionStatus === "reconnecting") {
setIsReconnecting(true);
} else {
setIsReconnecting(false);
}
}, [context.callState.connectionStatus]);

// Device management helpers
const changeAudioDevice = useCallback(
async (deviceId: string) => {
await context.changeDevice("audio", deviceId);
},
[context.changeDevice]
);

const changeVideoDevice = useCallback(
async (deviceId: string) => {
await context.changeDevice("video", deviceId);
},
[context.changeDevice]
);

// Participant helpers
const getParticipantById = useCallback(
(participantId: string) => {
return context.participants.get(participantId);
},
[context.participants]
);

return {
// Connection state
isConnected: context.isConnected,
isConnecting: context.isConnecting,
isReconnecting,
connectionError: context.error,

// Participants
self: context.self,
participants: participantsArray,
participantCount: participantsArray.length + (context.self ? 1 : 0),
dominantSpeaker,
pinnedParticipant,

// Media state
localStream: context.localStream,
isMicrophoneEnabled,
isCameraEnabled,
isScreenShareEnabled,

// Call actions
joinCall: context.joinCall,
leaveCall: context.leaveCall,

// Media controls
toggleMicrophone: context.toggleMicrophone,
toggleCamera: context.toggleCamera,
startScreenShare: context.startScreenShare,
stopScreenShare: context.stopScreenShare,

// Participant management
pinParticipant: context.pinParticipant,
unpinParticipant: context.unpinParticipant,
getParticipantById,

// Device management
changeAudioDevice,
changeVideoDevice,

// Call statistics
stats: context.stats,

// Advanced
callClient: context.callClient,
};
}
169 changes: 169 additions & 0 deletions packages/call-sdk/src/hooks/useMediaDevices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { useState, useEffect, useCallback } from "react";

export interface MediaDeviceState {
audioInputDevices: MediaDeviceInfo[];
audioOutputDevices: MediaDeviceInfo[];
videoInputDevices: MediaDeviceInfo[];
selectedAudioInput: string | null;
selectedAudioOutput: string | null;
selectedVideoInput: string | null;
hasAudioPermission: boolean;
hasVideoPermission: boolean;
isLoading: boolean;
error: Error | null;
}

const initialState: MediaDeviceState = {
audioInputDevices: [],
audioOutputDevices: [],
videoInputDevices: [],
selectedAudioInput: null,
selectedAudioOutput: null,
selectedVideoInput: null,
hasAudioPermission: false,
hasVideoPermission: false,
isLoading: true,
error: null,
};

export function useMediaDevices() {
const [state, setState] = useState<MediaDeviceState>(initialState);

const updateDevices = useCallback(async () => {
try {
setState((prev) => ({ ...prev, isLoading: true, error: null }));

const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputs = devices.filter(
(device) => device.kind === "audioinput"
);
const audioOutputs = devices.filter(
(device) => device.kind === "audiooutput"
);
const videoInputs = devices.filter(
(device) => device.kind === "videoinput"
);

setState((prev) => ({
...prev,
audioInputDevices: audioInputs,
audioOutputDevices: audioOutputs,
videoInputDevices: videoInputs,
selectedAudioInput:
prev.selectedAudioInput || audioInputs[0]?.deviceId || null,
selectedAudioOutput:
prev.selectedAudioOutput || audioOutputs[0]?.deviceId || null,
selectedVideoInput:
prev.selectedVideoInput || videoInputs[0]?.deviceId || null,
isLoading: false,
}));
} catch (error) {
setState((prev) => ({
...prev,
isLoading: false,
error: error as Error,
}));
}
}, []);

const checkPermissions = useCallback(async () => {
try {
const audioStream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
audioStream.getTracks().forEach((track) => track.stop());
const videoStream = await navigator.mediaDevices.getUserMedia({
video: true,
});
videoStream.getTracks().forEach((track) => track.stop());

setState((prev) => ({
...prev,
hasAudioPermission: true,
hasVideoPermission: true,
}));
} catch (error) {
const permissionError = error as Error;
setState((prev) => ({
...prev,
hasAudioPermission: !permissionError.message.includes("audio"),
hasVideoPermission: !permissionError.message.includes("video"),
}));
Comment on lines +86 to +91
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Unreliable permission detection logic

The current implementation assumes permissions are granted based on whether the error message contains "audio" or "video". This is browser-dependent and unreliable.

Consider a more robust approach:

   } catch (error) {
-    const permissionError = error as Error;
-    setState((prev) => ({
-      ...prev,
-      hasAudioPermission: !permissionError.message.includes("audio"),
-      hasVideoPermission: !permissionError.message.includes("video"),
-    }));
+    // Try each permission separately to determine which failed
+    let hasAudioPermission = false;
+    let hasVideoPermission = false;
+    
+    try {
+      const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
+      audioStream.getTracks().forEach((track) => track.stop());
+      hasAudioPermission = true;
+    } catch {}
+    
+    try {
+      const videoStream = await navigator.mediaDevices.getUserMedia({ video: true });
+      videoStream.getTracks().forEach((track) => track.stop());
+      hasVideoPermission = true;
+    } catch {}
+    
+    setState((prev) => ({
+      ...prev,
+      hasAudioPermission,
+      hasVideoPermission,
+    }));
   }
🤖 Prompt for AI Agents
In packages/call-sdk/src/hooks/useMediaDevices.ts around lines 86 to 91, the
current logic checks permission by inspecting error message strings for "audio"
or "video", which is unreliable and browser-dependent. Replace this with a more
robust method by checking the error's name or code properties against known
permission error identifiers, or use the Permissions API if available, to
accurately determine audio and video permission statuses before updating the
state.

}
}, []);

useEffect(() => {
checkPermissions();
updateDevices();

// Listen for device changes
navigator.mediaDevices.addEventListener("devicechange", updateDevices);

return () => {
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
};
}, [checkPermissions, updateDevices]);

const setAudioInput = useCallback(async (deviceId: string) => {
try {
// Test if we can access the device
const stream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: deviceId } },
});
stream.getTracks().forEach((track) => track.stop());

setState((prev) => ({
...prev,
selectedAudioInput: deviceId,
}));
} catch (error) {
setState((prev) => ({
...prev,
error: error as Error,
}));
throw error;
}
}, []);

const setAudioOutput = useCallback((deviceId: string) => {
setState((prev) => ({
...prev,
selectedAudioOutput: deviceId,
}));
}, []);

const setVideoInput = useCallback(async (deviceId: string) => {
try {
// Test if we can access the device
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: deviceId } },
});
stream.getTracks().forEach((track) => track.stop());

setState((prev) => ({
...prev,
selectedVideoInput: deviceId,
}));
} catch (error) {
setState((prev) => ({
...prev,
error: error as Error,
}));
throw error;
}
}, []);

const requestPermissions = useCallback(async () => {
await checkPermissions();
await updateDevices();
}, [checkPermissions, updateDevices]);

return {
...state,
setAudioInput,
setAudioOutput,
setVideoInput,
refreshDevices: updateDevices,
requestPermissions,
};
}
Loading