233 lines
8 KiB
Nim
233 lines
8 KiB
Nim
# https://gitlab.freedesktop.org/libnice/libnice/-/blob/master/examples/sdp-example.c
|
|
# nim c --gc:arc sdp_example.nim
|
|
#
|
|
# Example using libnice to negotiate a UDP connection between two clients,
|
|
# possibly on the same network or behind different NATs and/or stateful
|
|
# firewalls.
|
|
#
|
|
# Run two clients, one controlling and one controlled:
|
|
# ./sdp-example 0 $(host -4 -t A stun.stunprotocol.org | awk '{ print $4 }')
|
|
# ./sdp-example 1 $(host -4 -t A stun.stunprotocol.org | awk '{ print $4 }')
|
|
|
|
## This is a legacy Nim libnice example. Deprecated, we may remove it, or improve the code...
|
|
|
|
# https://forum.nim-lang.org/t/3752
|
|
when (compiles do: import gintro/gtk):
|
|
import gintro/[gtk, glib, gobject, gio, nice]
|
|
else:
|
|
import gintro/[dummygtk, glib, gobject, gio, nice] # For windows with glib but no gtk
|
|
|
|
from strutils import `%`
|
|
from os import paramCount, paramStr
|
|
from strutils import parseInt
|
|
|
|
when defined(windows):
|
|
import winlean
|
|
else:
|
|
discard # import posix
|
|
|
|
var
|
|
gloop: glib.MainLoop
|
|
stunAddr: string
|
|
stunPort: int
|
|
controlling: int
|
|
exitThread: bool
|
|
candidateGatheringDone: bool
|
|
negotiationDone: bool
|
|
gatherMutex: glib.Mutex
|
|
negotiateMutex: glib.Mutex
|
|
gatherCond: glib.Cond
|
|
negotiateCond: glib.Cond
|
|
stateName = ["disconnected", "gathering", "connecting", "connected", "ready", "failed"]
|
|
|
|
proc gDebug(s: string) =
|
|
stdout.write(s)
|
|
flushFile(stdout)
|
|
|
|
proc gError(s: string) =
|
|
stderr.write(s)
|
|
flushFile(stderr)
|
|
|
|
proc toStringVal(s: string): Value =
|
|
let gtype = gStringGetType()
|
|
discard init(result, gtype)
|
|
setString(result, s)
|
|
|
|
proc toBoolVal(b: bool): Value =
|
|
let gtype = gBooleanGetType()
|
|
discard init(result, gtype)
|
|
setBoolean(result, b)
|
|
|
|
proc toIntVal(i: int): Value =
|
|
let gtype = typeFromName("gint")
|
|
discard init(result, gtype)
|
|
setInt(result, i)
|
|
|
|
proc cbCandidateGatheringDone(agent: nice.Agent; streamID: int) =
|
|
gDebug("SIGNAL candidate gathering done\n")
|
|
glib.lock(gatherMutex)
|
|
candidateGatheringDone = true
|
|
glib.signal(gatherCond)
|
|
glib.unlock(gatherMutex)
|
|
|
|
proc cbComponentStateChanged(agent: nice.Agent; streamID: int; componentID: int; state: int) =
|
|
gDebug("SIGNAL: state changed $1 $2 $3[$4]\n" % [$streamID, $componentID, stateName[state.ord], $state])
|
|
if state == nice.ComponentState.ready.ord:
|
|
glib.lock(negotiateMutex)
|
|
negotiationDone = true
|
|
glib.signal(negotiateCond)
|
|
glib.unlock(negotiateMutex)
|
|
elif state == nice.ComponentState.failed.ord:
|
|
glib.quit(gloop)
|
|
|
|
# proc nice_agent_attach_recv*(self: ptr Agent00; streamID: cuint; componentID: cuint; ctx: ptr glib.MainContext;
|
|
# fn: AgentRecvFunc; data: pointer): gboolean {.importc, libprag.}
|
|
#
|
|
# this has to be a low level function!
|
|
# AgentRecvFunc* = proc (agent: ptr Agent00; streamId: uint32; componentId: uint32; len: uint32;
|
|
# buf: cstring; userData: pointer) {.cdecl.}
|
|
proc cbNiceRecv(agent: ptr nice.Agent00; streamID: cuint; componentID: cuint; len: cuint; buf: cstring; userData: pointer) {.cdecl.} =
|
|
if len == 1 and buf[0] == '\x00':
|
|
glib.quit(gloop)
|
|
discard stdout.writeBuffer(buf, len)
|
|
flushfile(stdout)
|
|
|
|
proc exampleThread(data: pointer): pointer {.cdecl.} =
|
|
var
|
|
agent: nice.Agent
|
|
ioStdin: glib.IOChannel
|
|
streamID: int
|
|
line: string
|
|
sdp: string
|
|
sdp64: string
|
|
|
|
# https://gist.github.com/demotomohiro/c9d9ad7e17639c6acb9bd2be7c204f3f
|
|
when hostOS == "windows":
|
|
ioStdin = win32NewFd(system.stdin.getFileHandle)
|
|
else:
|
|
ioStdin = unixNew(system.stdin.getFileHandle)
|
|
discard setFlags(ioStdin, glib.IOFlags.nonBlock) # not implemented for windows? But it is used in C code!
|
|
|
|
# Create the nice agent
|
|
agent = nice.newAgent(glib.getContext(gloop), nice.CompatibilityRfc5245)
|
|
if agent == nil:
|
|
gError("Failed to create agent")
|
|
# Set the STUN settings and controlling mode
|
|
if stunAddr.len > 0:
|
|
setProperty(agent, "stun-server", toStringVal(stunAddr))
|
|
setProperty(agent, "stun-server-port", toIntVal(stunPort))
|
|
setProperty(agent, "controlling-mode", toIntVal(controlling))
|
|
# Connect to the signals
|
|
agent.connect("candidate-gathering-done", cbCandidateGatheringDone)
|
|
agent.connect("component-state-changed", cbComponentStateChanged)
|
|
# Create a new stream with one component
|
|
streamID = addStream(agent, 1)
|
|
if streamID == 0:
|
|
gError("Failed to add stream")
|
|
discard setStreamName(agent, streamID, "text")
|
|
# Attach to the component to receive the data
|
|
# Without this call, candidates cannot be gathered
|
|
discard attachRecv(agent, streamID, 1, getContext(gloop), cbNiceRecv, nil)
|
|
# Start gathering local candidates
|
|
if not gatherCandidates(agent, stream_id):
|
|
gError("Failed to start candidate gathering")
|
|
gDebug("waiting for candidate-gathering-done signal...")
|
|
lock(gatherMutex)
|
|
while not exitThread and not candidateGatheringDone:
|
|
wait(gatherCond, gatherMutex)
|
|
unlock(gatherMutex)
|
|
block gotoEnd:
|
|
if exitThread:
|
|
break gotoEnd
|
|
# Candidate gathering is done. Send our local candidates on stdout
|
|
sdp = generateLocalSdp(agent)
|
|
stdout.write("Generated SDP from agent :\n$1\n\n" % [sdp])
|
|
stdout.write("Copy the following line to remote client:\n")
|
|
sdp64 = base64Encode(sdp)
|
|
#stdout.write("\n $1\n" % [sdp64])
|
|
stdout.write("\n ", sdp64, "\n")
|
|
# Listen on stdin for the remote candidate list
|
|
stdout.write("Enter remote data (single line, no wrapping):\n")
|
|
stdout.write("> ")
|
|
flushFile(stdout)
|
|
while not exitThread:
|
|
var terminatorPos: uint64
|
|
var length: uint64
|
|
var s: IOStatus = readLine(ioStdin, line, length, terminatorPos)
|
|
if s == IOStatus.normal:
|
|
var sdpLen: uint64
|
|
let sdp = cast[string](base64Decode(line, sdpLen)) #seq[uint8]
|
|
# Parse remote candidate list and set it on the agent
|
|
if sdp.len > 0 and parseRemoteSdp(agent, sdp) > 0:
|
|
break
|
|
else:
|
|
write(stderr, "ERROR: failed to parse remote data\n") # here we may get an error -- we have to debug!
|
|
write(stdout, "Enter remote data (single line, no wrapping):\n")
|
|
stdout.write("> ")
|
|
flushFile(stdout)
|
|
elif s == IOStatus.again:
|
|
usleep(100000)
|
|
gDebug("waiting for state READY or FAILED signal...")
|
|
lock(negotiateMutex)
|
|
while not exitThread and not negotiationDone:
|
|
wait(negotiateCond, negotiateMutex)
|
|
unlock(negotiateMutex)
|
|
if exitThread:
|
|
break gotoEnd
|
|
# Listen to stdin and send data written to it
|
|
stdout.write("\nSend lines to remote (Ctrl-D to quit):\n")
|
|
stdout.write("> ")
|
|
flushFile(stdout)
|
|
while not exitThread:
|
|
var terminatorPos: uint64
|
|
var length: uint64
|
|
var s: IOStatus = readLine(ioStdin, line, length, terminatorPos);
|
|
if s == IOStatus.normal:
|
|
discard send(agent, streamID, 1, line.len, line)
|
|
stdout.write("> ")
|
|
flushFile(stdout)
|
|
elif s == IOStatus.again:
|
|
usleep(100000)
|
|
else:
|
|
# Ctrl-D was pressed.
|
|
discard send(agent, stream_id, 1, 1, "\x00")
|
|
break
|
|
# END label
|
|
quit(gloop)
|
|
return nil
|
|
|
|
proc main =
|
|
#echo paramCount()
|
|
#for i in 0 .. paramCount():
|
|
# echo paramStr(i)
|
|
let argc = paramCount() + 1
|
|
var argv = newSeq[string](argc)
|
|
for i in 0 ..< argc:
|
|
argv[i] = paramStr(i)
|
|
var gexamplethread: glib.Thread
|
|
# Parse arguments
|
|
if argc > 4 or argc < 2 or argv[1].len > 1: # argv[1][1] != '\x00':
|
|
write(stderr, "Usage: $1 0|1 stun_addr [stun_port]\n" % [argv[0]])
|
|
quit(QuitFailure)
|
|
controlling = argv[1][0].ord - '0'.ord
|
|
if controlling != 0 and controlling != 1:
|
|
write(stderr, "Usage: $1 0|1 stun_addr [stun_port]\n" % [argv[0]])
|
|
quit(QuitFailure)
|
|
if argc > 2:
|
|
stun_addr = argv[2]
|
|
if argc > 3:
|
|
stun_port = parseInt(argv[3])
|
|
else:
|
|
stun_port = 3478
|
|
gDebug("Using stun server \'[$1]:$2\'\n" % [$stunAddr, $stunPort])
|
|
gio.networkingInit()
|
|
gloop = newMainLoop(nil, false)
|
|
# Run the mainloop and the example thread
|
|
exitThread = false
|
|
gexamplethread = newThread("example thread", exampleThread, nil)
|
|
run(gloop)
|
|
exitThread = true
|
|
discard join(gexamplethread)
|
|
quit(QuitSuccess)
|
|
|
|
main()
|