Skip to content

Commit

Permalink
feat: added sip outgoing hook, UI and demo (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
giangndm authored Oct 23, 2024
1 parent 33af70d commit 3c67638
Show file tree
Hide file tree
Showing 37 changed files with 4,184 additions and 56 deletions.
98 changes: 98 additions & 0 deletions apps/web/app/actions/sip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use server";

import { env } from "../env";
import { generate_random_token } from "./token";

interface MakeCallParams {
sip_server: string;
sip_auth?: {
username: string;
password: string;
};
from_number: string;
to_number: string;
hook: string;
}

interface MakeCallResponse {
status: boolean,
data?: {
gateway: string,
call_id: string,
call_token: string,
call_ws: string
},
error?: string
}

export async function make_outgoing_call(params: MakeCallParams) {
console.log("Creating webrtc token");
const [room, peer, token] = await generate_random_token();
const url = env.SIP_GATEWAY + "/call/outgoing";
const rawResponse = await fetch(url, {
method: "POST",
headers: {
Authorization: "Bearer " + env.APP_SECRET,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
...params,
streaming: {
room,
peer: params.to_number,
record: false,
},
}),
cache: "no-cache",
});

const content = await rawResponse.json() as MakeCallResponse;
if (content.status && content.data) {
return {
room: room!,
peer: peer!,
token: token!,
callTo: params.to_number,
callWs: env.SIP_GATEWAY + content.data.call_ws,
}
} else {
throw new Error(content.error);
}
}

interface CreateNotifyTokenParams {
client_id: string,
ttl: number,
}

interface CreateNotifyTokenResponse {
status: boolean,
data?: {
token: string
},
error?: string
}

export async function create_notify_ws(params: CreateNotifyTokenParams) {
console.log("Creating notify token");
const url = env.SIP_GATEWAY + "/token/notify";
const rawResponse = await fetch(url, {
method: "POST",
headers: {
Authorization: "Bearer " + env.APP_SECRET,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(params),
cache: "no-cache",
});

const content = await rawResponse.json() as CreateNotifyTokenResponse;
if (content.status && content.data) {
return env.SIP_GATEWAY + "/call/incoming/notify?token=" + content.data.token;
} else {
throw new Error(content.error);
}
}

2 changes: 1 addition & 1 deletion apps/web/app/actions/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export async function generate_token(
}
}

