-
Notifications
You must be signed in to change notification settings - Fork 106
feat: added call-sdk package #187
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
base: main
Are you sure you want to change the base?
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,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", | ||||||||||||||||||
| "react": "^19.1.1", | ||||||||||||||||||
| "react-dom": "^19.1.1" | ||||||||||||||||||
| }, | ||||||||||||||||||
|
Comment on lines
+27
to
+31
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. 🛠️ Refactor suggestion Review React version consistency. There's a potential version conflict: the Consider moving React to "dependencies": {
- "mediasoup-client": "^3.14.0",
- "react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "mediasoup-client": "^3.14.0"
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| "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" | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| 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 | ||||||||||||
|
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. Replace 'any' type with proper CallClient type. Using +import type { CallClient } from '../services/call-client';
// Advanced
- callClient: any; // CallClient instance for advanced usage
+ callClient: CallClient; // CallClient instance for advanced usage📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * 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, | ||||||||||||
| }; | ||||||||||||
| } | ||||||||||||
| 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
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. 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 |
||
| } | ||
| }, []); | ||
|
|
||
| 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, | ||
| }; | ||
| } | ||
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.
💡 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:
💡 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:
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)
"mediasoup-client": "^3.14.0","mediasoup-client": "^3.18.0",🤖 Prompt for AI Agents