Create "working" example

This commit is contained in:
AlexanderHD27
2024-11-03 23:52:06 +01:00
parent 41a32b450c
commit f9ce4db95a
42 changed files with 2067 additions and 21 deletions

View File

@@ -0,0 +1,3 @@
{
"deno.enable": true
}

View File

View File

@@ -0,0 +1,5 @@
{
"tasks": {
"dev": "deno run -A --unstable-net --watch main.ts"
}
}

104
gobot-gui/webrtc_test/api/deno.lock generated Normal file
View File

@@ -0,0 +1,104 @@
{
"version": "3",
"packages": {
"specifiers": {
"jsr:@oak/commons@0.7": "jsr:@oak/commons@0.7.0",
"jsr:@oak/oak@14": "jsr:@oak/oak@14.2.0",
"jsr:@std/assert@0.218": "jsr:@std/assert@0.218.2",
"jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2",
"jsr:@std/bytes@0.218": "jsr:@std/bytes@0.218.2",
"jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2",
"jsr:@std/crypto@0.218": "jsr:@std/crypto@0.218.2",
"jsr:@std/encoding@0.218": "jsr:@std/encoding@0.218.2",
"jsr:@std/encoding@^0.218.2": "jsr:@std/encoding@0.218.2",
"jsr:@std/http@0.218": "jsr:@std/http@0.218.2",
"jsr:@std/io@0.218": "jsr:@std/io@0.218.2",
"jsr:@std/media-types@0.218": "jsr:@std/media-types@0.218.2",
"jsr:@std/path@0.218": "jsr:@std/path@0.218.2",
"npm:@types/node": "npm:@types/node@18.16.19",
"npm:@types/sdp-transform": "npm:@types/sdp-transform@2.4.9",
"npm:path-to-regexp@6.2.1": "npm:path-to-regexp@6.2.1",
"npm:sdp-transform@2.14.2": "npm:sdp-transform@2.14.2"
},
"jsr": {
"@oak/commons@0.7.0": {
"integrity": "4bd889b3dc9ddac1b602034d88c137f06de7078775961b51081beb5f175c120b"
},
"@oak/oak@14.2.0": {
"integrity": "b683b089693004ac3bca80b52159b3e9ad214dc8246ff5dc61ba658da78bc166",
"dependencies": [
"jsr:@oak/commons@0.7",
"jsr:@std/assert@0.218",
"jsr:@std/bytes@0.218",
"jsr:@std/crypto@0.218",
"jsr:@std/encoding@0.218",
"jsr:@std/http@0.218",
"jsr:@std/io@0.218",
"jsr:@std/media-types@0.218",
"jsr:@std/path@0.218",
"npm:path-to-regexp@6.2.1"
]
},
"@std/assert@0.218.2": {
"integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf"
},
"@std/bytes@0.218.2": {
"integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670"
},
"@std/crypto@0.218.2": {
"integrity": "8c5031a3a1c3ac3bed3c0d4bed2fe7e7faedcb673bbfa0edd10570c8452f5cd2",
"dependencies": [
"jsr:@std/assert@^0.218.2",
"jsr:@std/encoding@^0.218.2"
]
},
"@std/encoding@0.218.2": {
"integrity": "da55a763c29bf0dbf06fd286430b358266eb99c28789d89fe9a3e28edecb8d8e"
},
"@std/http@0.218.2": {
"integrity": "54223b62702e665b9dab6373ea2e51235e093ef47228d21cfa0469ee5ac75c9b",
"dependencies": [
"jsr:@std/assert@^0.218.2",
"jsr:@std/encoding@^0.218.2"
]
},
"@std/io@0.218.2": {
"integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f",
"dependencies": [
"jsr:@std/bytes@^0.218.2"
]
},
"@std/media-types@0.218.2": {
"integrity": "1ed3bd2a05e44bad3fc2bab1767d0ce7f2fd68baee62a980751ce51633acb788"
},
"@std/path@0.218.2": {
"integrity": "b568fd923d9e53ad76d17c513e7310bda8e755a3e825e6289a0ce536404e2662",
"dependencies": [
"jsr:@std/assert@^0.218.2"
]
}
},
"npm": {
"@types/node@18.16.19": {
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
"dependencies": {}
},
"@types/sdp-transform@2.4.9": {
"integrity": "sha512-bVr+/OoZZy7wrHlNcEAAa6PAgKA4BoXPYVN2EijMC5WnGgQ4ZEuixmKnVs2roiAvr7RhIFVH17QD27cojgIZCg==",
"dependencies": {}
},
"path-to-regexp@6.2.1": {
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==",
"dependencies": {}
},
"sdp-transform@2.14.2": {
"integrity": "sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==",
"dependencies": {}
}
}
},
"remote": {
"https://deno.land/std@0.224.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376",
"https://deno.land/std@0.224.0/encoding/hex.ts": "6270f25e5d85f99fcf315278670ba012b04b7c94b67715b53f30d03249687c07"
}
}

View File

