P2P Over a Remote Network

To use networking remotely, you need an RTMFP-capable server, such as Flash Media Server.

If you do not have access to such a server, Adobe provides a beta developer key to use its Cirrus service. Sign up to instantly receive a developer key and a URL, at http://labs .adobe.com/technologies/cirrus/.

The traditional streaming model requires clients to receive all data from a centralized server cluster. Scaling is achieved by adding more servers. Figure 15-2 shows traditional streaming/communication with the Unicast model and RTMFP in Flash Player/Cirrus.

Figure 15-2. Traditional streaming/communication with the Unicast model (left) and RTMFP in Flash Player 10.1/Cirrus 2 (right)
Figure 15-2. Traditional streaming/communication with the Unicast model (left) and RTMFP in Flash Player 10.1/Cirrus 2 (right)

RTMFP, now in its second generation, supports application-level multicast. Multicasting is the process of sending messages as a single transmission from one source to the group where each peer acts as a relay to dispatch the data to the next peer. It reduces the load on the server and there is no need for a streaming server.

The Cirrus service is only for clients communicating directly. It has low latency and good security. It does not support shared objects or custom server-side programming. You could still use shared objects with Flash Media Server, but via the traditional clientserver conduit.

The NetGroup uses ring topology. Its neighborCount property stores the number of peers. Each peer is assigned a peerID, which can be mapped to a group address using Group.convertPeerIDToGroupAddress(connection.nearID). An algorithm is run every few seconds to update ring positions for the group.

When the group is connected, you can obtain statistics such as Quality of Service in bytes per second from NetGroup’s info property:

[code]

function onStatus(event:NetStatusEvent):void {
if (event.info.code == NetGroup.Connect.Success”) {
trace(event.info.group);
// NetGroupInfo object with Quality of Service statistics
}
}

[/code]

The NetStream object is now equipped with new multicast properties. For instance, multicastWindowDuration specifies the duration in seconds of the peer-to-peer multicast reassembly window. A short value reduces latency but also quality.

NetGroup is best used for an application with a many-to-many spectrum. NetStream is for a one-to-many or few-to-many spectrum.

Communication can be done in different ways:

  • Posting is for lots of peers sending small messages.
  • Multicasting is for any size group, but with a small number of the peers being senders, and for continuous/live data which is large over time.
  • Direct routing is for sending messages to specific peers in the group using methods such as sendToAllNeighbors, sendToNeighbor, and sendToNearest.
  • Object replication is for more reliable data delivery whereby information is sent in packets between clients and reassembled.

Matthew Kaufman explains this technology in depth in his MAX 2009 presentation, at http://tv.adobe.com/watch/max-2009-develop/p2p-on-the-flash-platform-with-rtmfp.

Simple Text Chat

This example is very similar to the one we created for P2P over a local network, except for a few minor, yet important, changes.

The connection is made to a remote server using the NetConnection object and RTMFP. If you have the Adobe URL and developer key, use them as demonstrated in the following code:

[code]

const SERVER:String = “rtmfp://” + YOUR_SERVER_ADDRESS;
const KEY:STRING = YOUR_DEVELOPER_KEY;
var connection:NetConnection = new NetConnection();
connection.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection.connect(SERVER, KEY);

[/code]

Connecting to a traditional streaming server would still use the URI construct as “rtmfp://server/application/instance” and additional optional parameters to connect, such as a login and password.

The GroupSpecifier now needs serverChannelEnabled set to true to use the Cirrus server, and helps in peer discovery. PostingEnabled is still on to send messages. The IPMulticastAddress property is optional but can help optimize the group topology if the group is large:

[code]

function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success”) {
var groupSpec:GroupSpecifier = new GroupSpecifier(“chatGroup”);
groupSpec.postingEnabled = true;
groupSpec.serverChannelEnabled = true;
group = new NetGroup(connection,
groupSpec.groupspecWithAuthorizations());
group.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
}
}

[/code]

The exchange of messages is very similar to the local example. Note that a post method is well suited for many peers sending small messages, as in a chat application that is not time-critical:

[code]

function sendMessage():void {
var object:Object = new Object();
object.user = “Véronique”;
object.message = “This is a chat message”;
object.time = new Date().time;
group.post(object);
}
function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetGroup.Posting.Notify”) {
trace(event.info.message);
}
}

[/code]

Multicast Streaming

This example demonstrates a video chat between one publisher and many receivers who help redistribute the stream to other receivers.

The application connects in the same way as in the previous example, but instead of a NetGroup, we create a NetStream to transfer video, audio, and messages.

Publisher

This is the code for the publisher sending the stream.

