playrtc

Client side part of PlayRTC

npm install playrtc
10 downloads in the last week
19 downloads in the last month

PlayRTC

Start your room-based real time app (WebRTC Data Channel & WebSocket) in minutes with Play Framework 2.2

PlayRTC gives you out-of-the-box rooms where members can send P2P messages to each other via WebRTC SCTP Data Channels, and communicate with the server via Websocket. It also provides automatic heartbeats.

The P2P topology is for now * to * (every member is connected to every member). However, if you need to, you can build a custom topology (an overlay network) on top of this. A future version may allow to define a custom topology before the P2P connection part to avoid establishing useless P2P connections.

Client side

playrtc.js depends on backbone-events-standalone and exposes a global Playrtc object.

Installation

Standard include

Take playrtc.js or playrtc.min.js from client/

<script type="text/javascript" src="/assets/javascripts/backbone-events-standalone.min.js"></script>
<script type="text/javascript" src="/assets/javascripts/playrtc.js"></script>

Bower

bower install playrtc

NPM/Browserify

npm install playrtc

Use it

To connect to a room, do:

var io = Playrtc.connect('ws://<your_websocket_endpoint_for_a_particular_room>');
var p2p = io.p2p;
var server = io.server;

io, p2p and server are Event Emitters extending the Backbone.Events model. Take a look there to see all the methods that you can use with a Backbone event emitter.

Playrtc.connect(wsEndpoint, config) can optionally take a config object. config.webrtcConfig allows to define a custom Webrtc config (default to {'iceServers': [{'url': 'stun:stun.l.google.com:19302'}]}).

Playrtc.isCompatible() return true if the browser is compatible with SCTP Data Channels.

io

io is an event emitter object that emits all the room-related control events.

io.on('ready', function() {
  // Connected to the server and to every other members (P2P connection phase finished)
  trace('My id: ' + io.id);
  trace('Other members: ' + io.members); // Array of other member ids
});

io.on('newmember', function(id) {
  trace('New member: ' + id);
});

io.on('memberleft', function(id) {
  trace('Member left: ' + id);
});

io.id is the member id (string).

io.members is an array of the other room member ids

server

server is an event emitter object that emits events (messages) coming from the server.

server.send(eventName, data) allows to send a message to the server.

io.on('ready', function() {
  server.send('ping', {'msg': 'pingmsg'});
});

server.on('pong', function(data) {
  trace('Server Pong msg: ' + data.msg);
});

p2p

p2p is an event emitter object that emits events (messages) coming from peers.

p2p.send(peerId, eventName, data) allows to send a P2P message to a peer.

p2p.broadcast(eventName, data) allows to broadcast a P2P message to all other room members.

io.on('ready', function() {
  p2p.broadcast('ping', {'msg': 'pingmsg'});
});

p2p.on('ping', function(from, data) {
  trace('P2P ping msg from ' + from +': ' + data.msg);
  var to = from;
  p2p.send(to, 'pong', {'msg': 'pongmsg'});
});

p2p.on('pong', function(from, data) {
  trace('P2P pong msg from ' + from +': ' + data.msg);
});

Take a look at main.js in the example app folder for an all-together example.

Server side

The server side code is heavily inspired by play-actor-room. PlayRTC was originally on top of it, but as I found myself re-writing many internal stuffs to adapt it to PlayRTC, I eventually ended up re-writing the code parts of play-actor-room that PlayRTC is using.

As in play-actor-room, each member is represented by 2 actors: a receiver and a sender. For each room there is 1 supervisor which is the parent of the receivers and senders of the members of this room.

Add the following resolver and library to your build.sbt:

resolvers += "Atamborrino repository releases" at "https://github.com/atamborrino/maven-atamborrino/raw/master/releases/"
libraryDependencies += "com.github.atamborrino" %% "playrtc" % "0.1.+"

Then, you must define a receiver to handle incoming messages:

import com.github.atamborrino.playrtc._

class MyReceiver(id: String) extends Receiver(id) {
  import context._

  def customReceive: Receive = {
    case Ready =>
      // Member of id = this.id is connected to every other members via Data Channels
      Logger.info(s"Member of id = $id is ready")
      // do some initialization server side...

    case Msg("ping", data: JsValue) =>
      val maybePong = (data \ "msg").asOpt[String] map { pingmsg =>
        val pongdata = Json.obj("msg" -> "pongmsg")
        Msg("pong", pongdata)
      }
      maybePong.foreach(parent ! Send(id, _))

    case Msg("processThenBroadcast", dataToProcess: JsObject) =>
      // process dataToProcess server side...
      val processed = Msg("broadcastedFromServer", Json.obj("processedData" -> "processed data"))
      parent ! Broadcast(processed) // Broadcast to every member (including ourself)
  }

  override def receive = customReceive orElse super.receive // don't forget this !

  override def postStop() {
    // Disconnection...
  }

}

A receiver can also ask his supervisor for the list of room members parent ! ListMembersIdsand he will receive a message of type MemberIds(ids: Seq[String]).

Heartbeats' interval and delay are respectively 7 seconds and 3 seconds. You can change them by overriding their values in your receiver actor:

import scala.concurrent.duration._

override val HB_INTERVAL = 10 seconds
override val HB_DELAY = 5 seconds

To connect everything, you just need to create your websocket endpoint in a Play controller. Example when the app contains only one room:

val uniqueRoom = Room()

def websocket = WebSocket.using { req =>
  val userid = // generate user id
  uniqueRoom.websocket[MyReceiver](userid)
}

Note that if you want to use custom Akka Props, you can use websocket(userid, myReceiverProps). websocket[MyReceiver](userid) returns a (Iteratee[JsValue, Unit], Enumerator[JsValue]), but if it scares you don't worry, you don't have to deal with it.

But your app usually needs several rooms. To do this you can for example create an actor that stores a map of roomId -> room, and then ask him for the room of id = roomdId. In thise case you will retrieve a room asynchronously:

def websocket(roomId: String) = WebSocket.async { req =>
  val futureRoom = // ask for the room of id = roomId
  val userid = // generate user id

  futureRoom.map(_.websocket[MyReceiver](userid))
}

That's it! A fully-implemented example app is located in the example folder.

Note that if you need more customization, you can override the Supervisor:

class MySupervisor extends Supervisor {
  def customReceive = {
    case CustomMsg => // Your custom msg, sent from a receiver for example
      sender ! CustomMsgAnswer // send an answer to the receiver

    case Broadcast(msg) =>
     val newMsg = // modify original msg
     readyMembers foreach { // readyMembers is a Map[String, Member] (id -> member)
      case (id, member) => member.sender ! Send(id, newMsg)
     }
  }

  override def receive = customReceive orElse super.receive
}

To use your new Supervisor, just pass it as a parameter when your create a Room:

val room = Room(Props[MySupervisor])

Browser support

  • Chrome 32+
  • Firefox 27+

Important note: due to Firefox and Chrome interop, the current implicit maximum data size when using datachannel.send(data) is 16KB (see https://code.google.com/p/webrtc/issues/detail?id=2279). For bigger data, you must do the data-chunking at the application level.

License

This software is licensed under the Apache 2 license, quoted below.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

npm loves you