P2P Over a Remote Network

0
285

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.