Skip to content

Instantly share code, notes, and snippets.

@demotomohiro
Last active October 31, 2020 13:38
Show Gist options
  • Save demotomohiro/c9d9ad7e17639c6acb9bd2be7c204f3f to your computer and use it in GitHub Desktop.
Save demotomohiro/c9d9ad7e17639c6acb9bd2be7c204f3f to your computer and use it in GitHub Desktop.
Nim version of libnice example code
# Nim version of example code of libnice:
# https://libnice.freedesktop.org
#
# Refer libnice/examples/simple-example.c
# Push enter key at same time as possible when you enter remote data.
#
# Public STUN server list:
# https://gist.github.com/mondain/b0ec1cf5f60ae726202e
import gintro/[nice, gio, glib, gobject]
import os, strformat, strutils, nativesockets, net, strscans
# See https://gitlab.gnome.org/GNOME/glib/-/blob/master/glib/gslist.h
type GSList {.pure.} = object
data: pointer
next: ptr GSList
var
gloop: MainLoop
ioStdin: IOChannel
streamId: int
iterator items(list: ptr GSList): pointer =
var item = list
while item != nil:
yield item.data
item = item.next
proc toStringVal(s: string): Value =
let gtype = typeFromName("gchararray")
discard init(result, gtype)
setString(result, s)
proc toUIntVal(i: int): Value =
let gtype = typeFromName("guint")
discard init(result, gtype)
setUint(result, i)
proc toBoolVal(b: bool): Value =
let gtype = typeFromName("gboolean")
discard init(result, gtype)
setBoolean(result, b)
proc upCast[T: object, U](gobj: ptr T): U =
static:
assert T is gobject.Object00
let qdata = g_object_get_qdata(gobj, Quark)
assert qdata != nil
result = cast[type(result)](qdata)
assert(result.impl == gobj)
proc toAgent(src: ptr Agent00): Agent = upCast[Agent00, Agent](src)
proc port(niceAddr: NiceAddress): Port =
if niceAddr.`addr`.sa_family.cint == toInt(AF_INET):
result = ntohs(niceAddr.ip4.sin_port).Port
elif niceAddr.`addr`.sa_family.cint == toInt(AF_INET6):
result = ntohs(niceAddr.ip6.sin6_port).Port
else:
raise newException(ValueError, "Neither IPv4 nor IPv6")
proc addrString(niceAddr: NiceAddress): string =
getAddrString(unsafeAddr niceAddr.`addr`)
proc printLocalData(agent: Agent; streamId: uint32; component_id: uint32) =
var localUfrag, localPassword: string
assert agent.localCredentials(streamId.int, localUfrag, localPassword)
stdout.write localUfrag, ' ', localPassword
let cands = cast[ptr GSList](agent.localCandidates(streamId.int, componentId.int))
assert cands != nil
for data in cands:
let
c = cast[ptr NiceCandidate](data)
stdout.write &" {c.foundation.addr.cstring},{c.priority},{c.`addr`.addrString},{c.`addr`.port},{c.`type`}"
stdout.write '\n'
proc cbCandidateGatheringDone(self: ptr Agent00; streamId: uint32; data: pointer) {.cdecl.} =
var agent = self.toAgent
echo "Copy this line to remote client:"
stdout.write "\n "
agent.printLocalData(streamId, 1)
stdout.write '\n'
while true:
block takeInput:
echo "Enter remote data (single line, no wrapping):"
stdout.write "> "
stdout.flushFile
let line = system.stdin.readLine
var
ufrag, passwd: string
remoteCandidates: seq[tuple[list: GSList, cand: NiceCandidate]]
var i = 0
for x in line.split(' '):
case i:
of 0:
ufrag = x
of 1:
passwd = x
else:
var
foundation, ipaddr, typ: string
priority, port: int
if not scanf(x, "$*,$i,$*,$i,$*$.", foundation, priority, ipaddr, port, typ):
echo "Invalid candidate"
break takeInput
let ipAddress = ipaddr.parseIpAddress
var
sockAddr: Sockaddr_storage
sockLen: SockLen
toSockAddr ipAddress, port.Port, sockAddr, sockLen
var cand = NiceCandidate(
`type`: parseEnum[CandidateType](typ),
transport: CandidateTransport.udp,
priority: priority.uint32,
streamId: streamId,
componentId: 1
)
copyMem addr cand.`addr`, addr sockAddr,sockLen
copyMem addr cand.foundation, foundation.cstring, min(foundation.len, CANDIDATE_MAX_FOUNDATION - 1)
remoteCandidates.add (list: GSList(), cand: cand)
remoteCandidates[^1].list.data = addr remoteCandidates[^1].cand
if remoteCandidates.len > 1:
remoteCandidates[^2].list.next = addr remoteCandidates[^1].list
inc i
if ufrag.len == 0 or passwd.len == 0:
echo "ufrag or passwd is empty"
break takeInput
if remoteCandidates.len == 0:
echo "No candidates"
break takeInput
if not agent.setRemoteCredentials(streamId.int, ufrag.cstring, passwd.cstring):
echo "failed to set remote credentials"
break takeInput
if agent.setRemoteCandidates(streamId.int, 1, cast[ptr pointer](addr remoteCandidates[0].list)) < 1:
echo "failed to set remote candidates"
break takeInput
return
proc cbNewSelectedPair(self: ptr Agent00; streamId: uint32; componentId:
uint32; lfoundation: cstring; rfoundation: cstring; data: pointer) {.cdecl.} =
echo "SIGNAL: selected pair ", lfoundation, ' ', rfoundation
proc cbStdinSendData(source: ptr IOChannel00; condition: IOCondition; data: pointer): gboolean {.cdecl.} =
var
agent = cast[ptr Agent00](data).toAgent
src = IOChannel(impl: source, ignoreFinalizer: true)
line: string
length, terminatorPos: uint64
if src.readLine(line, length, terminatorPos) == IOStatus.normal:
discard agent.send(streamId, 1, line.len, line.cstring)
stdout.write "> "
stdout.flushFile
else:
discard agent.send(streamId, 1, 1, "\0")
gloop.quit
return GTrue
proc cbComponentStateChanged(self: ptr Agent00; streamId: uint32; componentId: uint32;
state: uint32; xdata: pointer) {.cdecl.} =
let st = ComponentState(state)
echo &"SIGNAL: state changed {streamId} {componentId} {st}"
var agent = self.toAgent
if st == ComponentState.connected:
var
local, remote: ptr NiceCandidate
loc = Candidate(impl: cast[ptr Candidate00](addr local), ignoreFinalizer: true)
rem = Candidate(impl: cast[ptr Candidate00](addr remote), ignoreFinalizer: true)
if agent.getSelectedPair(streamId.int, componentId.int, loc, rem):
echo &"Negotiation complete: ([{local.`addr`.addrString}]:{local.`addr`.port} [{remote.`addr`.addrString}]:{remote.`addr`.port})"
echo "Send lines to remote (Ctrl-D to quit):"
discard ioStdin.ioAddWatch(glib.PRIORITY_DEFAULT, IOCondition.`in`, cbStdinSendData, self, nil)
elif st == ComponentState.failed:
gloop.quit
proc cbNiceRecv(agent: ptr Agent00; streamId: uint32; componentId: uint32; len: uint32;
buf: cstring; userData: pointer) {.cdecl.} =
if len == 1 and buf[0] == '\0':
gloop.quit
var msg = newString(len)
for i in 0..<len:
msg[i] = buf[i]
msg.removeSuffix
echo msg
stdout.write "> "
stdout.flushFile
proc main =
if paramCount() > 3 or paramCount() == 0 or paramStr(1).len != 1 or paramStr(1)[0] notin {'0', '1'}:
quit &"Usage: {getAppFilename()} 0|1 stun_addr [stun_port]"
let
controlling = paramStr(1)[0] == '1'
stunAddr = if paramCount() >= 2: paramStr(2) else: ""
stunPort = if paramCount() >= 3: parseUInt(paramStr(3)) else: 3478
if stunAddr.len != 0:
echo &"Using stun server '[{stunAddr}]:{stunPort}'"
networkingInit()
gloop = newMainLoop(nil, false)
when hostOS == "windows":
ioStdin = win32NewFd(system.stdin.getFileHandle)
else:
ioStdin = unixNew(system.stdin.getFileHandle)
var agent = newAgent(gloop.context, CompatibilityRfc5245)
if stunAddr.len != 0:
var stunIp: string
if stunAddr.isIpAddress:
stunIp = stunAddr
else:
stunIp = stunAddr.getHostByName.addrList[0]
agent.setProperty "stun-server", stunIp.toStringVal
agent.setProperty "stun-server-port", stunPort.int.toUIntVal
agent.setProperty "controlling-mode", toBoolVal controlling
discard agent.scCandidateGatheringDone(cbCandidateGatheringDone, nil, {})
discard agent.scNewSelectedPair(cbNewSelectedPair, nil, {})
discard agent.scComponentStateChanged(cbComponentStateChanged, nil, {})
streamId = agent.addStream 1
if streamId == 0:
quit "Failed to add stream"
assert agent.attachRecv(streamId, 1, gloop.context, cbNiceRecv, nil)
if not agent.gatherCandidates(streamId):
quit "Failed to start candidate gathering"
gloop.run
main()
# This is additional libnice wrapper code.
# Add following branch after `if namespace == "GObject":` block in `proc main` in gintro/tests/gen.nim
# if namespace == "Nice":
# output.write("include niceimpl\n")
#
# and `main("Nice")` after `main("cairo")` in `proc launch()`
# Add `nice_agent_attach_recv` that is not introspectable.
# https://gitlab.freedesktop.org/libnice/libnice/-/issues/37
proc nice_agent_attach_recv(
self: ptr Agent00;
streamId: uint32;
componentId: uint32;
ctx: ptr glib.MainContext00;
`func`: AgentRecvFunc;
data: pointer): gboolean {.
importc, libprag.}
proc attachRecv*(
self: Agent;
streamId: int;
componentId: int;
ctx: glib.MainContext;
`func`: AgentRecvFunc;
data: pointer): bool =
toBool(
nice_agent_attach_recv(
cast[ptr Agent00](self.impl),
uint32(streamId),
uint32(componentId),
cast[ptr glib.MainContext00](ctx.impl),
`func`,
data))
when defined(windows):
import winlean
else:
import posix
type
# See https://gitlab.freedesktop.org/libnice/libnice/-/blob/master/agent/address.h
NiceAddress* {.union.} = object
`addr`*: SockAddr
ip4*: Sockaddr_in
ip6*: Sockaddr_in6
# See https://gitlab.freedesktop.org/libnice/libnice/-/blob/master/agent/candidate.h
NiceCandidate* {.pure.} = object
`type`*: CandidateType
transport*: CandidateTransport
`addr`*: NiceAddress
baseAddr*: NiceAddress
priority*: uint32
streamId*: uint32
componentId*: uint32
foundation*: array[0 .. (CANDIDATE_MAX_FOUNDATION.int - 1), char]
username*: cstring
password*: cstring
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment