P2P multicast in Flash Player 10, yeap… ready? This method creates n:n model of p2p communicators. So broadcasting data goes from broadcaster directly to all receivers. All users are broadcasters and receivers. There is no multilevel logic in it, so keep in mind the upload bandwidth of broadcaster. Feel free to use it in your flash applications, just hit me when you decide.
Core classes and files:
- sk.yoz.net.P2PMulticast
- sk.yoz.net.P2PMulticastPersistence
- sk.yoz.net.P2PMulticastStream
- Application.mxml
- http://localhost/p2p/clients.php
- http://localhost/p2p/clients.xml (empty file works just fine)
- All required files (.zip)
P2PMulticast application
sk.yoz.net.P2PMulticast.as is core class that cares about p2p clients and dispatches important events, such as connecting peer, disconnecting peer, incomming data:
package sk.yoz.net { import flash.events.EventDispatcher; import flash.events.TimerEvent; import flash.utils.Timer; import sk.yoz.events.P2PMulticastEvent; import sk.yoz.events.P2PMulticastPersistenceEvent; import sk.yoz.events.P2PMulticastStreamEvent; public class P2PMulticast extends EventDispatcher { private var stream:P2PMulticastStream; private var persistence:IP2PMulticastPersistence; private var clientReloadTimer:Timer; public function P2PMulticast(persistence:IP2PMulticastPersistence, developerKey:String = "", publishName:String = "", maxPeerConnections:uint = 0, handshakeURL:String = "", clientConnectionTimeout:uint = 0) { super(); stream = new P2PMulticastStream(developerKey, publishName, maxPeerConnections, handshakeURL, clientConnectionTimeout); stream.addEventListener(P2PMulticastStreamEvent.CLIENT_ID_ASSIGNED, clientIdAssignedHandler); stream.addEventListener(P2PMulticastStreamEvent.PEER_CONNECTED, peerConnectedHandler); stream.addEventListener(P2PMulticastStreamEvent.PEER_DISCONNECTED, peerDisconnectedHandler); stream.addEventListener(P2PMulticastStreamEvent.PEER_DATA, peerDataHandler); this.persistence = persistence; persistence.addEventListener(P2PMulticastPersistenceEvent.CLIENTS_DEFINED, clientsDefinedHandler); } public function get connected():Boolean { return stream.connected; } public function set debug(value:Boolean):void { if(stream) stream.debug = value; } public function get onlineClients():Array { return stream.onlineClients; } public function broadcast(data:Object):Boolean { return stream.broadcast(data); } private function clientIdAssignedHandler(event:P2PMulticastStreamEvent):void { persistence.getClientList(); dispatchEvent(new P2PMulticastEvent(P2PMulticastEvent.STREAM_CONNECTED)); } private function clientsDefinedHandler(event:P2PMulticastPersistenceEvent):void { var list:Array = event.data.list; for each(var farID:String in list) stream.connectClient(farID); if(list.indexOf(stream.nearID) == -1) list.push(stream.nearID); persistence.saveClientList(list); clientReloadTimer = new Timer(10000 + Math.random() * 5000, 1); clientReloadTimer.addEventListener(TimerEvent.TIMER_COMPLETE, clientReloadTimerComplete); clientReloadTimer.start(); } private function clientReloadTimerComplete(event:TimerEvent):void { persistence.getClientList(); var timer:Timer = event.target as Timer; timer.removeEventListener(TimerEvent.TIMER_COMPLETE, clientReloadTimerComplete); timer = null; } private function peerDataHandler(event:P2PMulticastStreamEvent):void { dispatchEvent(new P2PMulticastEvent(P2PMulticastEvent.PEER_DATA, false, false, event.data)); } private function peerConnectedHandler(event:P2PMulticastStreamEvent):void { var farID:String = event.data.farID; verifyClient(farID); dispatchEvent(new P2PMulticastEvent(P2PMulticastEvent.PEER_CONNECTED, false, true, event.data)); } private function peerDisconnectedHandler(event:P2PMulticastStreamEvent):void { var farID:String = event.data.farID; verifyClient(farID); dispatchEvent(new P2PMulticastEvent(P2PMulticastEvent.PEER_DISCONNECTED, false, false, event.data)); } private function verifyClient(farID:String):void { if(!stream.allConnected) return; var list:Array = stream.getClientList(); list.push(stream.nearID); persistence.saveClientList(list); } } }
sk.yoz.net.P2PMulticastPersistence.as class works as database of online clients. Before peer connection can be established, clients need to know each other ID. This class works as persistence where all active IDs are stored. This one is based on .xml (data) and .php (processor), but you can create your own persistence lets say based on database…:
package sk.yoz.net { import com.adobe.crypto.MD5; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.HTTPStatusEvent; import flash.events.IOErrorEvent; import flash.events.ProgressEvent; import flash.events.SecurityErrorEvent; import flash.net.URLLoader; import flash.net.URLRequest; import flash.net.URLRequestMethod; import flash.utils.ByteArray; import sk.yoz.Log; import sk.yoz.events.P2PMulticastPersistenceEvent; public class P2PMulticastPersistence extends EventDispatcher implements IP2PMulticastPersistence { private var signSalt:String = "StratusIsCoool"; private var loader:URLLoader = new URLLoader(); public var scriptURL:String = ""; // http://mydomain/script.php? or ... // http://mydomain/script.php?id=123 // "&sign={HASH}" is added public var debug:Boolean = false; private var log:Log = Log.instance; public function P2PMulticastPersistence(scriptURL:String, signSalt:String = "") { super(); this.scriptURL = scriptURL; this.signSalt = signSalt; loader.addEventListener(Event.COMPLETE, loaderComplete); loader.addEventListener(Event.OPEN, openHandler); loader.addEventListener(ProgressEvent.PROGRESS, progressHandler); loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler); loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler); } public function saveClientList(list:Array):void { var data:String = listToMessage(list); var sign:String = signData(data); var request:URLRequest = new URLRequest(scriptURL + "&sign=" + sign); var saver:URLLoader = new URLLoader(); request.contentType = "text/xml"; request.data = data; request.method = URLRequestMethod.POST; saver.load(request); } public function getClientList():void { var request:URLRequest = new URLRequest(scriptURL); request.method = URLRequestMethod.GET; try { loader.load(request); } catch (error:Error) { if(debug) log.write("P2PMulticastPersistence.getClientList() load() Error"); } } private function loaderComplete(event:Event):void { var list:Array = messageToList(loader.data); dispatchEvent(new P2PMulticastPersistenceEvent(P2PMulticastPersistenceEvent.CLIENTS_DEFINED, true, true, {list:list})); } private function messageToList(message:String):Array { var xml:XML = XML(message); var list:Array = []; for each(var id:String in xml.client.@['id']) list.push(id); return list; } private function listToMessage(list:Array):String { var xml:XML = <clients /> var xmlNode:XML; for each(var id:String in list) { xmlNode = <client />; xmlNode.@id = id; xml.appendChild(xmlNode); } return xml.toXMLString(); } private function openHandler(event:Event):void { if(debug) log.write("P2PMulticastPersistence.openHandler(" + event.toString() + ")"); } private function progressHandler(event:ProgressEvent):void { if(debug) log.write("P2PMulticastPersistence.progressHandler() " + "// loaded:" + event.bytesLoaded + " total: " + event.bytesTotal); } private function securityErrorHandler(event:SecurityErrorEvent):void { if(debug) log.write("P2PMulticastPersistence.securityErrorHandler(" + event.toString() + ")"); } private function httpStatusHandler(event:HTTPStatusEvent):void { if(debug) log.write("P2PMulticastPersistence.httpStatusHandler(" + event.toString() + ")"); } private function ioErrorHandler(event:IOErrorEvent):void { if(debug) log.write("P2PMulticastPersistence.ioErrorHandler(" + event.toString() + ")"); } private function signData(data:String):String { return MD5.hash(signSalt + MD5.hash(data)); } } }
sk.yoz.net.P2PMulticastStream.as class provides stratus handshake (assigned ID) and directly communicates via NetStream
package sk.yoz.net</p> { import flash.events.AsyncErrorEvent; import flash.events.EventDispatcher; import flash.events.IOErrorEvent; import flash.events.NetStatusEvent; import flash.events.SecurityErrorEvent; import flash.events.TimerEvent; import flash.net.NetConnection; import flash.net.NetStream; import sk.yoz.utils.DataTimer; import sk.yoz.Log; import sk.yoz.events.P2PMulticastStreamEvent; public class P2PMulticastStream extends EventDispatcher { private var publishName:String = "com"; private var maxPeerConnections:uint = 100; private var handshakeURL:String = "rtmfp://stratus.adobe.com"; // eg: rtmfp://stratus.adobe.com private var developerKey:String = ""; private var clientConnectionTimeout:uint = 15000; private var stratus:NetConnection; private var publish:NetStream; private var clients:Object = {}; public var debug:Boolean = false; private var log:Log = Log.instance; public function P2PMulticastStream(developerKey:String = "", publishName:String = "", maxPeerConnections:uint = 0, handshakeURL:String = "", clientConnectionTimeout:uint = 0) { super(); if(publishName) this.publishName = publishName; if(maxPeerConnections) this.maxPeerConnections = maxPeerConnections; if(handshakeURL) this.handshakeURL = handshakeURL; if(developerKey) this.developerKey = developerKey; if(clientConnectionTimeout) this.clientConnectionTimeout = clientConnectionTimeout; stratus = new NetConnection(); stratus.maxPeerConnections = this.maxPeerConnections; stratus.addEventListener(NetStatusEvent.NET_STATUS, stratusStatusHandler); stratus.addEventListener(AsyncErrorEvent.ASYNC_ERROR, stratusAsyncErrorHandler); stratus.addEventListener(IOErrorEvent.IO_ERROR, stratusIOErrorHandler); stratus.addEventListener(SecurityErrorEvent.SECURITY_ERROR, stratusSecurityErrorHandler); stratus.connect(this.handshakeURL + "/" + this.developerKey + "/"); } public function get connected():Boolean { return nearID ? true : false; } public function get onlineClients():Array { var list:Array = []; for(var farID:String in clients) if(client(farID).connected) list.push(farID); return list; } public function get allConnected():Boolean { for(var farID:String in clients) if(!client(farID).connected) return false; return true; } private function initPublish():void { if(debug) log.write("P2PMulticastStream.initPublish()"); publish = new NetStream(stratus, NetStream.DIRECT_CONNECTIONS); publish.addEventListener(NetStatusEvent.NET_STATUS, publishStatusHandler); publish.addEventListener(AsyncErrorEvent.ASYNC_ERROR, publishAsyncErrorHandler); publish.addEventListener(IOErrorEvent.IO_ERROR, publishIOErrorHandler); publish.publish(publishName); publish.client = { onPeerConnect: function(caller:NetStream):Boolean { connectClient(caller.farID); return true; } }; } public function get nearID():String { return stratus && stratus.connected ? stratus.nearID : ''; } private function client(farID:String):StratusStream { return clients.hasOwnProperty(farID) ? clients[farID] : null; } public function connectClient(farID:String):void { if(farID == nearID || clients.hasOwnProperty(farID)) return; if(debug) log.write("P2PMulticastStream.connectClient(" + farID + ")"); var c:StratusStream = new StratusStream(stratus, farID); c.addEventListener(NetStatusEvent.NET_STATUS, clientStatusHandler); c.play(publishName); c.client = { onData: function(data:Object):void { if(debug) log.write("P2PMulticastStream.client(" + farID + ").onData(" + data.toString() + ")"); dispatchEvent(new P2PMulticastStreamEvent(P2PMulticastStreamEvent.PEER_DATA, false, false, {farID:farID, data:data})); } }; clients[farID] = c; var timer:DataTimer = new DataTimer(clientConnectionTimeout, 1, {farID:farID}); timer.addEventListener(TimerEvent.TIMER_COMPLETE, clientConnectionTimeoutHandler); timer.start(); } private function clientConnectionTimeoutHandler(event:TimerEvent):void { var timer:DataTimer = event.target as DataTimer; var farID:String = timer.data.farID; if(client(farID) && !client(farID).connected) disconnectClient(farID); timer.removeEventListener(TimerEvent.TIMER_COMPLETE, clientConnectionTimeoutHandler); timer = null; } private function disconnectClient(farID:String):void { if(farID == nearID || !client(farID)) return; clients[farID] = null; delete clients[farID]; if(debug) log.write("P2PMulticastStream.disconnectClient(" + farID + ")"); dispatchEvent(new P2PMulticastStreamEvent(P2PMulticastStreamEvent.PEER_DISCONNECTED, true, true, {farID:farID})); } public function broadcast(data:Object):Boolean { if(!connected) return false; try { publish.send("onData", data); } catch(error:Error) { if(debug) log.write("P2PMulticastStream.sendData(" + data.toString() + ") // ERROR: " + error.message); return false; } return true; } private function clientStatusHandler(event:NetStatusEvent):void { var farID:String = event.target.farID; if(debug) log.write("P2PMulticastStream.clientStatusHandler(" + event.info.code + ")"); switch(event.info.code) { case "NetStream.Play.Start": if(debug) log.write("P2PMulticastStream.clientStatusHandler(" + event.info.code + ") " + "// client(" + farID + ") connected"); client(farID).connected = true; dispatchEvent(new P2PMulticastStreamEvent(P2PMulticastStreamEvent.PEER_CONNECTED, true, true, {farID:farID})); break; } } private function stratusStatusHandler(event:NetStatusEvent):void { if(debug) log.write("P2PMulticastStream.stratusStatusHandler(" + event.info.code + ")"); switch(event.info.code) { case "NetConnection.Connect.Success": if(debug) log.write("P2PMulticastStream.stratusStatusHandler(" + event.info.code + ") " + "// p2p connection ready (" + nearID + ")"); initPublish(); dispatchEvent(new P2PMulticastStreamEvent(P2PMulticastStreamEvent.CLIENT_ID_ASSIGNED, true, true, {nearID:nearID})); break; case "NetStream.Connect.Closed": if(debug) log.write("P2PMulticastStream.stratusStatusHandler(" + event.info.code + ") " + "// p2p disconnected (" + event.info.stream.farID + ")"); disconnectClient(event.info.stream.farID); break; } } private function stratusAsyncErrorHandler(event:AsyncErrorEvent):void { if(debug) log.write("P2PMulticastStream.stratusAsyncErrorHandler(" + event.text + ")"); } private function stratusIOErrorHandler(event:IOErrorEvent):void { if(debug) log.write("P2PMulticastStream.stratusIOErrorHandler(" + event.text + ")"); } private function stratusSecurityErrorHandler(event:SecurityErrorEvent):void { if(debug) log.write("P2PMulticastStream.stratusSecurityErrorHandler(" + event.text + ")"); } private function publishStatusHandler(event:NetStatusEvent):void { if(debug) log.write("P2PMulticastStream.publishStatusHandler(" + event.info.code + ")"); switch(event.info.code) { case "NetStream.Publish.Start": if(debug) log.write("P2PMulticastStream.publishStatusHandler(" + event.info.code + ") " + "// publishing started (" + nearID + ")"); break; } } private function publishAsyncErrorHandler(event:AsyncErrorEvent):void { if(debug) log.write("P2PMulticastStream.publishAsyncErrorHandler(" + event.text + ")"); } private function publishIOErrorHandler(event:IOErrorEvent):void { if(debug) log.write("P2PMulticastStream.publishIOErrorHandler(" + event.text + ")"); } public function getClientList():Array { var list:Array = []; for(var farID:String in clients) list.push(farID); return list; } } }
Application.mxml is demo application, export it and open in 2 (or more) different windows. When connected (online text appear), you can communicate to other instances via p2p. Do not forget to insert your stratus developer key:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" applicationComplete="init()" viewSourceURL="srcview/index.html"> <mx:Script> <![CDATA[ import sk.yoz.events.P2PMulticastEvent; import sk.yoz.net.P2PMulticastPersistence; import sk.yoz.net.P2PMulticast; import sk.yoz.Log; private var log:Log = Log.instance; private var p2p:P2PMulticast; private function init():void { log.output = logArea; var scriptURL:String = "http://localhost/p2p/clients.php?"; var signSalt:String = "StratusIsCoool"; var developerKey:String = "***"; // HERE COMES YOUR STRATUS DEVELOPER KEY var persistence:P2PMulticastPersistence = new P2PMulticastPersistence(scriptURL, signSalt); p2p = new P2PMulticast(persistence, developerKey); p2p.debug = true; persistence.debug = true; p2p.addEventListener(P2PMulticastEvent.STREAM_CONNECTED, streamConnectedHandler); p2p.addEventListener(P2PMulticastEvent.PEER_CONNECTED, peerChangeHandler); p2p.addEventListener(P2PMulticastEvent.PEER_DISCONNECTED, peerChangeHandler); p2p.addEventListener(P2PMulticastEvent.PEER_DATA, peerDataHandler); } private function streamConnectedHandler(event:P2PMulticastEvent):void { info.text = "online"; } private function peerChangeHandler(event:P2PMulticastEvent):void { peers.dataProvider = p2p.onlineClients; } private function peerDataHandler(event:P2PMulticastEvent):void { dataArea.text += event.data.farID + ":" + event.data.data; } private function broadcast():void { p2p.broadcast(message.text); callLater(function():void { message.text = ""; }); } ]]> </mx:Script> <mx:HBox width="100%" height="100%"> <mx:VBox width="100%" height="100%"> <mx:TextArea id="logArea" width="100%" height="100%" /> <mx:TextArea id="dataArea" width="100%" height="100%" /> <mx:HBox width="100%"> <mx:TextInput id="message" width="100%" enter="broadcast()"/> <mx:Button click="broadcast()" label="send"/> </mx:HBox> </mx:VBox> <mx:VBox width="400"> <mx:Text id="info" text="offline"/> <mx:List id="peers" width="100%"/> </mx:VBox> </mx:HBox> </mx:Application>
clients.php file reads and writes clients.xml. It uses hash to validate data. Make sure you set sign salt “StratusIsCoool” in .php file same as signSalt in Application.mxml
<?php function signData($data){ return md5("StratusIsCoool".md5($data)); } function defaultize(&$key, $default=null){ return is_null($key)?$key=$default:$key; } $file=dirname(__FILE__)."/clients.xml"; $bytes=defaultize($GLOBALS['HTTP_RAW_POST_DATA'], null); $sign=defaultize($_GET['sign'], null); if($bytes){ if(!$sign || $sign !== signData($bytes)) exit; $fd=fopen($file, 'w'); fwrite($fd, $bytes); fclose($fd); exit; } if(is_file($file)) readfile($file);
Other required files can be found here: P2PMulticast source. Don not forget to export your application into Flash Player 10. P2P Multicast is used in onBoard collaborative painting.