Skip to content

Instantly share code, notes, and snippets.

@pachacamac
Created July 10, 2014 12:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pachacamac/4e1b0297802571126e3d to your computer and use it in GitHub Desktop.
Save pachacamac/4e1b0297802571126e3d to your computer and use it in GitHub Desktop.
web rtc video chat with ruby backend
require 'webrick'
require 'cgi'
require 'erb'
require 'em-websocket'
module EventMachine
module WebSocket
class Connection < EventMachine::Connection
def remote_ip
get_peername[2,6].unpack('nC4')[1..4].join('.')
end
end
end
end
Thread.new {
@ws_clients = []
EM.run do
EM::WebSocket.run(:host => "0.0.0.0", :port => 8008) do |ws|
ws.onopen do |handshake|
puts "WebSocket connection opened from #{ws.remote_ip}"
@ws_clients << ws
end
ws.onclose do
puts "WebSocket connection closed"
@ws_clients.delete(ws)
end
ws.onmessage do |msg|
puts "WebSocket received message '#{msg}'"
@ws_clients.each{|e| e.send(msg) if e != ws}
end
end
end
}
class Busker
def initialize(opts={}, &block)
@routes = {}
(block.arity < 1 ? instance_eval(&block) : block.call(self)) if block_given?
opts[:Port] ||= opts.delete(:port) || 8080
opts[:DocumentRoot] ||= opts.delete(:document_root) || File.expand_path('./')
@server = WEBrick::HTTPServer.new(opts)
@server.mount_proc '' do |req, res|
begin
res.status, res.content_type, method = nil, 'text/html', req.request_method.tr('-', '_').upcase
route, handler = @routes.find{|k,v| k[0].include?(method) && k[1].match(req.path_info)}
params = Hash[CGI::parse(req.query_string||'').map{|k,v| [k.to_sym,v[0]]} + ($~ ? $~.names.map(&:to_sym).zip($~.captures) : [])]
res.status, res.body = route ? [res.status || 200, handler[:block].call(params, req, res)] : [404, 'not found']
rescue => e
@server.logger.error "#{e.message}\n#{e.backtrace.map{|line| "\t#{line}"}.join("\n")}"
res.status, res.body = 500, "#{e}"
end
end
end
def route(path, methods = ['GET'], opts={}, &block)
methods = (methods.is_a?(Array) ? methods : [methods]).map{|e| e.to_s.tr('-', '_').upcase}
path.gsub!(/(:\w+)/){|m| "(?<#{$1[1..-1]}>\\w+)"}
@routes[[methods, Regexp.new("\\A#{path}\\Z")]] = {:opts => opts, :block => block}
end
def render(name)
@templates ||= (Hash[DATA.read.split(/^@@\s*(.*\S)\s*$/)[1..-1].map(&:strip).each_slice(2).to_a] rescue {})
ERB.new(@templates[name.to_s] || File.read(name)).result(binding)
end
def start
@server.start ensure @server.shutdown
end
end
Busker.new(:port => 8000){
route('/'){ render :main }
route('/test'){ render 'meet.html'}
}.start
__END__
@@ main
<!DOCTYPE html>
<html>
<head>
<title>WebRTC P2P Videochat</title>
<style>
body{ background: #333; }
@keyframes blinker { from { opacity: 1; } to { opacity: 0; }}
@-webkit-keyframes blinker { from { opacity: 1; } to { opacity: 0; }}
#notice { color: white; margin-left: 23%;
animation: blinker 1s cubic-bezier(.5, 0, 1, 1) infinite alternate;
-webkit-animation: blinker 1s cubic-bezier(.5, 0, 1, 1) infinite alternate; }
#video { width: 640px; height: 480px; position: absolute; top:0; bottom: 0; left: 0; right: 0; margin: auto; }
#remote-video { width: 640px; height: 480px; }
#local-video { left: 0; top: 0; position: absolute; z-index: 1; width: 160px; height: 120px; }
.mirror { -webkit-transform:scaleX(-1); -moz-transform:scaleX(-1); transform:scaleX(-1); }
</style>
</head>
<body>
<h2 id="notice">&#x21e7; Please allow this &#x21e7;</h2>
<div id="video" class="ceterflex">
<video id="remote-video" autoplay="true" controls="true"></video>
<video id="local-video" class="mirror" autoplay="true" controls="true" muted="true"></video>
</div>
<script>
/* MIT License: https://webrtc-experiment.appspot.com/licence/ */
/* 2013, Muaz Khan<muazkh>--[github.com/muaz-khan] */
/* Demo & Documentation: http://bit.ly/RTCPeerConnection-Documentation */
window.moz = !! navigator.mozGetUserMedia;
var PeerConnection = function (options) {
var PeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection,
SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription,
IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;
// See https://gist.github.com/zziuni/3741933 for a list of public STUN servers
var iceServers = { iceServers: [{url: 'stun:stun.stunprotocol.org'}, {url: 'stun:stunserver.org'},
{url: 'stun:stun.l.google.com:19302'}, {url: 'stun:stun.schlund.de'}] };
var optional = { optional: [] };
// See http://www.webrtc.org/interop under "Constraints / configurations issues."
if (!moz) optional.optional = [{ DtlsSrtpKeyAgreement: true }];
var peerConnection = new PeerConnection(iceServers, optional);
peerConnection.onicecandidate = function(event) {
if (!event.candidate) return;
options.onicecandidate(event.candidate);
}
peerConnection.onaddstream = function(event) {
console.log('------------onaddstream');
options.onaddstream(event.stream);
}
var constraints = options.constraints || {
optional: [],
mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: true }
};
if (moz) constraints.mandatory.MozDontOfferDataChannel = true;
return {
createOffer: function (callback) {
peerConnection.createOffer(function (sessionDescription) {
peerConnection.setLocalDescription(sessionDescription);
callback(sessionDescription);
}, null, constraints);
},
createAnswer: function (offerSDP, callback) {
peerConnection.setRemoteDescription(new SessionDescription(offerSDP));
peerConnection.createAnswer(function (sessionDescription) {
peerConnection.setLocalDescription(sessionDescription);
callback(sessionDescription);
}, null, constraints);
},
setRemoteDescription: function (sdp) {
console.log('--------adding answer sdp:');
console.log(sdp.sdp);
sdp = new SessionDescription(sdp);
peerConnection.setRemoteDescription(sdp);
},
addICECandidate: function (candidate) {
console.log("addICE: got candidate: " + candidate.candidate);
peerConnection.addIceCandidate(new IceCandidate({
sdpMLineIndex: candidate.sdpMLineIndex,
candidate: candidate.candidate
}));
},
addStream: function(stream) {
console.log("stream provided, attaching...");
peerConnection.addStream(stream);
}
};
};
</script>
<script>
/* MIT License: https://webrtc-experiment.appspot.com/licence/ */
var call = function (config) {
var wsAddr = 'ws://'+document.location.hostname;
var port = config.wsPort || document.location.port;
if(port) wsAddr += ':'+port;
var peerConnection = PeerConnection(makePeerConfig()), webSocket = new WebSocket(wsAddr+'/');
// configure the signalling WebSocket
webSocket.onmessage = function (event) {
console.log("received a message: " + event.data);
onIncomingMessage(JSON.parse(event.data));
};
webSocket.push = webSocket.send;
webSocket.send = function (data) { webSocket.push(JSON.stringify(data)); };
function onIncomingMessage(response) {
// the other client has sent me an offer SDP
if (response.offerSDP) {
console.log("received offerSDP " + response.offerSDP + ", will answer");
peerConnection.addStream(config.localStream);
peerConnection.createAnswer(response.offerSDP, function (sdp) {
console.log("sending answer SDP");
webSocket.send({ answerSDP: sdp });
});
}
// the other client has sent me an answer SDP
if (response.answerSDP) { peerConnection.setRemoteDescription(response.answerSDP); }
// the other client has sent me an ICE candidate
if (response.candidate) {
console.log("got a candidate message, passing to RTCPeerConnection");
peerConnection.addICECandidate({
sdpMLineIndex: response.candidate.sdpMLineIndex,
candidate: response.candidate.candidate
});
}
}
// PeerConnection.js's options structure
function makePeerConfig() {
return {
onicecandidate: function (candidate) {
console.log("onICE");
webSocket.send({
candidate: {
sdpMLineIndex: candidate.sdpMLineIndex,
candidate: candidate.candidate
}
});
},
onaddstream: function (stream) {
console.log("onRemoteStream");
config.video['src'] = URL.createObjectURL(stream);
clearInterval(callInterval); //TODO: is this a good place?
}
};
}
return {
initiateCall: function(localStream) {
peerConnection.addStream(localStream); // attach the stream to the peer connection
// create the offer SDP and send it when it's ready
peerConnection.createOffer(function (sdp) {
console.log("sending offer SDP");
webSocket.send({ offerSDP: sdp })
});
}
};
};
</script>
<script>
navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
var config = { video: document.getElementById('remote-video'), wsPort: 8008 };
var callPeer = call(config);
var callInterval = null;
navigator.getUserMedia({ audio: true, video: true },
function (localStream) {
video = document.getElementById('local-video');
video['src'] = URL.createObjectURL(localStream);
config.localStream = localStream;
callInterval = setInterval(function(){ callPeer.initiateCall(config.localStream) }, 1000);
document.getElementById('notice').style.display = 'none';
},
function (e) { console.error(e); }
);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment