Ruby User Group Berlin | October 10, 2013
Signaling server written in Ruby
Talks WebSockets via em-websockets
Allows client to join a room to find other clients
A few defined JSON messages
join_room(room_id, status)
send_to_peer(peer_id, data)
update_status(status)
joined_room(me, peers)
peer_updated_status(peer_id, peer_status)
error(id, message)
shutdown(seconds)
send_to_peer
offer(sdp)
answer(sdp)
ice_candidate(...)
EM.run{
em_init
trap(:TERM){ em_sigterm }
trap(:INT){ em_sigint }
EM::WebSocket.run(options){ |ws|
ws.onopen{ |handshake| ws_open(ws, handshake) }
ws.onmessage{ |message| ws_message(ws, message) }
ws.onclose{ |why| ws_close(ws, why) }
ws.onerror{ |error| ws_error(ws, error) }
EM.error_handler{ |e| em_error(e) }
}
}
def ws_message(ws, message)
ws_message_action(ws, ws_message_parse(ws, message))
rescue MessageParsingError, MessageError => e
send_error(ws, e)
end
def ws_message_parse(ws, message)
connection_id = manager.connections.get_connection_id(ws) or
raise MessageError.new(ws), 'unknown connection'
ClientMessage.new(message, connection_id)
end
def ws_message_action(ws, message_event)
manager.debug "#{message_event.connection_id} <#{message_event.name}>"
manager.public_send(
message_event.name,
message_event.connection_id,
*message_event.arguments
)
end
Stores in which room a connection resides
Stores a connection ids per open socket
Uses asynchronous em-hiredis
driver
def join_room(connection_id, room_id, status)
return_error connection_id, 'no room id given' if !room_id
return_error connection_id, 'room id too long' if room_id.size > 50
# ...
info "#{connection_id} joins ##{room_id[0..10]}... #{status}"
script_join_room(connection_id, room_id, status){ |members|
# ...
def script_join_room(connection_id, room_id, status, &block)
@redis.eval \
SCRIPT_JOIN_ROOM,
4,
"store:room:members:#{room_id}",
"store:room:peak_members:#{room_id}",
"store:connection:joined:#{connection_id}",
"store:connection:room:#{connection_id}",
connection_id,
PAYLOAD_NEW_PEER[connection_id, status],
Time.now.getutc.to_i,
room_id,
&block
end
local members = redis.call('smembers', KEYS[1])
local count = 0
for _, peer_id in pairs(members) do
redis.call('publish', "ps:connection:" .. peer_id, ARGV[2])
count = count + 1
end
redis.call('sadd', KEYS[1], ARGV[1])
if count == 0 or tonumber(redis.call('get', KEYS[2])) <= count then
redis.call('set', KEYS[2], count + 1)
end
redis.call('set', KEYS[3], ARGV[3])
redis.call('set', KEYS[4], ARGV[4])
return members
Using redis' PubSub feature
Allows for multiple EM socket servers running simultanously
Every socket connection listens on its own redis queue
def announce_connection(ws)
connection_id = @connections.register_connection(ws)
info "#{connection_id} <open>"
@subscriber.subscribe "ps:connection:#{connection_id}" do |payload|
ws.send_text(payload)
end
end
# ...
unless %w[offer answer ice_candidate].include? data['event']
return_error connection_id, 'event not allowed'
end
@publisher.publish "ps:connection:#{peer_id}",
(data || {}).merge("sender_id" => connection_id).to_json
Extendable Plugin Architecture
General Usage Stats
signalmaster
webrtc.io
together.js hub
peerjs server
We are working on a webrtc signaling service. You might want to check it out if you consider using webrtc in a future project.
More palava Information
Signaling Service