@@ -0,0 +1,66 @@
import { Application, Router } from "jsr:@oak/oak@14";
import { router_webrtc } from "./webrtc_fake.ts";
const app = new Application();
const router = new Router({
prefix: "/api",
});
let offer: any = {};
let answer: any = {};
router.post("/offer", async (ctx) => {
const body = await ctx.request.body.json();
if (body) {
offer = body;
ctx.response.status = 200;
ctx.response.body = { message: "Answer stored successfully" };
} else {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid JSON" };
}
});
router.post("/answer", async (ctx) => {
const body = await ctx.request.body.json();
if (body) {
answer = body;
ctx.response.status = 200;
ctx.response.body = { message: "Answer stored successfully" };
} else {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid JSON" };
}
});
router.get("/offer", (ctx) => {
ctx.response.body = offer;
if(Object.keys(answer).length === 0) {
ctx.response.status = 204;
} else {
offer = {};
ctx.response.status = 200;
}
ctx.response.headers.set("Content-Type", "application/json");
});
router.get("/answer", (ctx) => {
ctx.response.body = answer;
if(Object.keys(answer).length === 0) {
ctx.response.status = 204;
} else {
answer = {};
ctx.response.status = 200;
}
ctx.response.headers.set("Content-Type", "application/json");
});
app.use(router_webrtc.routes());
app.use(router_webrtc.allowedMethods());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen({
port: 8080,
});

View File

@@ -0,0 +1,6 @@
import { assertEquals } from "jsr:@std/assert";
import { add } from "./main.ts";
Deno.test(function addTest() {
assertEquals(add(2, 3), 5);
});

View File

@@ -0,0 +1,8 @@
v=0
o=- 0 0 IN IP4 127.0.0.1
s=No Name
t=0 0
m=video 1234 RTP/AVP 96
b=AS:3000
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1

View File

@@ -0,0 +1,14 @@
#/usr/bin/bash
SDP_FILE="output.sdp"
ffmpeg \
-stream_loop -1 \
-rtbufsize 2M \
-re \
-i "test/Big_Buck_Bunny_1080_10s_10MB.mp4" \
-b:v 3M \
-sdp_file $SDP_FILE \
-vcodec libx264 \
-acodec aac \
-f rtp rtp://0.0.0.0:1234

View File

@@ -0,0 +1,48 @@
export interface IceCandidate {
foundation: string;
componentId: number;
transport: string;
priority: number;
ipAddress: string;
port: number;
candidateType: string;
relatedAddress: string | null;
relatedPort: number | null;
}
export function parseIceCandidate(candidate: string): IceCandidate {
const parts = candidate.split(' ');
const iceCandidate: IceCandidate = {
foundation: parts[0].split(':')[1],
componentId: Number.parseInt(parts[1]),
transport: parts[2],
priority: Number.parseInt(parts[3]),
ipAddress: parts[4],
port: Number.parseInt(parts[5]),
candidateType: parts[7],
relatedAddress: null,
relatedPort: null
};
for (let i = 8; i < parts.length; i++) {
if (parts[i] === 'raddr') {
iceCandidate.relatedAddress = parts[i + 1];
} else if (parts[i] === 'rport') {
iceCandidate.relatedPort = Number.parseInt(parts[i + 1]);
}
}
return iceCandidate;
}
export function findHighestPriorityCandidate(candidates: IceCandidate[]): IceCandidate | null {
if (candidates.length === 0) {
return null;
}
return candidates.reduce((highest, candidate) =>
candidate.priority > highest.priority ? candidate : highest
);
}

View File

@@ -0,0 +1,114 @@
import { assertEquals } from "jsr:@std/assert";
import { IceCandidate, parseIceCandidate } from '../src/iceCandiate.ts';
const iceCandidates = [
"candidate:842163049 1 udp 1677729535 192.168.1.2 3478 typ host",
"candidate:842163049 2 udp 1677729534 192.168.1.3 3479 typ srflx raddr 192.168.1.2 rport 3478",
"candidate:842163049 1 tcp 1677729533 192.168.1.4 3480 typ relay raddr 192.168.1.3 rport 3479",
"candidate:842163049 2 tcp 1677729532 192.168.1.5 3481 typ host",
"candidate:842163049 1 udp 1677729531 192.168.1.6 3482 typ srflx rport 3480",
"candidate:842163049 2 udp 1677729530 192.168.1.7 3483 typ relay raddr 192.168.1.5",
"candidate:842163049 1 tcp 1677729529 192.168.1.8 3484 typ host",
"candidate:842163049 2 tcp 1677729528 192.168.1.9 3485 typ srflx raddr 192.168.1.6 rport 3482"
];
const parsedExamples: IceCandidate[] = [
{
foundation: "842163049",
componentId: 1,
transport: "udp",
priority: 1677729535,
ipAddress: "192.168.1.2",
port: 3478,
candidateType: "host",
relatedAddress: null,
relatedPort: null
},
{
foundation: "842163049",
componentId: 2,
transport: "udp",
priority: 1677729534,
ipAddress: "192.168.1.3",
port: 3479,
candidateType: "srflx",
relatedAddress: "192.168.1.2",
relatedPort: 3478
},
{
foundation: "842163049",
componentId: 1,
transport: "tcp",
priority: 1677729533,
ipAddress: "192.168.1.4",
port: 3480,
candidateType: "relay",
relatedAddress: "192.168.1.3",
relatedPort: 3479
},
{
foundation: "842163049",
componentId: 2,
transport: "tcp",
priority: 1677729532,
ipAddress: "192.168.1.5",
port: 3481,
candidateType: "host",
relatedAddress: null,
relatedPort: null
},
{
foundation: "842163049",
componentId: 1,
transport: "udp",
priority: 1677729531,
ipAddress: "192.168.1.6",
port: 3482,
candidateType: "srflx",
relatedPort: 3480,
relatedAddress: null
},
{
foundation: "842163049",
componentId: 2,
transport: "udp",
priority: 1677729530,
ipAddress: "192.168.1.7",
port: 3483,
candidateType: "relay",
relatedAddress: "192.168.1.5",
relatedPort: null
},
{
foundation: "842163049",
componentId: 1,
transport: "tcp",
priority: 1677729529,
ipAddress: "192.168.1.8",
port: 3484,
candidateType: "host",
relatedAddress: null,
relatedPort: null
},
{
foundation: "842163049",
componentId: 2,
transport: "tcp",
priority: 1677729528,
ipAddress: "192.168.1.9",
port: 3485,
candidateType: "srflx",
relatedAddress: "192.168.1.6",
relatedPort: 3482
}
];
Deno.test("parseIceCandidate should correctly parse ICE candidates", async (t) => {
for (const [index, candidate] of iceCandidates.entries()) {
await t.step(`${index}-${candidate}`, () => {
const parsed = parseIceCandidate(candidate);
console.log(candidate);
assertEquals(parsed, parsedExamples[index]);
})
}
});