To access the camera, add the permission in your descriptor file:

[code]<uses-permission android:name=”android.permission.CAMERA”/>[/code]

Set the GroupSpecifier and the NetStream. The GroupSpecifier needs to have multicas tEnabled set to true to support streaming:

[code]

import flash.net.NetStream;
var outStream:NetStream;
function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success”) {
var groupSpec:GroupSpecifier = new GroupSpecifier(“videoGroup”);
groupSpec.serverChannelEnabled = true;
groupSpec.multicastEnabled = true;
outStream = new NetStream(connection,
groupSpec.groupspecWithAuthorizations());
outStream.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
}
}

[/code]

Once the NetStream is connected, add a reference to the camera and the microphone and attach them to the stream. A Video object displays the camera feed. Finally, call the publish method and pass the name of your choice for the video session:

[code]

function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetStream.Connect.Success”) {
var camera:Camera = Camera.getCamera();
var video:Video = new Video();
video.attachCamera(camera);
addChild(video);
outStream.attachAudio(Microphone.getMicrophone());
outStream.attachCamera(camera);
outStream.publish(“remote video”);
}
}

[/code]

Recipients

The code for the peers receiving the video is similar, except for the few changes described next.

The incoming NetStream, used for the peers receiving the stream, must be the same GroupSpecifier as the publisher’s stream. The same stream cannot be used for sending and receiving:

[code]

var inStream:NetStream = new NetStream(connection,
groupSpec.groupspecWithAuthorizations());
inStream.addEventListener(NetStatusEvent.NET_STATUS, onStatus);

[/code]

The recipient needs a Video object but no reference to the microphone and the camera. The play method is used to stream the video in:

[code]

var video:Video = new Video();
addChild(video);
inStream.play(“remote video”);

[/code]

Sending and receiving data

Along with streams, NetStream can be used to send data. It is only an option for the publisher:

[code]

var object:Object = new Object();
object.type = “chat”;
object.message = “hello”;
outStream.send(“onReceiveData”, object);

[/code]

To receive data, the incoming stream must assign a NetStream.client for callbacks. Note that the onReceiveData function matches the first parameter passed in the publisher send call:

[code]

inStream.client = this;
function onReceiveData(object:Object):void {
trace(object.type, object.message); // chat, hello
}

[/code]

Closing a stream

Do not forget to remove the stream and its listener after it closes:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetStream.Connect.Closed” :
case “NetStream.Connect.Failed” :
onDisconnect();
break;
}
}
function onDisconnect():void {
stream.removeEventListener(NetStatusEvent.NET_STATUS, onStatus);
stream = null;
}
group.peerToPeerDisabled = false;
group.objectReplicationEnabled = true;

[/code]

End-to-End Stream

Another approach is for the publisher to send a separate stream to each receiver. This limits the number of users, but is the most efficient transmission with the lowest latency. No GroupSpecifier is needed for this mode of communication. In fact, this is no longer a group, but a one-to-one transfer or unidirectional NetStream channel.

Sending a peer-assisted stream

Set the connection parameter to NetStream.DIRECT_CONNECTIONS; the stream now has its bufferTime property set to 0 for maximum speed:

[code]

var outStream:NetStream =
new NetStream(connection, NetStream.DIRECT_CONNECTIONS);
outStream.bufferTime = 0;
outStream.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
var video:Video = new Video();
var camera:Camera = Camera.getCamera();
video.attachCamera(camera);
addChild(video);
outStream.attachAudio(Microphone.getMicrophone());
outStream.attachCamera(camera);
outStream.publish(“privateVideo”);

[/code]

When first connected, every peer is assigned a unique 256-bit peerID. Cirrus uses it to match it to your IP address and port number when other peers want to communicate with you, as in this example. nearID represents you:

[code]

var myPeerID:String
function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success) {
myPeerID = connection.nearID;
trace(myPeerID);
// 02024ab55a7284ad9d9d4586dd2dc8d2fa1b207e53118d93a34abc946836fa4
}
}

[/code]

The receivers need the peerID of the publisher to subscribe. The publisher needs a way to communicate the ID to others. In a professional application, you would use a web service or a remote sharedObject, but for web development, or if you know the people you want to communicate with, you can send your peerID in the body of an email:

[code]

var myPeerID:String
function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success”) {
myPeerID = connection.nearID;
navigateToURL(new URLRequest(‘mailto:FRIEND_EMAIL?subject=id&body=’+
myPeerID));
}
}

[/code]

The streams are not sent until another endpoint subscribes to the publisher’s stream.

Receiving a stream

In this example, the subscribers get the ID via email and copy its content into the system clipboard. Then they press the giveMe button:

