Introducing RedSandy-0.9 - Release version to coincide with concurrent red5 version.
RedSandy project and SVN : http://code.google.com/p/redsandy/
My Blog : http://thebitstream.com
http://www.thebitstream.com/CuePointsPlayer.swf
We're coming close to another Red5 release, and so I thought it a good time to stir the pot. I used the red5 sandy application to record a flv stream.
This stream did not contain any microphone or camera data, but instead contained script data tags. Using..
ns.send("@setDataFrame","onAnimation",{frame:j,sin:Math.sin(j * 2 * Math.PI /180)});
…in one swf, I call that script at 40 or so millisecond intervals while recording a flv file on red5 with the sandy application.
In another swf, I play the flv file and use the script data to link to my primitives.
Each call from the broadcaster triggers the sandy world to render. Fun stuff!
Note:The playback code is not using rtmp.
package { import flash.display.Sprite; import flash.events.NetStatusEvent; import flash.net.NetConnection; import flash.net.NetStream; import flash.net.ObjectEncoding; import sandy.core.Scene3D; import sandy.core.scenegraph.Camera3D; import sandy.core.scenegraph.Group; import sandy.core.scenegraph.TransformGroup; import sandy.materials.Appearance; import sandy.materials.ColorMaterial; import sandy.primitive.Plane3D; import sandy.primitive.Sphere; public class CuePointsPlayer extends Sprite { private var world:Scene3D; private var camera:Camera3D; private var group:Group; var nc:NetConnection=new NetConnection(); var ns:NetStream; public function CuePointsPlayer() { super(); camera = new Camera3D(400,400); group=new Group(); world =new Scene3D('avatar'+Math.random(),this,camera, group); world.camera.near=.01; world.rectClipping=false; camera.z=-300; var b:Sphere=new Sphere('sphereShape',25); var tg:TransformGroup=new TransformGroup("sphere"); tg.addChild(b); var plane:Plane3D=new Plane3D("d"); plane.z=200; world.root.addChild(plane); world.root.addChild(tg); NetConnection.defaultObjectEncoding=ObjectEncoding.AMF0; nc.addEventListener(NetStatusEvent.NET_STATUS,onStatus); nc.objectEncoding=ObjectEncoding.AMF0; nc.client=this; nc.connect(null);//local ns=new NetStream(nc); ns.addEventListener(NetStatusEvent.NET_STATUS,onStatus); ns.client=this; ns.bufferTime=4; ns.play("http://thebitstream.com/CuePoints.flv"); } public function onStatus(e :NetStatusEvent):void { trace(e.info.code); switch(e.info.code) { case "NetConnection.Connect.Success": break; break; } } public function onMetaData(obj:Object):void { } public function onSandy(obj:Object):void { } public function onPlayStatus(obj:Object):void { } public function onAnimation(obj:Object,onj:Object=null):void { TransformGroup(world.root.getChildByName("sphere")).rotateY=obj.frame*5; TransformGroup(world.root.getChildByName("sphere")).y=100*obj.sin; TransformGroup(world.root.getChildByName("sphere")).x=50*obj.sin; camera.x=obj.sin*60*-1; camera.y=obj.sin*60*-1; camera.lookAt(TransformGroup(world.root.getChildByName("sphere")).x,TransformGroup(world.root.getChildByName("sphere")).y,TransformGroup(world.root.getChildByName("sphere")).z) world.render(); trace("onAnimation"); } } }
The flv can be found here, http://www.thebitstream.com/CuePoints.flv
package { import flash.display.Sprite; import flash.events.AsyncErrorEvent; import flash.events.NetStatusEvent; import flash.media.Microphone; import flash.media.Camera; import flash.net.NetConnection; import flash.net.NetStream; import flash.net.ObjectEncoding; import flash.utils.setInterval; import flash.utils.setTimeout; public class CuePointsRecorder extends Sprite { var nc:NetConnection=new NetConnection(); var ns:NetStream; var j:int=0; public function CuePointsRecorder() { NetConnection.defaultObjectEncoding=ObjectEncoding.AMF0; nc.addEventListener(NetStatusEvent.NET_STATUS,onStatus); nc.addEventListener(NetStatusEvent.NET_STATUS,onStatus); nc.client=this; nc.objectEncoding=ObjectEncoding.AMF0; nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onError); nc.connect("rtmp://localhost/sandy"); } public function onError(e :AsyncErrorEvent):void { trace(e.text); } public function onSandy(e :Object):void { } public function onStop():void { ns.close(); } public function onMetaData(e:Object):void { } public function onAnimationTrigger():void { j++; ns.send("@setDataFrame","onAnimation",{frame:j,sin:Math.sin(j * 2 * Math.PI /180)}); trace("setDataFrame!"); } public function onStatus(e :NetStatusEvent):void { trace(e.info.code); switch(e.info.code) { case "NetConnection.Connect.Success": ns=new NetStream(nc); // ns.attachAudio(Microphone.getMicrophone()); // ns.attachCamera(Camera.getCamera()); ns.addEventListener(NetStatusEvent.NET_STATUS,onStatus); ns.client=this; ns.bufferTime=4; ns.publish("CuePoints","record"); break; case "NetStream.Publish.Start": setInterval(onAnimationTrigger,33); setTimeout(onStop,60000); break; } } } }
Alright, its time for an update. Introducing RedSandy 0.9
What we have here folks is a red5 handler and more importantly, a new class SharedTransformGroup. It allows clients to share the position and animation of a transform group using the power of red5. Now, you can either try to set up a 'real time' data model like this first example attempts, or you can set up turn based input.
So, you dream up a way to use it and make your regular sandy world. You will need a client to respond to server and remote-client generated events. This client object will need to implement the IRedSandy interface.
/** * Server initiated call to retrieve this instance's cooridinates. * It is called at the server-granularity setting(Red5 java setting), or as fast as your network and application can respond, whichever is greator. * It is a timely moment to process user input. */ function onInput(obj:SharedTransformGroup):void; /** * Another instance has been created. * @param obj user info * @param group the new user's SharedTransformGroup * */ function onJoined(obj:Object,group:SharedTransformGroup):void /** * This client's instance has been created. * @param obj The SharedTransformGroup * */ function onCreated(obj:SharedTransformGroup):void /** * Another instance has been destroyed. * @param obj The user's info * */ function onLeave(obj:Object):void /** * In response to a user generated 'sendEvent' call. * @param obj the Object sent ny the other client. * */ function onEvent(obj:Object):void
Here is an example swf application which produces a sphere for each connected user and allows slight, ever so slight, navigation of the spheres.
package { import flash.display.Sprite; import flash.events.Event; import flash.events.KeyboardEvent; import flash.events.NetStatusEvent; import flash.ui.Keyboard; import flash.utils.setTimeout; import sandy.core.Scene3D; import sandy.core.scenegraph.Camera3D; import sandy.core.scenegraph.Group; import sandy.primitive.Sphere; import sandy.redSandy.IRedSandy; import sandy.redSandy.RedSandy; import sandy.redSandy.SharedTransformGroup; import sandy.redSandy.Connection; [SWF(width="400",height="400")] /** * * @author Andy Shaules * */ public class Redsandy extends Sprite implements IRedSandy { private var c:RedSandy; private var world:Scene3D; private var camera:Camera3D; private var group:Group; private var keys:Object=new Object(); public var clientData:Object=new Object (); public function Redsandy() { //crank it up stage.frameRate=40; // make the connection. c=new RedSandy(); c.client=this; c.netConnectionEvents.addEventListener(NetStatusEvent.NET_STATUS,onStatExample); c.connect("rtmp://localhost:1935/sandy/room","hello","sandy"); // make the sandy world camera = new Camera3D(400,400); group=new Group(); world =new Scene3D('avatar'+Math.random(),this,camera, group); world.camera.near=.01; world.rectClipping=false; camera.z=-300; // add the example application funcitons stage.addEventListener(KeyboardEvent.KEY_DOWN,onKey); stage.addEventListener(KeyboardEvent.KEY_UP,onKey); addEventListener(Event.ENTER_FRAME,onFrame); } /** * Server initiated call to retrieve this instance's cooridinates. * It is called at the server-granularity setting, or as fast as your network and application can respond, whichever is greator. * It is a timely moment to process user input. */ public function onInput(myPosition:SharedTransformGroup):void { if(isDown(Keyboard.UP)) { myPosition.m_nVelocityX=myPosition.m_nVelocityX-1; } else if(isDown(Keyboard.DOWN)) { myPosition.m_nVelocityX=myPosition.m_nVelocityX+1; } if(isDown(Keyboard.LEFT)) { myPosition.m_nRotateVelocityY=myPosition.m_nRotateVelocityY-1; } if(isDown(Keyboard.RIGHT)) { myPosition.m_nRotateVelocityY=myPosition.m_nRotateVelocityY+1; } } /** * Called from another user's sendEvent function call. * @param obj * */ public function onEvent(obj:Object):void { trace(obj.value) } /** * Called when user joins the room. */ public function onJoined(obj:Object,group:SharedTransformGroup):void { trace("joined by "+obj.id); var b:Sphere=new Sphere('sphere'+obj.id,25); group.addChild(b); world.root.addChild(group); } /** * Called when user leaves the room. */ public function onLeave(obj:Object):void { world.root.removeChildByName(obj.id); } /** * Here is this instance's SharedTransformGroup * */ public function onCreated(obj:SharedTransformGroup):void { clientData.id=obj.name; var b:Sphere=new Sphere('sphere',25); obj.addChild(b); world.root.addChild(obj); } /** * If using the velocity properties of the SharedTransformGroup, * you must call 'SharedTransformGroup.initiateRender()'. * Test App implementation * @param e * */ private function onFrame(e:Event):void { SharedTransformGroup.initiateRender(); world.render(); } /** * Test App implementation * @param ns * */ private function onStatExample(ns:NetStatusEvent):void { trace(ns.info.code); if(ns.info.code=='NetConnection.Connect.Success') { // Run some tests. // set a single property setTimeout(setPropertyExample,4000); // retrieve it setTimeout(getPropertyExample,5000); // get another user property (in this test case, our own.) setTimeout(getUserPropertyExample,6000); // get all my properties setTimeout(getAllPropertiesExample,7000); // Send to other clients. This instance will not get this event. setTimeout(sendEventExample,8000); } } /** * Test App implementation * */ public function setPropertyExample():void { c.setProperty("test","success",onResult); function onResult(obj:Object):void { trace(obj); } } /** * Test App implementation * */ public function getPropertyExample():void { c.getProperty("test",onResult); function onResult(obj:Object):void { trace(obj); clientData["test"]=obj; } } /** * Test App implementation * */ public function getAllPropertiesExample():void { c.getAllProperties(onResult2); function onResult2(obj2:Object):void { clientData=obj2; trace(obj2.test); } } /** * Test App implementation * */ public function getUserPropertyExample():void { c.getUserProperty(clientData.id,"test",onResult); function onResult(obj:Object):void { trace(obj); } } /** * Test App implementation * */ public function sendEventExample():void { c.sendEvent({type:"event.test", value:"success"}); } /** * Test App implementation * @param ke * */ private function onKey(ke:KeyboardEvent):void { switch(ke.type) { case KeyboardEvent.KEY_UP: keys[ke.keyCode.toString()]="up"; break; case KeyboardEvent.KEY_DOWN: keys[ke.keyCode.toString()]="down"; break; } } /** * Test App implementation * @param val * @return * */ public function isDown(val:uint):Boolean { if(keys[val]=="down") { return true; } return false; } } }