diff --git a/gobot-gui/api/.vscode/settings.json b/gobot-gui/api/.vscode/settings.json new file mode 100644 index 0000000..b943dbc --- /dev/null +++ b/gobot-gui/api/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} \ No newline at end of file diff --git a/gobot-gui/api/asd.json b/gobot-gui/api/asd.json new file mode 100644 index 0000000..e69de29 diff --git a/gobot-gui/api/deno.json b/gobot-gui/api/deno.json index 3c5130f..932d1c0 100644 --- a/gobot-gui/api/deno.json +++ b/gobot-gui/api/deno.json @@ -1,5 +1,8 @@ { "tasks": { "dev": "deno run --watch main.ts" + }, + "imports": { + "@earthstar/dns-sd": "jsr:@earthstar/dns-sd@^3.1.0" } } diff --git a/gobot-gui/api/deno.lock b/gobot-gui/api/deno.lock new file mode 100644 index 0000000..95010d2 --- /dev/null +++ b/gobot-gui/api/deno.lock @@ -0,0 +1,169 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@earthstar/dns-sd": "jsr:@earthstar/dns-sd@3.1.0", + "jsr:@oak/commons@0.7": "jsr:@oak/commons@0.7.0", + "jsr:@oak/oak@14": "jsr:@oak/oak@14.2.0", + "jsr:@std/assert": "jsr:@std/assert@0.218.2", + "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/bytes@^0.224.0": "jsr:@std/bytes@0.224.0", + "jsr:@std/crypto": "jsr:@std/crypto@0.218.2", + "jsr:@std/crypto@0.218": "jsr:@std/crypto@0.218.2", + "jsr:@std/encoding": "jsr:@std/encoding@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/fmt@^0.218.2": "jsr:@std/fmt@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": { + "@earthstar/dns-sd@3.1.0": { + "integrity": "8b76b7ccfe230677f6177227e0250bff350b3b19604b3d8c534c1e2e3318d411", + "dependencies": [ + "jsr:@std/bytes@^0.224.0" + ] + }, + "@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", + "dependencies": [ + "jsr:@std/fmt@^0.218.2" + ] + }, + "@std/bytes@0.218.2": { + "integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670" + }, + "@std/bytes@0.224.0": { + "integrity": "a2250e1d0eb7d1c5a426f21267ab9bdeac2447fa87a3d0d1a467d3f7a6058e49" + }, + "@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/fmt@0.218.2": { + "integrity": "99526449d2505aa758b6cbef81e7dd471d8b28ec0dcb1491d122b284c548788a" + }, + "@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": {} + } + } + }, + "redirects": { + "https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.224.0/testing/asserts.ts", + "https://deno.land/x/port/mod.ts": "https://deno.land/x/port@1.0.0/mod.ts" + }, + "remote": { + "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", + "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", + "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", + "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", + "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", + "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", + "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", + "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", + "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", + "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", + "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", + "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", + "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", + "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", + "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", + "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", + "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", + "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", + "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", + "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", + "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", + "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", + "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", + "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", + "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", + "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", + "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", + "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", + "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e", + "https://deno.land/std@0.224.0/testing/asserts.ts": "d0cdbabadc49cc4247a50732ee0df1403fdcd0f95360294ad448ae8c240f3f5c", + "https://deno.land/x/free_port@v1.2.0/mod.ts": "512646732aaea41fbfd1f210f3ae82660f38251777d189d290da331d0235a58e", + "https://deno.land/x/port@1.0.0/mod.ts": "2dc04ce1ccf133ae09205e30b550044c4c6f64a1a7d00ea91c66dbb9f6cc00f5", + "https://deno.land/x/port@1.0.0/types.ts": "42d6ae4147d5d67408d60209da070ddfa79ec8389c6cab1b8002df0cf6c03af6" + }, + "workspace": { + "dependencies": [ + "jsr:@earthstar/dns-sd@^3.1.0" + ] + } +} diff --git a/gobot-gui/api/main.ts b/gobot-gui/api/main.ts deleted file mode 100644 index be043e9..0000000 --- a/gobot-gui/api/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function add(a: number, b: number): number { - return a + b; -} - -// Learn more at https://deno.land/manual/examples/module_metadata#concepts -if (import.meta.main) { - console.log("Add 2 + 3 =", add(2, 3)); -} diff --git a/gobot-gui/api/output.sdp b/gobot-gui/api/output.sdp new file mode 100644 index 0000000..981ae0b --- /dev/null +++ b/gobot-gui/api/output.sdp @@ -0,0 +1,10 @@ +v=0 +o=- 0 0 IN IP4 127.0.0.1 +s=No Name +c=IN IP4 127.0.0.1 +t=0 0 +a=tool:libavformat 60.16.100 +m=video 1234 RTP/AVP 96 +b=AS:3000 +a=rtpmap:96 MP4V-ES/90000 +a=fmtp:96 profile-level-id=1 diff --git a/gobot-gui/api/scripts/ffmpeg_stream.bash b/gobot-gui/api/scripts/ffmpeg_stream.bash new file mode 100644 index 0000000..6b202ff --- /dev/null +++ b/gobot-gui/api/scripts/ffmpeg_stream.bash @@ -0,0 +1,12 @@ +#/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 \ + -f rtp rtp://127.0.0.1:1234 \ No newline at end of file diff --git a/gobot-gui/api/src/iceCandiate.ts b/gobot-gui/api/src/iceCandiate.ts new file mode 100644 index 0000000..fc3007c --- /dev/null +++ b/gobot-gui/api/src/iceCandiate.ts @@ -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 + ); +} \ No newline at end of file diff --git a/gobot-gui/api/src/main.ts b/gobot-gui/api/src/main.ts new file mode 100644 index 0000000..83da533 --- /dev/null +++ b/gobot-gui/api/src/main.ts @@ -0,0 +1,145 @@ +import { Application,Router } from "jsr:@oak/oak@14"; +import { crypto } from "jsr:@std/crypto"; +import { randomBytes } from "node:crypto"; +import { encodeHex } from "jsr:@std/encoding/hex"; +// @deno-types="npm:@types/sdp-transform" +import sdpTransform from "npm:sdp-transform@2.14.2"; +import { IceCandidate, findHighestPriorityCandidate, parseIceCandidate } from "./iceCandiate.ts"; +import { browse, MulticastInterface } from "jsr:@earthstar/dns-sd"; + +const app = new Application(); +const router = new Router({ + prefix: "/api", +}); + +const rtpIncommingConnection = Deno.listenDatagram({ port: 1234, transport: "udp" }); +let outgoingAddresses: Map = new Map(); + +(async () => { + for await (const [data, addr] of rtpIncommingConnection) { + //console.log(data.length); + + outgoingAddresses.forEach((outgoingAddr) => { + rtpIncommingConnection.send(data, outgoingAddr); + }); + } +})() + +async function getFingerprint(): Promise { + const cert = randomBytes(32); + const hash = encodeHex(await crypto.subtle.digest("SHA-256", cert)); + return hash.split(/(..)/g).filter(s => s !== "").join(":"); +} + +async function resolveMDNS(address: string): Promise { + const pingCommand = new Deno.Command("timeout", { + args: ["2", "ping", "-c", "1", address], + }); + + const { code, stdout, stderr } = await pingCommand.output(); + + if (code !== 0) { + return undefined; + } + + const realAddr = (new TextDecoder().decode(stdout)) + .match(/\((.*)\):/)?.at(1); + + return realAddr +} + +async function findWorkingIceCandiate(candidates: IceCandidate[]): Promise<{ candiate: IceCandidate, ipAddress: string } | undefined> { + const workingCandiates: { + candiate: IceCandidate, + ipAddress: string, + priority: number + }[] = []; + + candidates = candidates.sort((a, b) => a.priority - b.priority); + + for (const candidate of candidates) { + if(candidate.ipAddress === undefined || candidate.transport.toLowerCase() !== "udp") continue; + + const ipAddr = await resolveMDNS(candidate.ipAddress); + console.log(ipAddr, candidate.ipAddress); + if (ipAddr) { + workingCandiates.push({ + candiate: candidate, + ipAddress: ipAddr, + priority: candidate.priority + }); + break; + } + } + + if (workingCandiates.length === 0) { + return undefined; + } else { + return workingCandiates.reduce((highest, candidate) => + candidate.priority > highest.priority ? candidate : highest + ); + } +} + +router.post("/webrtc-offer", async (ctx) => { + const body = await ctx.request.body.json(); + const clientSdp = sdpTransform.parse(body.offer.sdp); + const clientIceRaw: { + candidate: string, + sdpMid: string, + sdpMLineIndex: number + usernameFragment: string + }[] = body.ice; + + const clientIce = clientIceRaw.map((ice) => parseIceCandidate(ice.candidate)); + const remoteIceCandiate = await findWorkingIceCandiate(clientIce); + + if (!remoteIceCandiate) { + ctx.response.status = 400; + ctx.response.type = "application/json"; + ctx.response.body = { error: "None of the provided IceCandiates where reachable" }; + return; + } + + const clientAddr: Deno.NetAddr = { + transport: "udp", + hostname: remoteIceCandiate.ipAddress, + port: remoteIceCandiate.candiate.port + }; + + console.log(clientAddr); + + const ffmpegSdp = sdpTransform.parse(await Deno.readTextFile("./output.sdp")); + ffmpegSdp.media[0].fingerprint = { + type: "sha-256", + hash: await getFingerprint() + } + + ffmpegSdp.iceUfrag = encodeHex(randomBytes(32)); + ffmpegSdp.icePwd = encodeHex(randomBytes(32)); + ffmpegSdp.media[0].fmtp = []; + ffmpegSdp. + //ffmpegSdp.media[0].rtcpMux = ""; + + const sessionId = clientSdp.origin?.sessionId; + if (!sessionId) { + ctx.response.status = 400; + ctx.response.type = "application/json"; + ctx.response.body = { error: "sessionId is undefined" }; + return; + } + + outgoingAddresses.set(sessionId.toString(), clientAddr); + + ctx.response.type = "application/json"; + ctx.response.body = { + "type": "answer", + "sdp": sdpTransform.write(ffmpegSdp) + } +}); + + +app.use(router.routes()); +app.use(router.allowedMethods()); + +app.listen({ port: 3000 }); \ No newline at end of file diff --git a/gobot-gui/api/test/Big_Buck_Bunny_1080_10s_10MB.mp4 b/gobot-gui/api/test/Big_Buck_Bunny_1080_10s_10MB.mp4 new file mode 100644 index 0000000..c0327e9 Binary files /dev/null and b/gobot-gui/api/test/Big_Buck_Bunny_1080_10s_10MB.mp4 differ diff --git a/gobot-gui/api/test/iceCandiates.test.ts b/gobot-gui/api/test/iceCandiates.test.ts new file mode 100644 index 0000000..21304e4 --- /dev/null +++ b/gobot-gui/api/test/iceCandiates.test.ts @@ -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]); + }) + } +}); \ No newline at end of file diff --git a/gobot-gui/frontend/src/lib/Feedtes.svelte b/gobot-gui/frontend/src/lib/Feedtes.svelte new file mode 100644 index 0000000..afc09a7 --- /dev/null +++ b/gobot-gui/frontend/src/lib/Feedtes.svelte @@ -0,0 +1,111 @@ + + + +
+ + + + + + +
+ +