[code]

var giveMe:Sprite = new Sprite();
giveMe.y = 100;
var g:Graphics = giveMe.graphics;
g.beginFill(0x0000FF);
g.drawRect(20, 20, 100, 75);
g.endFill();
giveMe.addEventListener(MouseEvent.CLICK, startStream);

[/code]

The startStream method gets the content of the clipboard and uses it to create the stream. The ID needs to be passed as the second parameter in the stream constructor:

[code]

function startStream():void {
var id:String =
Clipboard.generalClipboard.getData(ClipboardFormats.TEXT_FORMAT) as String;
var inStream:NetStream = new NetStream(connection, id);
inStream.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
var video:Video = new Video();
addChild(video);
inStream.play(“privateVideo”);
video.attachNetStream(inStream);
}

[/code]

The publisher has control, if needed, over accepting or rejecting subscribers. When a subscriber attempts to receive the stream, the onPeerConnect method is invoked. Create an object to capture the call. The way to monitor whom to accept (or not) is completely a function of your application:

[code]

var farPeerID:String;
var outClient:Object = new Object();
outClient.onPeerConnect = onConnect;
outStream.client = outClient;
function onConnect(stream:NetStream):Boolean {
farPeerID = stream.farID;
return true; // accept
OR
return false; // reject
}

[/code]

The publisher stream has a peerStreams property that holds all the subscribers for the publishing stream. Use NetStream.send() to send messages to all the recipients or Net Stream.peerStreams[0].send() for an individual user, here the first one in the list.

NetConnection.maxPeerConnections returns the limit of peer streams, typically set to a maximum of eight.

Directed Routing

Directed routing is for sending data to a specific peer in a group. Peers can send each other messages if they know their counterpart PeerID. This feature only works in a group via NetGroup. It is not available via NetStream.

Sending a message

Individual messages can be sent from one neighbor to another using the NetGroup.send ToNeighbor method:

[code]

var groupSpec:GroupSpecifier = new GroupSpecifier(“videoGroup”);
groupSpec.postingEnabled = true;
groupSpec.serverChannelEnabled = true;
groupSpec.routingEnabled = true;
var netGroup = new NetGroup(connection,
groupSpec.groupspecWithAuthorizations());
netGroup.addEventListener(NetStatusEvent.NET_STATUS, onStatus);

[/code]

The message is an Object. It needs a destination which is the peer receiving the message. Here, PeerID is converted to a group address. It also needs the message itself. Here, we added the time to make each message unique and a type to filter the conversation:

[code]

var message:Object = new Object();
var now:Date = new Date();
message.time = now.getHours() + “” + now.getMinutes()+ “” + now.getSeconds();
message.destination = group.convertPeerIDToGroupAddress(peerID);
message.value = “south”;
message.type = “direction”;
group.sendToNearest(message, message.destination);

[/code]

Receiving a message

The recipient must be in the same group. The message is received at an event with an info.code value of NetGroup.SendTo.Notify. The recipient checks to see if the message is for her by checking if event.info.fromLocal is true, and if it is not, sends it to the next neighbor until its destination is reached:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetGroup.SendTo.Notify” :
trace(event.info.fromLocal);
// if true, recipient is the intended destination
var message:Object = event.info.message;
(if message.type == “direction”) {
trace(message.value); // south
}
break;
}
}

[/code]

Relay

A simple message relay service was introduced in January 2011. It is not intended for ongoing communication, but rather for a few introductory messages, and is a feature for the Cirrus service only. It requires that the sender knows the PeerID of the recipient.

The sender requests a relay:

[code]

connection.call(“relay”, null, “RECIPIENT_ID”, “hello”);

[/code]

The recipient receives and responds to the relay:

[code]

connection.client = this;
function onRelay(senderID:String, message):void {
trace(senderID); // ID of the sender
trace(message); // “hello”
}

[/code]

Treasure Hunt

This treasure hunt game illustrates various aspects of this technology.

Referring to Figure 15-3, imagine the first user on the left walking outdoors looking for a treasure without knowing where it is. She streams a live video as she walks to indicate her progress. The second user from the left knows where the treasure is but is off-site. She guides the first user by pressing keys, representing the cardinal points, to send directions. Other peers (in the two screens toward the right) can watch the live stream and chat among themselves.

Figure 15-3. The Treasure Hunt activity; the panels shown here are (left to right) for the hunter walking, for the guide, and for users viewing the video and chatting over text
Figure 15-3. The Treasure Hunt activity; the panels shown here are (left to right) for the hunter walking, for the guide, and for users viewing the video and chatting over text

