Create "working" example
This commit is contained in:
3
gobot-gui/webrtc_test/api/.vscode/settings.json
vendored
Normal file
3
gobot-gui/webrtc_test/api/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"deno.enable": true
|
||||
}
|
||||
0
gobot-gui/webrtc_test/api/a
Normal file
0
gobot-gui/webrtc_test/api/a
Normal file
5
gobot-gui/webrtc_test/api/deno.json
Normal file
5
gobot-gui/webrtc_test/api/deno.json
Normal 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
104
gobot-gui/webrtc_test/api/deno.lock
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
66
gobot-gui/webrtc_test/api/main.ts
Normal file
66
gobot-gui/webrtc_test/api/main.ts
Normal 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,
|
||||
});
|
||||
6
gobot-gui/webrtc_test/api/main_test.ts
Normal file
6
gobot-gui/webrtc_test/api/main_test.ts
Normal 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);
|
||||
});
|
||||
8
gobot-gui/webrtc_test/api/output.sdp
Normal file
8
gobot-gui/webrtc_test/api/output.sdp
Normal 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
|
||||
14
gobot-gui/webrtc_test/api/scripts/ffmpeg_stream.bash
Normal file
14
gobot-gui/webrtc_test/api/scripts/ffmpeg_stream.bash
Normal 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
|
||||
48
gobot-gui/webrtc_test/api/src/iceCandiate.ts
Normal file
48
gobot-gui/webrtc_test/api/src/iceCandiate.ts
Normal 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
|
||||
);
|
||||
}
|
||||
BIN
gobot-gui/webrtc_test/api/test/Big_Buck_Bunny_1080_10s_10MB.mp4
Normal file
BIN
gobot-gui/webrtc_test/api/test/Big_Buck_Bunny_1080_10s_10MB.mp4
Normal file
Binary file not shown.
114
gobot-gui/webrtc_test/api/test/iceCandiates.test.ts
Normal file
114
gobot-gui/webrtc_test/api/test/iceCandiates.test.ts
Normal 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]);
|
||||
})
|
||||
}
|
||||
});
|
||||
145
gobot-gui/webrtc_test/api/webrtc_fake.ts
Normal file
145
gobot-gui/webrtc_test/api/webrtc_fake.ts
Normal 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" };
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user