Offer: {offerBase64Out}

+ + +
+ +

Answere: {answerBase64Out}

+ + + +
+

Ice Candiates

+ {#each iceCandidate as ice} +

{ice.address}, {ice.port}, {ice.protocol}

+ {/each} +
\ No newline at end of file diff --git a/gobot-gui/frontend/src/lib/LiveFeed.svelte b/gobot-gui/frontend/src/lib/LiveFeed.svelte index 210ea54..e75bdf4 100644 --- a/gobot-gui/frontend/src/lib/LiveFeed.svelte +++ b/gobot-gui/frontend/src/lib/LiveFeed.svelte @@ -3,11 +3,25 @@ let peerConnection: RTCPeerConnection; let videoElement: HTMLVideoElement; + let incommingStream: MediaStream; async function negotiateWebRTC(peerConn: RTCPeerConnection) { + incommingStream = new MediaStream(); + videoElement.srcObject = incommingStream; peerConn.addTransceiver('video', { direction: 'recvonly' }); - const offer = await peerConn.createOffer(); + const offer = await peerConn.createOffer({ + offerToReceiveVideo: true + }); + let icecandidate: RTCIceCandidate[] = []; + + peerConnection.addEventListener("icecandidate", (event) => { + if(event.candidate) { + icecandidate.push(event.candidate); + } + }); + + // This does ICE gathering await peerConn.setLocalDescription(offer); await new Promise((resolve) => { @@ -15,9 +29,9 @@ resolve(null); } else { peerConn.addEventListener('icegatheringstatechange', () => { - console.log(peerConn.iceGatheringState); - if(peerConn.iceGatheringState === 'complete') + if(peerConn.iceGatheringState === 'complete') { resolve(null); + } }); } }); @@ -27,34 +41,50 @@ headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ offer }) + body: JSON.stringify({ offer, ice: icecandidate}) })).json(); + //console.log(remoteOffer.sdp); + await peerConn.setRemoteDescription(remoteOffer); } onMount(async () => { peerConnection = new RTCPeerConnection({ - iceServers: [] + certificates: [], + iceServers: [], + iceTransportPolicy: "all", + // @ts-ignore + rtcpMuxPolicy: 'negotiate' // Workaround, typescript doesn't know about this property }); peerConnection.addEventListener('track', (event) => { - console.log('track', event); - videoElement.srcObject = event.streams[0]; + event.streams[0].getTracks().forEach(track => { + incommingStream.addTrack(track); + }); + }); + + peerConnection.addEventListener("icecandidate", (event) => { + if(event.candidate) { + console.log(event.candidate.candidate); + } }); await negotiateWebRTC(peerConnection); - peerConnection.onconnectionstatechange = () => { - console.log("PC State:", peerConnection.connectionState); - } + }); + + function start_playing() { + videoElement.play(); + }
- + + - +