Review the sample code provided in this chapter to build such an application. We covered a one-to-many streaming example. We discussed chat in an earlier example. And we just went over sending direct messages.

As a final exercise, you can put all the pieces together to build a treasure hunt application. Good luck, and please post your results.

Other Multiuser Services

If you want to expand your application beyond what this service offers, several other options are available to set up communication between parties remotely, such the Adobe Media Server, Electrotank’s ElectroServer, and gotoAndPlay()’s SmartFox. All of them require server setup and some financing.

ElectroServer was developed for multiplayer games and tools to build a multiplayer lobby system. One installation scales up to tens of thousands of connected game players with message rates of more than 100,000 messages per second. You can try a free 25- user license (see http://www.electrotank.com/). Server-side code requires Java or ActionScript 1. It supports AIR and Android.

SmartFox is a platform for developing massive multiuser games and was designed with simplicity in mind. It is fast and reliable and can handle tens of thousands of concurrent clients with low CPU and memory usage. It is well documented. You can try a full version with a free 100-user license (see http://www.smartfoxserver.com/). Server-side
code requires Java. It supports AIR and Android.

 

 

P2P Over a Local Network

If your local network supports broadcasting, you can create peer-to-peer direct routing. All the clients need to be on the same subnet, but you do not need to manage them. Verify that your devices have WiFi enabled and are using the same network.

The code to create a peer-to-peer application with RTMFP is quite simple but introduces new concepts. Let’s go over all the steps one at a time.

The connection is established using the flash.net.NetConnection class. Set a listener to receive a NetStatusEvent event. Create the connection by calling the connect function and passing rtmfp as an argument:

[code]

import flash.net.NetConnection;
import flash.events.NetStatusEvent;
var connection:NetConnection = new NetConnection();
connection.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection.connect(“rtmfp:”);

[/code]

Wait for the connection to be established. Then several objects need to be created:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetConnection.Connect.Success” :
trace(“I am connected”);
// object creation can now happen
break;
}
}

[/code]

NetGroup is the group of peers. Its capabilities are defined in the GroupSpecifier. The IPMulticastAddress property stores the IPv4 multicast address. It needs to be in the range 224.0.0.0 through 239.255.255.25. The UDP port should be higher than 1024. A group name is passed in its constructor. Try to make it unique. The IPMulticast MemberUpdatesEnabled property must be set to true for clients to receive updates from other clients on a LAN. The postingEnabled property allows clients to send messages to the group:

[code]

import flash.net.GroupSpecifier;
var groupName:String = “com.veronique.simple/”;
var IPMulticastAddress:String = “230.0.0.1:3000″;
var groupSpec:GroupSpecifier = new GroupSpecifier(groupName);
groupSpec.addIPMulticastAddress(IPMulticastAddress);
groupSpec.ipMulticastMemberUpdatesEnabled = true;
groupSpec.postingEnabled = true;

[/code]

Now create the NetGroup. Pass the connection and the GroupSpecifier in its construction. The latter is passed with an authorization property to define the communication allowed: groupspecWithAuthorizations to post and multicast, or groupspecWithout Authorizations to only receive messages. Note that this setting is only relevant if a posting password is set (as defined by your application):

[code]

import flash.net.NetGroup;
var netGroup = new NetGroup
(connection, groupSpec.groupspecWithAuthorizations());
netGroup.addEventListener(NetStatusEvent.NET_STATUS, onStatus);

[/code]

The group is composed of neighbors, you as well as others. Using the same Net StatusEvent event, check for its info.code. Wait to receive the NetGroup.Connect.Suc cess event before using the functionality of NetGroup to avoid getting an error.

When a user joins or leaves the group, the code is as follows:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case ” NetGroup.Connect.Success” :
trace(“I joined the group”);
break;
case “NetGroup.Connect.Rejected” :
case “NetGroup.Connect.Failed” :
trace(“I am not a member”);
break;
}
}

[/code]

Others in the group receive the following events. Note that if the group is large, only a subset of members is informed that a new peer has joined or left the group:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetGroup.Neighbor.Connect” :
trace(“neighbor has arrived”, neighborCount);
break;
case “NetGroup.Neighbor.Disconnect” :
trace(“neighbor has left”);
break;
}
}

[/code]

To send a message, use the NetGroup.post method. It takes an Object as an argument. Messages are serialized in AMF (binary format for serialized ActionScript objects), so a variation of data types can be used, such as Object, Number, Integer, and String types:

[code]

var message:Object = new Object();
message.type = “testing”;
message.body = {name:”Véronique”, greeting:”Bonjour”};
group.post(message);

[/code]