export async function generate_random_token(server?: string) {
export async function generate_random_token(server?: string): Promise<[string, string, string]> {
const now = new Date().getTime();
const room = "room-" + now;
const peer = "peer-" + now;
Expand Down
5 changes: 3 additions & 2 deletions apps/web/app/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export const env = {
GATEWAY_ENDPOINTS: (
(process.env.NEXT_PUBLIC_GATEWAYS as string) || "http://localhost:3001"
).split(";"),
APP_SECRET: (process.env.APP_SECRET as string) || "insecure",
};
SIP_GATEWAY: (process.env.NEXT_PUBLIC_SIP_GATEWAY as string) || "http://localhost:3001",
APP_SECRET: (process.env.APP_SECRET as string) || "zsz94nsrj3xvmbu555nmu25hwqo6shiq",
};
6 changes: 6 additions & 0 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export default function Page(): JSX.Element {
<div>
<Link href="/react_ui_samples/meet">Meet</Link>
</div>
<div>
<Link href="/react_ui_samples/sipOutgoing">Sip Outgoing call</Link>
</div>
<div>
<Link href="/react_ui_samples/sipIncoming">Sip Incoming call</Link>
</div>
</div>
</main>
);
Expand Down
54 changes: 54 additions & 0 deletions apps/web/app/react_ui_samples/sipIncoming/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import { Atm0sMediaProvider } from "@atm0s-media-sdk/react-hooks";
import { Atm0sMediaUIProvider, SipIncomingCallWidget } from "@atm0s-media-sdk/react-ui";
import { useCallback, useState } from "react";
import { env } from "../../env";
import { AudioMixerMode } from "@atm0s-media-sdk/core";

export interface IncomingCallPanelProps {
callFrom: string,
callWs: string,
room: string;
peer: string,
token: string,
record: boolean,
onEnd?: () => void;
}

export default function PageContent({ callFrom, callWs, room, peer, token, record, onEnd }: IncomingCallPanelProps) {
const [active, setActive] = useState(true);
const onEnd2 = useCallback(() => {
setActive(false);
onEnd && onEnd();
}, [onEnd])

return (
<main className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<div className="mb-4">
{active && <Atm0sMediaProvider
gateway={env.GATEWAY_ENDPOINTS[0]!}
cfg={{
token,
join: {
room,
peer,
publish: { peer: true, tracks: true },
subscribe: { peers: true, tracks: true },
features: {
mixer: {
mode: AudioMixerMode.AUTO,
outputs: 3
}
}
},
}}
>
<Atm0sMediaUIProvider>
<SipIncomingCallWidget callFrom={callFrom} callWs={callWs} room={room} record={record} onEnd={onEnd2} />
</Atm0sMediaUIProvider>
</Atm0sMediaProvider>}
</div>
</main>
);
}
65 changes: 65 additions & 0 deletions apps/web/app/react_ui_samples/sipIncoming/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { generate_random_token } from "../../actions/token";
import Content, { IncomingCallPanelProps } from "./content";
import { useState } from "react";

export default function SipIncoming({
searchParams,
}: {
searchParams: { server?: string };
}) {
// Create a single session state object using the Session interface
const [session, setSession] = useState<IncomingCallPanelProps | null>(null);

const [callWs, setCallWs] = useState("");
const [callFrom, setCallFrom] = useState("");
const [record, setRecord] = useState(false);

const handleCall = async () => {
const [room, peer, token] = await generate_random_token();
setSession({
callFrom,
callWs,
room,
peer,
token,
record,
})
};

return (
<div className="flex items-center justify-center min-h-screen bg-gray-100"> {/* Centering the form */}
<form onSubmit={(e) => { e.preventDefault(); handleCall(); }} className="bg-white p-4 rounded-lg shadow-md space-y-4 w-96"> {/* Reduced padding and width */}
<h2 className="text-2xl font-bold text-center text-gray-800 mb-4">Show a Incoming SIP Call</h2> {/* Reduced title size */}
<input
type="text"
placeholder="Websocket Call URL"
value={callWs}
onChange={(e) => setCallWs(e.target.value)}
className="input-class p-2 border border-gray-300 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500" // Reduced padding
/>
<input
type="text"
placeholder="Call From"
value={callFrom}
onChange={(e) => setCallFrom(e.target.value)}
className="input-class p-2 border border-gray-300 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500" // Reduced padding
/>

<div className="flex items-center">
<input
type="checkbox"
checked={record}
onChange={(e) => setRecord(e.target.checked)}
className="mr-2"
/>
<label className="text-gray-700">Record Call</label>
</div>

<button type="submit" className="button-class w-full bg-blue-600 text-white p-2 rounded-lg hover:bg-blue-700 transition duration-200">Show</button>
</form>
{session && <Content {...session} onEnd={() => setSession(null)} />} {/* Pass session object to Content */}
</div>
);
}
51 changes: 51 additions & 0 deletions apps/web/app/react_ui_samples/sipOutgoing/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { AudioMixerMode } from "@atm0s-media-sdk/core";
import { Atm0sMediaProvider } from "@atm0s-media-sdk/react-hooks";
import { Atm0sMediaUIProvider, SipOutgoingCallWidget } from "@atm0s-media-sdk/react-ui";
import { useCallback, useState } from "react";
import { env } from "../../env";

export interface OutgoingCallPanelProps {
room: string;
peer: string;
token: string;
callTo: string,
callWs: string;
onEnd?: () => void;
}

export default function PageContent({ room, peer, token, callTo, callWs, onEnd }: OutgoingCallPanelProps) {
const [active, setActive] = useState(true);
const hangUp = useCallback(() => {
setActive(false);
onEnd && onEnd();
}, [onEnd])

return (
<main>
{active && <Atm0sMediaProvider
gateway={env.GATEWAY_ENDPOINTS[0]!}
cfg={{
token,
join: {
room,
peer,
publish: { peer: true, tracks: true },
subscribe: { peers: true, tracks: true },
features: {
mixer: {
mode: AudioMixerMode.AUTO,
outputs: 3
}
}
},
}}
>
<Atm0sMediaUIProvider>
<SipOutgoingCallWidget callTo={callTo} callWs={callWs} onEnd={hangUp} />
</Atm0sMediaUIProvider>
</Atm0sMediaProvider>}
</main>
);
}
91 changes: 91 additions & 0 deletions apps/web/app/react_ui_samples/sipOutgoing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use client";

import { generate_random_token } from "../../actions/token";
import { make_outgoing_call } from "../../actions/sip"; // Import the server action
import Content, { OutgoingCallPanelProps } from "./content";
import { useState } from "react";

export default function SipOutgoing({
searchParams,
}: {
searchParams: { server?: string };
}) {
// New state variables for the input form
const [sipServer, setSipServer] = useState("");
const [sipUser, setSipUser] = useState("");
const [sipPassword, setSipPassword] = useState("");
const [sipFrom, setSipFrom] = useState("");
const [sipTo, setSipTo] = useState("");
const [sipHook, setSipHook] = useState("");

const [outgoingProps, setOutgoingProps] = useState<OutgoingCallPanelProps | null>(null)

// Function to handle the call button click
const handleCall = async () => {
const props = await make_outgoing_call({
sip_server: sipServer,
sip_auth: sipUser ? {
username: sipUser,
password: sipPassword,
} : undefined,
from_number: sipFrom,
to_number: sipTo,
hook: sipHook,
});
// Logic to display the outgoing call with callWs and streamingToken
setOutgoingProps(props)
};

return (
<div className="flex items-center justify-center min-h-screen bg-gray-100"> {/* Centering the form */}
<form onSubmit={(e) => { e.preventDefault(); handleCall(); }} className="bg-white p-4 rounded-lg shadow-md space-y-4 w-96"> {/* Reduced padding and width */}
<h2 className="text-2xl font-bold text-center text-gray-800 mb-4">Make a SIP Call</h2> {/* Reduced title size */}
<input
type="text"
placeholder="SIP Server"
value={sipServer}
onChange={(e) => setSipServer(e.target.value)}
className="input-class p-2 border border-gray-300 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500" // Reduced padding
/>
<input
type="text"
placeholder="SIP User"
value={sipUser}
onChange={(e) => setSipUser(e.target.value)}
className="input-class p-2 border border-gray-300 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500" // Reduced padding
/>
<input
type="password"
placeholder="SIP Password"
value={sipPassword}
onChange={(e) => setSipPassword(e.target.value)}
className="input-class p-2 border border-gray-300 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500" // Reduced padding
/>
<input
type="text"
placeholder="SIP From"
value={sipFrom}
onChange={(e) => setSipFrom(e.target.value)}
className="input-class p-2 border border-gray-300 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500" // Reduced padding
/>
<input
type="text"
placeholder="SIP To"
value={sipTo}
onChange={(e) => setSipTo(e.target.value)}
className="input-class p-2 border border-gray-300 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500" // Reduced padding
/>
<input
type="text"
placeholder="SIP Hook"
value={sipHook}
onChange={(e) => setSipHook(e.target.value)}
className="input-class p-2 border border-gray-300 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500" // Reduced padding
/>
<button type="submit" className="button-class w-full bg-blue-600 text-white p-2 rounded-lg hover:bg-blue-700 transition duration-200">Call</button> {/* Reduced button padding */}
</form>

{outgoingProps && <Content {...outgoingProps} onEnd={() => setOutgoingProps(null)} />}
</div>
);
}
Empty file.
Loading

0 comments on commit 3c67638

Please sign in to comment.