= T extends T ? keyof T : never;
+export type Exact = P extends Builtin ? P
+ : P & { [K in keyof P]: Exact
} & { [K in Exclude>]: never };
+
+function isSet(value: any): boolean {
+ return value !== null && value !== undefined;
+}
diff --git a/packages/sdk-core/src/lib.ts b/packages/sdk-core/src/lib.ts
index 05cd72c..a89c49d 100644
--- a/packages/sdk-core/src/lib.ts
+++ b/packages/sdk-core/src/lib.ts
@@ -45,3 +45,6 @@ export { ServerEvent_Receiver_VoiceActivity as TrackReceiverVoiceActivity } from
export { Mode as AudioMixerMode } from "./generated/protobuf/features.mixer";
export { Receiver_Source as AudioMixerSource } from "./generated/protobuf/shared";
+
+export { SipIncomingCall, type IncomingSipCallStatus } from "./sip_incoming";
+export { SipOutgoingCall, type OutgoingSipCallStatus } from "./sip_outgoing";
\ No newline at end of file
diff --git a/packages/sdk-core/src/session.ts b/packages/sdk-core/src/session.ts
index 6336319..beca912 100644
--- a/packages/sdk-core/src/session.ts
+++ b/packages/sdk-core/src/session.ts
@@ -55,7 +55,7 @@ export class Session extends EventEmitter {
conn_id?: string;
receivers: TrackReceiver[] = [];
senders: TrackSender[] = [];
- msgChannels: Map = new Map();
+ msgChannels: Map = new Map();
_mixer?: mixer.AudioMixer;
/// Prepaer state for flagging when ever this peer is created offer.
@@ -68,7 +68,7 @@ export class Session extends EventEmitter {
) {
super();
this.created_at = new Date().getTime();
- console.warn("Create session", this.created_at);
+ console.info("Create session", this.created_at);
this.peer = new RTCPeerConnection();
this.dc = new Datachannel(
this.peer.createDataChannel("data", { negotiated: true, id: 0 }),
@@ -195,7 +195,7 @@ export class Session extends EventEmitter {
}
this.prepareState = false;
this.version = version;
- console.warn("Prepare senders and receivers to connect");
+ console.info("Prepare senders and receivers to connect");
//prepare for senders. We need to lazy prepare because some transceiver dont allow update before connected
for (let i = 0; i < this.senders.length; i++) {
console.log("Prepare sender ", this.senders[i]!.name);
@@ -358,9 +358,9 @@ export class Session extends EventEmitter {
config?: MessageChannelConfig | undefined,
) {
await this.dc.ready();
- console.warn("[MessageChannel] creating a new channel:", key);
+ console.info("[MessageChannel] creating a new channel:", key);
if (this.msgChannels.has(key)) {
- console.warn("[MessageChannel] a channel already exist with key:", key);
+ console.info("[MessageChannel] a channel already exist with key:", key);
return this.msgChannels.get(key)!;
}
const msgChannel = new RoomMessageChannel(key, this.dc, config);
@@ -387,11 +387,19 @@ export class Session extends EventEmitter {
}
async disconnect() {
- console.warn("Disconnecting session", this.created_at);
+ if (this.prepareState) {
+ console.info("Disconnect session in prepare state");
+ return;
+ }
+ if (["closed", "disconnected", "failed", "new"].includes(this.peer.connectionState)) {
+ console.info("Disconnect already disconnected session");
+ return;
+ }
+ console.info("Disconnecting session", this.created_at);
await this.dc.requestSession({
disconnect: {},
});
- console.warn("Disconnected session", this.created_at);
+ console.info("Disconnected session", this.created_at);
this.peer.close();
}
}
diff --git a/packages/sdk-core/src/sip_incoming.ts b/packages/sdk-core/src/sip_incoming.ts
new file mode 100644
index 0000000..256092a
--- /dev/null
+++ b/packages/sdk-core/src/sip_incoming.ts
@@ -0,0 +1,153 @@
+import { IncomingCallData, IncomingCallData_IncomingCallRequest_Accept2, IncomingCallData_IncomingCallResponse_Accept2 } from "./generated/protobuf/sip_gateway";
+import { EventEmitter } from "./utils";
+
+export interface IncomingSipCallStatus {
+ wsState: "WsConnecting" | "WsConnected" | "WsClosed",
+ sipState?: "Accepted" | "Cancelled" | "Bye",
+ startedAt?: number,
+}
+
+export class SipIncomingCall extends EventEmitter {
+ _status: IncomingSipCallStatus = { wsState: "WsConnecting" }
+ wsConn: WebSocket;
+ reqIdSeed = 1;
+ reqs: Map void, (err: Error) => void]> = new Map();
+
+ constructor(private callWs: string) {
+ super()
+ this.wsConn = new WebSocket(callWs);
+ this.wsConn.binaryType = "arraybuffer";
+ this.wsConn.onopen = () => {
+ this._status = {
+ ...this._status,
+ wsState: "WsConnected",
+ };
+ this.emit("status", this._status)
+ };
+ this.wsConn.onmessage = (msg) => {
+ let data = IncomingCallData.decode(new Uint8Array(msg.data));
+ if (data.event) {
+ let event = data.event;
+ if (event.accepted) {
+ this._status = {
+ ...this._status,
+ sipState: "Accepted",
+ startedAt: Date.now(),
+ };
+ this.emit("status", this._status)
+ } else if (event.ended) {
+
+ } else if (event.err) {
+ this.emit("error", event.err.message)
+ } else if (event.sip) {
+ if (event.sip.cancelled) {
+ this._status = {
+ ...this._status,
+ sipState: "Cancelled",
+ };
+ this.emit("status", this._status)
+ } else if (event.sip.bye) {
+ this._status = {
+ ...this._status,
+ sipState: "Bye",
+ };
+ this.emit("status", this._status)
+ }
+ }
+ } else if (data.request) {
+
+ } else if (data.response) {
+ const response = data.response;
+ if (response.reqId && this.reqs.has(response.reqId)) {
+ let [resolve, reject] = this.reqs.get(response.reqId)!;
+ this.reqs.delete(response.reqId);
+ if (response.error) {
+ reject(new Error(response.error.message))
+ } else {
+ resolve(response.accept || response.end || response.end || response.ring || response.accept2)
+ }
+ } else {
+ console.error("Invalid response:", response);
+ }
+ }
+ };
+ this.wsConn.onerror = (e) => {
+ this.emit("error", "WsConnectError");
+ };
+ this.wsConn.onclose = () => {
+ this._status = {
+ ...this._status,
+ wsState: "WsClosed",
+ };
+ this.emit("status", this._status)
+ };
+ }
+
+ get status(): IncomingSipCallStatus {
+ return this._status;
+ }
+
+ async accept(room: string, peer: string, record: boolean) {
+ return new Promise((resolve, reject) => {
+ const buf = IncomingCallData.encode({
+ request: {
+ reqId: this.reqIdSeed,
+ accept: {
+ room,
+ peer,
+ record
+ }
+ }
+ }).finish();
+ this.reqs.set(this.reqIdSeed, [resolve, reject]);
+ this.reqIdSeed += 1;
+ this.wsConn.send(buf);
+ });
+ }
+
+ async accept2(room: string, peer: string, record: boolean): Promise {
+ return new Promise((resolve, reject) => {
+ const buf = IncomingCallData.encode({
+ request: {
+ reqId: this.reqIdSeed,
+ accept2: {}
+ }
+ }).finish();
+ this.reqs.set(this.reqIdSeed, [resolve, reject]);
+ this.reqIdSeed += 1;
+ this.wsConn.send(buf);
+ });
+ }
+
+ async reject() {
+ return new Promise((resolve, reject) => {
+ const buf = IncomingCallData.encode({
+ request: {
+ reqId: this.reqIdSeed,
+ end: {}
+ }
+ }).finish();
+ this.reqs.set(this.reqIdSeed, [resolve, reject]);
+ this.reqIdSeed += 1;
+ this.wsConn.send(buf);
+ });
+ }
+
+ async end() {
+ return new Promise((resolve, reject) => {
+ const buf = IncomingCallData.encode({
+ request: {
+ reqId: this.reqIdSeed,
+ end: {}
+ }
+ }).finish();
+ this.reqs.set(this.reqIdSeed, [resolve, reject]);
+ this.reqIdSeed += 1;
+ this.wsConn.send(buf);
+ });
+ }
+
+ disconnect() {
+ this.wsConn.close();
+ }
+}
\ No newline at end of file
diff --git a/packages/sdk-core/src/sip_outgoing.ts b/packages/sdk-core/src/sip_outgoing.ts
new file mode 100644
index 0000000..673b272
--- /dev/null
+++ b/packages/sdk-core/src/sip_outgoing.ts
@@ -0,0 +1,201 @@
+import { OutgoingCallData } from "./generated/protobuf/sip_gateway";
+import { EventEmitter } from "./utils";
+
+export interface OutgoingSipCallStatus {
+ wsState: "WsConnecting" | "WsConnected" | "WsClosed",
+ sipState?: "Provisional" | "Early" | "Accepted" | "Failure" | "Bye",
+ sipCode?: number,
+ sipCodeStr?: string,
+ startedAt?: number,
+}
+
+interface WsEvent {
+ type: "Sip" | "Destroyed" | "Error",
+ content?: {
+ type: "Provisional" | "Early" | "Accepted" | "Failure" | "Bye",
+ code?: number
+ },
+ message?: string,
+}
+
+interface WsMessage {
+ type: "Event",
+ content: WsEvent,
+}
+
+export class SipOutgoingCall extends EventEmitter {
+ _status: OutgoingSipCallStatus = { wsState: "WsConnecting" }
+ wsConn: WebSocket;
+ reqIdSeed = 1;
+ reqs: Map void, (err: Error) => void]> = new Map();
+
+ constructor(callWs: string) {
+ super()
+ this.wsConn = new WebSocket(callWs);
+ this.wsConn.binaryType = "arraybuffer";
+ this.wsConn.onopen = () => {
+ this._status = {
+ ...this._status,
+ wsState: "WsConnected",
+ };
+ this.emit("status", this._status)
+ };
+ this.wsConn.onmessage = (msg) => {
+ console.log(msg.data);
+ const data = OutgoingCallData.decode(new Uint8Array(msg.data));
+ if (data.event) {
+ const event = data.event;
+ if (event.sip) {
+ if (event.sip.provisional) {
+ this._status = {
+ ...this._status,
+ sipState: "Provisional",
+ sipCode: event.sip.provisional.code,
+ sipCodeStr: (sipStatusCodes[event.sip.provisional.code] || 'Code ' + event.sip.provisional.code),
+ };
+ } else if (event.sip.early) {
+ this._status = {
+ ...this._status,
+ sipState: "Early",
+ sipCode: event.sip.early.code,
+ sipCodeStr: (sipStatusCodes[event.sip.early.code] || 'Code ' + event.sip.early.code),
+ };
+ } else if (event.sip.failure) {
+ this._status = {
+ ...this._status,
+ sipState: "Failure",
+ sipCode: event.sip.failure.code,
+ sipCodeStr: (sipStatusCodes[event.sip.failure.code] || 'Code ' + event.sip.failure.code),
+ };
+ } else if (event.sip.accepted) {
+ this._status = {
+ ...this._status,
+ startedAt: Date.now(),
+ sipState: "Accepted",
+ sipCode: event.sip.accepted.code,
+ sipCodeStr: (sipStatusCodes[event.sip.accepted.code] || 'Code ' + event.sip.accepted.code),
+ };
+ } else if (event.sip.bye) {
+ this._status = {
+ ...this._status,
+ sipState: "Bye",
+ sipCode: undefined,
+ sipCodeStr: undefined,
+ };
+ } else {
+ console.warn("Invalid sip event", event.sip);
+ }
+ this.emit("status", this._status)
+ } else if (event.ended) {
+
+ } else if (event.err) {
+ this.emit("error", event.err.message || 'Unknown error')
+ }
+ } else if (data.request) {
+
+ } else if (data.response) {
+ const response = data.response;
+ if (response.reqId && this.reqs.has(response.reqId)) {
+ let [resolve, reject] = this.reqs.get(response.reqId)!;
+ this.reqs.delete(response.reqId);
+ if (response.error) {
+ reject(new Error(response.error.message))
+ } else {
+ resolve()
+ }
+ } else {
+ console.error("Invalid response:", response);
+ }
+ }
+ };
+ this.wsConn.onerror = (e) => {
+ this.emit("error", "WsConnectError");
+ };
+ this.wsConn.onclose = () => {
+ this._status = {
+ ...this._status,
+ wsState: "WsClosed",
+ };
+ this.emit("status", this._status)
+ };
+ }
+
+ get status(): OutgoingSipCallStatus {
+ return this._status;
+ }
+
+ async end() {
+ return new Promise((resolve, reject) => {
+ const buf = OutgoingCallData.encode({
+ request: {
+ reqId: this.reqIdSeed,
+ end: {}
+ }
+ }).finish();
+ this.reqs.set(this.reqIdSeed, [resolve, reject]);
+ this.reqIdSeed += 1;
+ this.wsConn.send(buf);
+ });
+ }
+
+ disconnect() {
+ this.wsConn.close();
+ }
+}
+
+const sipStatusCodes: Record = {
+ 100: "Trying",
+ 180: "Ringing",
+ 181: "Call Is Being Forwarded",
+ 182: "Queued",
+ 183: "Session Progress",
+ 200: "OK",
+ 202: "Accepted",
+ 300: "Multiple Choices",
+ 301: "Moved Permanently",
+ 302: "Moved Temporarily",
+ 305: "Use Proxy",
+ 380: "Alternative Service",
+ 400: "Bad Request",
+ 401: "Unauthorized",
+ 402: "Payment Required",
+ 403: "Forbidden",
+ 404: "Not Found",
+ 405: "Method Not Allowed",
+ 406: "Not Acceptable",
+ 407: "Proxy Authentication Required",
+ 408: "Request Timeout",
+ 409: "Conflict",
+ 410: "Gone",
+ 413: "Request Entity Too Large",
+ 414: "Request-URI Too Long",
+ 415: "Unsupported Media Type",
+ 416: "Unsupported URI Scheme",
+ 420: "Bad Extension",
+ 421: "Extension Required",
+ 423: "Interval Too Brief",
+ 480: "Temporarily Unavailable",
+ 481: "Call/Transaction Does Not Exist",
+ 482: "Loop Detected",
+ 483: "Too Many Hops",
+ 484: "Address Incomplete",
+ 485: "Ambiguous",
+ 486: "Busy Here",
+ 487: "Request Terminated",
+ 488: "Not Acceptable Here",
+ 489: "Bad Event",
+ 491: "Request Pending",
+ 493: "Undecipherable",
+ 500: "Server Internal Error",
+ 501: "Not Implemented",
+ 502: "Bad Gateway",
+ 503: "Service Unavailable",
+ 504: "Server Time-out",
+ 505: "Version Not Supported",
+ 513: "Message Too Large",
+ 580: "Precondition Failure",
+ 600: "Busy Everywhere",
+ 603: "Decline",
+ 604: "Does Not Exist Anywhere",
+ 606: "Not Acceptable"
+};
\ No newline at end of file
diff --git a/packages/sdk-react-hooks/src/context.ts b/packages/sdk-react-hooks/src/context.ts
index 7456e6e..e331f67 100644
--- a/packages/sdk-react-hooks/src/context.ts
+++ b/packages/sdk-react-hooks/src/context.ts
@@ -30,7 +30,7 @@ export interface PublisherConfig {
}
export class Publisher {
- constructor(private _sender: TrackSender) {}
+ constructor(private _sender: TrackSender) { }
get sender() {
return this._sender;
diff --git a/packages/sdk-react-hooks/src/hooks/session.tsx b/packages/sdk-react-hooks/src/hooks/session.tsx
index 5a4e98d..f2c3cba 100644
--- a/packages/sdk-react-hooks/src/hooks/session.tsx
+++ b/packages/sdk-react-hooks/src/hooks/session.tsx
@@ -6,7 +6,7 @@ import { JoinInfo } from "@atm0s-media-sdk/core";
const VERSION = "react@0.0.0"; //TODO auto version
class SessionWrap {
- constructor(private ctx: Context) {}
+ constructor(private ctx: Context) { }
connect = () => {
return this.ctx.connect(VERSION);
};
diff --git a/packages/sdk-react-hooks/src/hooks/sip/sip_incoming.tsx b/packages/sdk-react-hooks/src/hooks/sip/sip_incoming.tsx
new file mode 100644
index 0000000..0f4fb8e
--- /dev/null
+++ b/packages/sdk-react-hooks/src/hooks/sip/sip_incoming.tsx
@@ -0,0 +1,22 @@
+import { IncomingSipCallStatus, SipIncomingCall } from "@atm0s-media-sdk/core";
+import { useEffect, useMemo, useState } from "react";
+
+export function useSipIncomingCallStatus(
+ callWs: string,
+): [IncomingSipCallStatus, string | null, SipIncomingCall] {
+ const call = useMemo(() => new SipIncomingCall(callWs), [callWs])
+ const [status, setStatus] = useState(call.status);
+ const [err, setErr] = useState(null);
+
+ useEffect(() => {
+ call.on('status', setStatus)
+ call.on('error', setErr)
+
+ return () => {
+ call.disconnect()
+ };
+ }, [call]);
+
+ return [status, err, call];
+}
+
diff --git a/packages/sdk-react-hooks/src/hooks/sip/sip_outgoing.tsx b/packages/sdk-react-hooks/src/hooks/sip/sip_outgoing.tsx
new file mode 100644
index 0000000..1e7a749
--- /dev/null
+++ b/packages/sdk-react-hooks/src/hooks/sip/sip_outgoing.tsx
@@ -0,0 +1,22 @@
+import { OutgoingSipCallStatus, SipOutgoingCall } from "@atm0s-media-sdk/core";
+import { useEffect, useMemo, useState } from "react";
+
+export function useSipOutgoingCallStatus(
+ callWs: string,
+): [OutgoingSipCallStatus, string | null, SipOutgoingCall] {
+ const call = useMemo(() => new SipOutgoingCall(callWs), [callWs])
+ const [status, setStatus] = useState(call.status);
+ const [err, setErr] = useState(null);
+
+ useEffect(() => {
+ call.on('status', setStatus)
+ call.on('error', setErr)
+
+ return () => {
+ call.disconnect()
+ };
+ }, [call]);
+
+ return [status, err, call];
+}
+
diff --git a/packages/sdk-react-hooks/src/lib.tsx b/packages/sdk-react-hooks/src/lib.tsx
index 160b042..9bee15c 100644
--- a/packages/sdk-react-hooks/src/lib.tsx
+++ b/packages/sdk-react-hooks/src/lib.tsx
@@ -25,3 +25,6 @@ export {
export type { ConsumerConfig } from "./hooks/consumer";
export { useMessageChannel } from "./hooks/msg_channel";
+
+export { useSipOutgoingCallStatus } from "./hooks/sip/sip_outgoing";
+export { useSipIncomingCallStatus } from "./hooks/sip/sip_incoming";
\ No newline at end of file
diff --git a/packages/sdk-react-hooks/src/provider.tsx b/packages/sdk-react-hooks/src/provider.tsx
index 0714dcd..ffda9d0 100644
--- a/packages/sdk-react-hooks/src/provider.tsx
+++ b/packages/sdk-react-hooks/src/provider.tsx
@@ -39,6 +39,6 @@ export function Atm0sMediaProvider({
{children}
) : (
-
+ <>>
);
}
diff --git a/packages/sdk-react-ui/src/components/previews/camera.tsx b/packages/sdk-react-ui/src/components/previews/camera.tsx
index c65ed03..19c3776 100644
--- a/packages/sdk-react-ui/src/components/previews/camera.tsx
+++ b/packages/sdk-react-ui/src/components/previews/camera.tsx
@@ -6,12 +6,12 @@ import { BitrateControlMode, Kind } from "@atm0s-media-sdk/core";
import { CameraIcon, CameraOffIcon } from "../icons/camera";
interface CameraPreviewProps {
- source_name: string;
+ trackName: string;
}
-export function CameraPreview({ source_name }: CameraPreviewProps) {
+export function CameraPreview({ trackName }: CameraPreviewProps) {
const videoRef = useRef(null);
- const stream = useDeviceStream(source_name);
+ const stream = useDeviceStream(trackName);
useEffect(() => {
if (stream && videoRef.current) {
videoRef.current.srcObject = stream;
@@ -38,8 +38,8 @@ export function CameraPreview({ source_name }: CameraPreviewProps) {
}
interface CameraSelectionProps {
- source_name: string;
- first_page?: boolean;
+ trackName: string;
+ defaultEnable?: boolean;
}
const PublisherConfig = {
@@ -49,18 +49,18 @@ const PublisherConfig = {
};
export function CameraSelection({
- source_name,
- first_page,
+ trackName,
+ defaultEnable,
}: CameraSelectionProps) {
- const publisher = usePublisher(source_name, Kind.VIDEO, PublisherConfig);
+ const publisher = usePublisher(trackName, Kind.VIDEO, PublisherConfig);
const [devices, setDevices] = useState<{ id: string; label: string }[]>([]);
const ctx = useContext(Atm0sMediaUIContext);
- const stream = useDeviceStream(source_name);
+ const stream = useDeviceStream(trackName);
useEffect(() => {
const init = async () => {
- if (first_page) {
- await ctx.requestDevice(source_name, "video");
+ if (defaultEnable) {
+ await ctx.requestDevice(trackName, "video");
}
const devices = await navigator.mediaDevices.enumerateDevices();
console.log(devices);
@@ -74,7 +74,7 @@ export function CameraSelection({
};
init();
- }, [ctx, source_name, setDevices, first_page]);
+ }, [ctx, trackName, setDevices, defaultEnable]);
useEffect(() => {
let track = stream?.getVideoTracks()[0];
@@ -87,10 +87,10 @@ export function CameraSelection({
const onToggle = useCallback(() => {
if (stream) {
- ctx.turnOffDevice(source_name);
+ ctx.turnOffDevice(trackName);
} else {
ctx
- .requestDevice(source_name, "video")
+ .requestDevice(trackName, "video")
.then(console.log)
.catch(console.error);
}
@@ -99,7 +99,7 @@ export function CameraSelection({
const onChange = useCallback((event: any) => {
let selected = event.target.options[event.target.selectedIndex].value;
ctx
- .requestDevice(source_name, "video", selected)
+ .requestDevice(trackName, "video", selected)
.then(console.log)
.catch(console.error);
}, []);
diff --git a/packages/sdk-react-ui/src/components/previews/microphone.tsx b/packages/sdk-react-ui/src/components/previews/microphone.tsx
index d27c110..82cacb5 100644
--- a/packages/sdk-react-ui/src/components/previews/microphone.tsx
+++ b/packages/sdk-react-ui/src/components/previews/microphone.tsx
@@ -6,12 +6,12 @@ import { Kind } from "@atm0s-media-sdk/core";
import { MicIcon, MicOffIcon } from "../icons/microphone";
interface MicrophonePreviewProps {
- source_name: string;
+ trackName: string;
}
-export function MicrophonePreview({ source_name }: MicrophonePreviewProps) {
+export function MicrophonePreview({ trackName }: MicrophonePreviewProps) {
const audioRef = useRef(null);
- const stream = useDeviceStream(source_name);
+ const stream = useDeviceStream(trackName);
useEffect(() => {
if (stream && audioRef.current) {
audioRef.current.srcObject = stream;
@@ -31,23 +31,23 @@ export function MicrophonePreview({ source_name }: MicrophonePreviewProps) {
}
interface MicrophoneSelectionProps {
- source_name: string;
- first_page?: boolean;
+ trackName: string;
+ defaultEnable?: boolean,
}
export function MicrophoneSelection({
- source_name,
- first_page,
+ trackName,
+ defaultEnable,
}: MicrophoneSelectionProps) {
- const publisher = usePublisher(source_name, Kind.AUDIO);
+ const publisher = usePublisher(trackName, Kind.AUDIO);
const [devices, setDevices] = useState<{ id: string; label: string }[]>([]);
const ctx = useContext(Atm0sMediaUIContext);
- const stream = useDeviceStream(source_name);
+ const stream = useDeviceStream(trackName);
useEffect(() => {
const init = async () => {
- if (first_page) {
- await ctx.requestDevice(source_name, "audio");
+ if (defaultEnable) {
+ await ctx.requestDevice(trackName, "audio");
}
const devices = await navigator.mediaDevices.enumerateDevices();
console.log(devices);
@@ -61,7 +61,12 @@ export function MicrophoneSelection({
};
init();
- }, [ctx, source_name, setDevices, first_page]);
+ return () => {
+ if (defaultEnable) { //TODO more better way
+ ctx.turnOffDevice(trackName);
+ }
+ }
+ }, [ctx, trackName, setDevices, defaultEnable]);
useEffect(() => {
let track = stream?.getAudioTracks()[0];
@@ -74,10 +79,10 @@ export function MicrophoneSelection({
const onToggle = useCallback(() => {
if (stream) {
- ctx.turnOffDevice(source_name);
+ ctx.turnOffDevice(trackName);
} else {
ctx
- .requestDevice(source_name, "audio")
+ .requestDevice(trackName, "audio")
.then(console.log)
.catch(console.error);
}
@@ -85,7 +90,7 @@ export function MicrophoneSelection({
const onChange = useCallback((event: any) => {
let selected = event.target.options[event.target.selectedIndex].value;
ctx
- .requestDevice(source_name, "audio", selected)
+ .requestDevice(trackName, "audio", selected)
.then(console.log)
.catch(console.error);
}, []);
diff --git a/packages/sdk-react-ui/src/components/uis/clock_timer.tsx b/packages/sdk-react-ui/src/components/uis/clock_timer.tsx
new file mode 100644
index 0000000..7d0c743
--- /dev/null
+++ b/packages/sdk-react-ui/src/components/uis/clock_timer.tsx
@@ -0,0 +1,27 @@
+import React, { useEffect, useState } from 'react';
+
+interface ClockTimerProps {
+ started_at: number; // Timestamp in milliseconds
+}
+
+const formatTime = (seconds: number) => {
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds % 60;
+ return `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
+};
+
+export function ClockTimer({ started_at }: ClockTimerProps) {
+ const [secondsElapsed, setSecondsElapsed] = useState(formatTime(Math.floor((Date.now() - started_at) / 1000)));
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setSecondsElapsed(formatTime(Math.floor((Date.now() - started_at) / 1000)));
+ }, 1000); // Update every second
+
+ return () => clearInterval(intervalId); // Cleanup on unmount
+ }, []);
+
+ return (
+ {secondsElapsed}
+ );
+};
diff --git a/packages/sdk-react-ui/src/lib.tsx b/packages/sdk-react-ui/src/lib.tsx
index b272daf..d29c6c1 100644
--- a/packages/sdk-react-ui/src/lib.tsx
+++ b/packages/sdk-react-ui/src/lib.tsx
@@ -10,3 +10,8 @@ export { PeersPanel } from "./panels/peers_panel";
export { DevicesSelection } from "./panels/devices_selection";
export { ControlsPanel } from "./panels/controls_panel";
export { ChatPanel } from "./panels/chat_panel";
+export type { SipOutgoingCallProps } from "./panels/sip_outgoing";
+export { SipOutgoingCallWidget } from "./panels/sip_outgoing";
+
+export type { SipIncomingCallProps } from "./panels/sip_incoming";
+export { SipIncomingCallWidget } from "./panels/sip_incoming";
\ No newline at end of file
diff --git a/packages/sdk-react-ui/src/panels/controls_panel.tsx b/packages/sdk-react-ui/src/panels/controls_panel.tsx
index d9a77a0..34ce9f5 100644
--- a/packages/sdk-react-ui/src/panels/controls_panel.tsx
+++ b/packages/sdk-react-ui/src/panels/controls_panel.tsx
@@ -9,8 +9,8 @@ export function ControlsPanel({ audio_name, video_name }: Props) {
return (
);
diff --git a/packages/sdk-react-ui/src/panels/devices_selection.tsx b/packages/sdk-react-ui/src/panels/devices_selection.tsx
index 49807f3..6cb7308 100644
--- a/packages/sdk-react-ui/src/panels/devices_selection.tsx
+++ b/packages/sdk-react-ui/src/panels/devices_selection.tsx
@@ -10,11 +10,11 @@ export function DevicesSelection({ audio_name, video_name }: Props) {
return (
);
diff --git a/packages/sdk-react-ui/src/panels/sip_incoming.tsx b/packages/sdk-react-ui/src/panels/sip_incoming.tsx
new file mode 100644
index 0000000..5713544
--- /dev/null
+++ b/packages/sdk-react-ui/src/panels/sip_incoming.tsx
@@ -0,0 +1,92 @@
+import { useCallback, useEffect, useState } from "react";
+import { useSession, useSipIncomingCallStatus } from "@atm0s-media-sdk/react-hooks";
+import { AudioMixerPlayer, MicrophoneSelection } from "../lib";
+import { ClockTimer } from "../components/uis/clock_timer";
+
+export interface SipIncomingCallProps {
+ callFrom: string,
+ callWs: string;
+ room: string,
+ record: boolean,
+ onEnd: () => void;
+}
+
+type AcceptState = "Connecting" | "Accepting" | "Accepted";
+type AcceptError = "MediaFailed" | "SipFailed";
+
+export function SipIncomingCallWidget(props: SipIncomingCallProps): JSX.Element {
+ const [status, callErr, call] = useSipIncomingCallStatus(props.callWs);
+ const [acceptState, setAcceptState] = useState(null);
+ const [acceptError, setAcceptError] = useState(null);
+ const session = useSession();
+
+ const showAccept = !status.sipState;
+ const showReject = !status.sipState;
+ const showHangup = status.sipState == "Accepted";
+
+ useEffect(() => {
+ return () => {
+ session.disconnect();
+ };
+ }, [session]);
+
+ const accept = useCallback(() => {
+ setAcceptState("Connecting")
+ session.connect().then(() => {
+ setAcceptState("Accepting")
+ return call.accept(props.room, props.callFrom, props.record).then(() => {
+ setAcceptState("Accepted")
+ }).catch(() => setAcceptError("SipFailed"))
+ }).catch(() => setAcceptError("MediaFailed"))
+
+ }, [call]);
+
+ const reject = useCallback(() => {
+ call.reject()
+ session.disconnect();
+ props.onEnd()
+ }, [session, call, props.onEnd]);
+
+ return (
+
+ {/* WebSocket Status Indicator */}
+
+
+ Status: {acceptError || callErr || status.sipState || acceptState}
+
+
+ {/* Time Counting */}
+
{status.startedAt && }
+
+ {/* Destination Number Info */}
+
Destination: {props.callFrom}
+
+
+
+
+ {/* Accept Button */}
+ {showAccept &&
}
+
+ {/* Reject Button */}
+ {showReject &&
}
+ {/* Reject Button */}
+ {showHangup &&
}
+
+ );
+}
+
diff --git a/packages/sdk-react-ui/src/panels/sip_outgoing.tsx b/packages/sdk-react-ui/src/panels/sip_outgoing.tsx
new file mode 100644
index 0000000..5fe41b9
--- /dev/null
+++ b/packages/sdk-react-ui/src/panels/sip_outgoing.tsx
@@ -0,0 +1,55 @@
+import { useCallback, useEffect } from "react";
+import { useSipOutgoingCallStatus, useSession } from "@atm0s-media-sdk/react-hooks";
+import { AudioMixerPlayer, MicrophoneSelection } from "../lib";
+import { ClockTimer } from "../components/uis/clock_timer";
+
+export interface SipOutgoingCallProps {
+ callTo: string,
+ callWs: string;
+ onEnd: () => void;
+}
+
+export function SipOutgoingCallWidget(props: SipOutgoingCallProps): JSX.Element {
+ const [status, callErr] = useSipOutgoingCallStatus(props.callWs);
+ const session = useSession();
+
+ useEffect(() => {
+ session.connect();
+ return () => {
+ session.disconnect();
+ };
+ }, [session]);
+
+ const hangUp = useCallback(() => {
+ session.disconnect();
+ props.onEnd()
+ }, [session, props.onEnd]);
+
+ return (
+
+ {/* WebSocket Status Indicator */}
+
+
+ SIP Status: {callErr || status.sipState} {status.sipCodeStr && '/ ' + status.sipCodeStr}
+
+
+ {/* Time Counting */}
+
{status.startedAt && }
+
+ {/* Destination Number Info */}
+
Destination: {props.callTo}
+
+
+
+
+ {/* Hangup Button */}
+
+
+ );
+}
+