To receive messages, check for an info.code equal to a NetGroup.Posting.Notify event. The message is received as event.info.message. The message is not distributed to the sender:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetGroup.Posting.Notify” :
trace(event.info.message); // [Object]
trace(event.info.message.body.greeting); // Bonjour
break;
}
}

[/code]

Identical messages are not re-sent. To make each message unique, store the current time as a property of the object:

[code]

var now:Date = new Date();
message.time = now.getHours() + “_” + now.getMinutes() +
“_” + now.getSeconds();
group.post(message);

[/code]

If the message only goes in one direction and there will be no overlap between clients, you could use a counter that gets incremented with every new message:

[code]message.count = count++;[/code]

When disconnecting, it is important to remove all objects and their listeners:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetConnection.Connect.Rejected” :
case “Connect.AppShutdown” :
trace(“I am not connected”);
onDisconnect();
break;
}
}
function onDisconnect():void {
group = null;
netGroup.removeEventListener(NetStatusEvent.NET_STATUS, onStatus);
netGroup = null;
connection.removeEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection = null;
}

[/code]

Color Exchange

Let’s create a simple example. The hueMe application starts with a shape of a random color. Each client can send a color value to the other client’s application. On the receiving end, the shape changes to the new color (see Figure 15-1).

Figure 15-1. The hueMe application
Figure 15-1. The hueMe application

Draw the initial colored sprite:

[code]

var sprite:Sprite = new Sprite();
var g:Graphics = sprite.graphics;
g.beginFill(Math.round(Math.random()*0xFFFFFF));
g.drawRect(20, 20, 200, 150);
g.endFill();

[/code]

Create the connection for the P2P communication:

[code]

var connection:NetConnection = new NetConnection();
connection.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection.connect(“rtmfp:”);

[/code]

Once the connection is established, create the group and check that the user has successfully connected to it:

[code]

function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success”) {
var groupSpec:GroupSpecifier = new GroupSpecifier(“colorGroup”);
groupSpec.addIPMulticastAddress(“225.0.0.1:4000”);
groupSepc.postingEnabled = true;
groupSepc.ipMulticastMemberUpdatesEnabled = true;
group = new NetGroup(connection,
groupSpec.groupspecWithAuthorizations());
group.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
} else if (event.info.code == “NetGroup.Connect.Success”) {
trace(“I am part of the group “);
}
}

[/code]

Send a random color value to the group when clicking the sprite:

[code]

g.addEventListener(MouseEvent.CLICK, hueYou);
function hueYou(event:MouseEvent):void {
var randomHue:int = Math.round(Math.random()*0xFFFFFF);
var object:Object = {type:”color”, hue:randomHue};
group.post(object);
}

[/code]

Finally, add the functionality to receive the value from other members of the group and color the sprite:

[code]

import flash.geom.ColorTransform;
function onStatus(event:NetStatusEvent):void {

if (event.info.code == “NetGroup.Posting.Notify”) {
if (event.info.message.type == “color”) {
applyColor(Number(event.info.message.hue));
}
}
}
function applyColor(hue:int):void {
var colorTransform:ColorTransform = new ColorTransform();
colorTransform.color = hue;
sprite.transform.colorTransform = colorTransform;
}

[/code]

Companion AIR Application

To make your application unidirectional, as in a remote control-style application, have one client sending messages and the other receiving messages. Only the networked clients registered for the NetGroup.Posting.Notify event receive data.

Mihai Corlan developed an Android remote control for a desktop MP3 player; read about it at http://corlan.org/2010/07/02/creating-multi-screen-apps-for-android-and -desktop-using-air/.

Tom Krcha created a remote controller to send accelerometer, speed, and brake information to a car racing game (see http://www.flashrealtime.com/game-remote-device-con troller/).

RTMFP UDP

Peer-to-peer (P2P) communication is the real-time transfer of data and media between clients.

Real Time Media Flow Protocol (RTMFP) is an Adobe proprietary protocol. It enables peer-to-peer communication between applications running in Flash Player or the AIR runtime. It is meant to provide a low-latency, secure, peering network experience.

Real Time Messaging Protocol (RTMP) uses Transmission Control Protocol (TCP). RTMFP uses User Datagram Protocol (UDP), which provides better latency, higher security (128-bit AES encryption), and scalability. RTMP is faster than RTMFP, but does not guarantee perfect message ordering and delivery.

RTMP was designed to connect via an RTMFP-capable server, such as the Cirrus service (RTMP uses the Flash Media Server), but it can also be used over a local network using WiFi without the need for a server. Note that Flash Media Server 4.0 speaks RTMFP as well.