View File

@@ -0,0 +1,145 @@
import { Router } from "jsr:@oak/oak@14";
// @deno-types="npm:@types/sdp-transform"
import sdpTransform from "npm:sdp-transform@2.14.2";
import { encodeHex,} from "https://deno.land/std@0.224.0/encoding/hex.ts";
import { parseIceCandidate, IceCandidate } from "./src/iceCandiate.ts";
const router = new Router({
prefix: "/api",
});
export {router as router_webrtc};
let rxAddress: Deno.Addr | undefined = undefined;
const ffmpegSocket = Deno.listenDatagram({
hostname: "localhost",
port: 1234,
transport: "udp"
});
let ffmpegRxRun = true;
// deno-lint-ignore no-async-promise-executor
const _ffmpegRxPromose = new Promise(async (resolve, _reject) => {
while(ffmpegRxRun) {
const data = new Uint8Array(65536);
await ffmpegSocket.receive(data);
if(rxAddress !== undefined) {
await ffmpegSocket.send(data, rxAddress);
}
}
resolve(null);
});
interface RTCIceCandidate {
candidate: string;
sdpMid: string;
sdpMLineIndex: number;
usernameFragment: string;
};
interface RTCSessionDescription {
sdp: string;
type: "offer" | "answer";
}
async function getSDP(): Promise<string> {
return await Deno.readTextFile("./output.sdp");
}
async function getFingerprint(): Promise<string> {
const cert = new Uint8Array(32);
crypto.getRandomValues(cert);
const hash = encodeHex(await crypto.subtle.digest("SHA-256", cert)).toUpperCase();
return hash.split(/(..)/g).filter((s: string) => s !== "").join(":");
}
async function modifySDP(sdp: string): Promise<string> {
//let sdpObj = sdpTransform.parse(sdp);
const localAddress = "127.0.0.1";
let sdpObj: sdpTransform.SessionDescription = {
fingerprint: {
"type": "SHA-256",
"hash": await getFingerprint()
},
icePwd : "AAAABBBB",
iceUfrag: "AAAABBBB",
origin: {
username: "-",
sessionId: (Math.random()*10000000).toFixed(0),
sessionVersion: 0,
netType: "IN",
ipVer: 4,
address: localAddress,
},
timing: {
start: 0,
stop: 0
},
media: [
{
type: "video",
port: 1235,
protocol: "RTP/AVP",
direction: "sendrecv",
connection: {
ip: localAddress,
version: 4,
},
payloads: "126",
fmtp: [
],
rtp: [
{
payload: 126,
codec: "H264",
rate: 90000
}
]
}
]
};
console.log(sdpObj);
return sdpTransform.write(sdpObj);
}
router.post("/webrtc", async (ctx) => {
const body: {
ice: RTCIceCandidate[];
offer: RTCSessionDescription
} = await ctx.request.body.json();
const ice: RTCIceCandidate[] = [];
const answer: RTCSessionDescription = {
sdp: await modifySDP(await getSDP()),
type: "answer"
};
console.log(body.offer);
const remoteIce = body.ice.map((candidate: RTCIceCandidate) => parseIceCandidate(candidate.candidate))
.filter((c: IceCandidate | null) => c !== null)
.filter((c: IceCandidate | null) => c?.transport === "UDP" && c?.candidateType === "host")
//console.log(remoteIce);
if (body) {
ctx.response.status = 200;
ctx.response.body = {
ice: ice,
answer: answer
};
} else {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid JSON" };
}
});