From 31166002c6b877ca9ebf7e76bf18e4ab181de8f4 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 4 Mar 2013 22:02:41 -0800 Subject: [PATCH] Tweaked click animation Fixed: Bug where you can hear users after leaving conversation --- com/sqwiggle/Conversations.as | 2 +- com/sqwiggle/Member.as | 2 +- com/sqwiggle/Self.as | 2 +- p2p.fla | Bin 574367 -> 577917 bytes 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/com/sqwiggle/Conversations.as b/com/sqwiggle/Conversations.as index 23cc378..c309241 100644 --- a/com/sqwiggle/Conversations.as +++ b/com/sqwiggle/Conversations.as @@ -1 +1 @@ -package com.sqwiggle { import com.sqwiggle.Conversation; import com.sqwiggle.Self; import com.sqwiggle.events.ApiEvent; import com.sqwiggle.events.ConversationEvent; import flash.events.Event; import flash.net.URLLoader; import flash.events.EventDispatcher; public class Conversations extends EventDispatcher { private var ongoing:Object; private var colours:Array; private var self:Self; public var members:Object; public function Conversations(self:Self, members:Object) { this.ongoing = new Object(); this.members = members; this.self = self; // make an API request to get ongoing conversations var request = Api.getRequest('/conversations'); request.addEventListener(Event.COMPLETE, function(event:ApiEvent) { Sqwiggle.trace('Conversations', JSON.stringify(event.data)); updateMembers(event.data); }); } public function updateMembers(conversations:Array):void { // first find if the current user is in any conversation var selfInConversation = getSelfInConversations(conversations); var previouslyInConversation = clearOldConversations(conversations); // then for each conversation that is occuring, ensure the // correct members are present in the conversation (we may // need to remove members that have left) for (var i in conversations) { var key = 'convo' + i; // we dont know about this conversation if (!ongoing[key]) { Sqwiggle.trace('creating new conversation'); ongoing[key] = new Conversation(conversations[i].id, conversations[i].color_id); } var conversation = ongoing[key]; var existingMembers = conversation.members; var selfInCurrentConversation = selfInConversation && (selfInConversation.id == conversation.id); var member; if (selfInCurrentConversation) { Sqwiggle.trace('Were in this conversation'); } else { Sqwiggle.trace('Were not in this conversation'); } // find existing members that should leave the convo for (var j in existingMembers) { // check if this user is still in the convo var filter = conversations[i].users.filter(function(user){ return user.id == this.userId; }, existingMembers[j]); var inConversation = !!filter.length; member = existingMembers[j]; if (!inConversation) { Sqwiggle.trace('Member has left a conversation', member.userId, conversation.id); conversation.removeMember(member, selfInCurrentConversation); } } // for every user that should be in this conversation Sqwiggle.trace('processing users'); for (var k in conversations[i].users) { var user = conversations[i].users[k]; Sqwiggle.trace('Looking at user', user.id); if (user.id == self.userId) { member = self; } else { member = members[user.id]; } if (!member) { Sqwiggle.trace('Couldnt find member for user', user.id); } else { conversation.addMember(member, selfInCurrentConversation); } if (member && !conversation.hasMember(member)) { if (selfInCurrentConversation) { Sqwiggle.trace('adding member to your conversation', member.userId); } else { Sqwiggle.trace('adding member to conversation', member.userId, conversation.id); } conversation.addMember(member, selfInCurrentConversation); } } } if (!previouslyInConversation && selfInConversation) { conversation = getConversationForMember(self); dispatchEvent(new ConversationEvent("conversationstarted", conversation.getMembersAsList())); } else if(previouslyInConversation && !selfInConversation) { dispatchEvent(new ConversationEvent("conversationended", null)); } dispatchEvent(new Event("conversationsupdated")); } private function getSelfInConversations(conversations:Array) { var selfInConversation = false; // search conversations in update for users presence for (var a in conversations) { for (var b in conversations[a].users) { if (conversations[a].users[b].id == self.userId) { selfInConversation = conversations[a]; Sqwiggle.trace('We are in a conversation, lucky us'); } } } return selfInConversation; } private function clearOldConversations(conversations:Array):Boolean { var previouslyInConversation = false; // search ongoing conversations for those that no longer // exist in the update. These need to be tidied up and removed for (var c in ongoing) { if (ongoing[c].hasMember(self)) { previouslyInConversation = true; } var noLongerExists = true; for (var d in conversations) { if (conversations[d].id == ongoing[c].id) { noLongerExists = false; break; } } if (noLongerExists) { Sqwiggle.trace('Removing conversation from our cache that no longer exists', c); ongoing[c].removeAllMembers(); delete ongoing[c]; } } return previouslyInConversation; } public function getConversationForMember(member:Member):Conversation { for (var i in ongoing) { if (ongoing[i].hasMember(member)) { return ongoing[i]; } } return null; } } } \ No newline at end of file +package com.sqwiggle { import com.sqwiggle.Conversation; import com.sqwiggle.Self; import com.sqwiggle.events.ApiEvent; import com.sqwiggle.events.ConversationEvent; import flash.events.Event; import flash.net.URLLoader; import flash.events.EventDispatcher; public class Conversations extends EventDispatcher { private var ongoing:Object; private var colours:Array; private var self:Self; public var members:Object; public function Conversations(self:Self, members:Object) { this.ongoing = new Object(); this.members = members; this.self = self; // make an API request to get ongoing conversations var request = Api.getRequest('/conversations'); request.addEventListener(Event.COMPLETE, function(event:ApiEvent) { Sqwiggle.trace('Conversations', JSON.stringify(event.data)); updateMembers(event.data); }); } public function updateMembers(conversations:Array):void { // first find if the current user is in any conversation var selfInConversation = getSelfInConversations(conversations); var previouslyInConversation = clearOldConversations(conversations); // then for each conversation that is occuring, ensure the // correct members are present in the conversation (we may // need to remove members that have left) for (var i in conversations) { var key = 'convo' + i; // we dont know about this conversation if (!ongoing[key]) { Sqwiggle.trace('creating new conversation'); ongoing[key] = new Conversation(conversations[i].id, conversations[i].color_id); } var conversation = ongoing[key]; var existingMembers = conversation.members; var selfInCurrentConversation = selfInConversation && (selfInConversation.id == conversation.id); var member; if (selfInCurrentConversation) { Sqwiggle.trace('Were in this conversation'); } else { Sqwiggle.trace('Were not in this conversation'); } // find existing members that should leave the convo for (var j in existingMembers) { // check if this user is still in the convo var filter = conversations[i].users.filter(function(user){ return user.id == this.userId; }, existingMembers[j]); var inConversation = !!filter.length; member = existingMembers[j]; if (!inConversation) { Sqwiggle.trace('Member has left a conversation', member.userId, conversation.id); conversation.removeMember(member, selfInCurrentConversation); } } // for every user that should be in this conversation Sqwiggle.trace('processing users'); for (var k in conversations[i].users) { var user = conversations[i].users[k]; Sqwiggle.trace('Looking at user', user.id); if (user.id == self.userId) { member = self; } else { member = members[user.id]; } if (!member) { Sqwiggle.trace('Couldnt find member for user', user.id); } else { conversation.addMember(member, selfInCurrentConversation); } if (member && !conversation.hasMember(member)) { if (selfInCurrentConversation) { Sqwiggle.trace('adding member to your conversation', member.userId); } else { Sqwiggle.trace('adding member to conversation', member.userId, conversation.id); } conversation.addMember(member, selfInCurrentConversation); } if (selfInCurrentConversation) { member.audioOn(); } else { member.audioOff(); } } } if (!previouslyInConversation && selfInConversation) { conversation = getConversationForMember(self); dispatchEvent(new ConversationEvent("conversationstarted", conversation.getMembersAsList())); } else if(previouslyInConversation && !selfInConversation) { dispatchEvent(new ConversationEvent("conversationended", null)); } dispatchEvent(new Event("conversationsupdated")); } private function getSelfInConversations(conversations:Array) { var selfInConversation = false; // search conversations in update for users presence for (var a in conversations) { for (var b in conversations[a].users) { if (conversations[a].users[b].id == self.userId) { selfInConversation = conversations[a]; Sqwiggle.trace('We are in a conversation, lucky us'); } } } return selfInConversation; } private function clearOldConversations(conversations:Array):Boolean { var previouslyInConversation = false; // search ongoing conversations for those that no longer // exist in the update. These need to be tidied up and removed for (var c in ongoing) { if (ongoing[c].hasMember(self)) { previouslyInConversation = true; } var noLongerExists = true; for (var d in conversations) { if (conversations[d].id == ongoing[c].id) { noLongerExists = false; break; } } if (noLongerExists) { Sqwiggle.trace('Removing conversation from our cache that no longer exists', c); ongoing[c].removeAllMembers(); delete ongoing[c]; } } return previouslyInConversation; } public function getConversationForMember(member:Member):Conversation { for (var i in ongoing) { if (ongoing[i].hasMember(member)) { return ongoing[i]; } } return null; } } } \ No newline at end of file diff --git a/com/sqwiggle/Member.as b/com/sqwiggle/Member.as index 8ccf3b1..8185a4a 100644 --- a/com/sqwiggle/Member.as +++ b/com/sqwiggle/Member.as @@ -1 +1 @@ -package com.sqwiggle { import flash.display.Sprite; import flash.display.Shape; import flash.display.MovieClip; import flash.display.GradientType; import flash.media.Video; import flash.net.NetConnection; import flash.net.NetStream; import flash.net.NetStreamInfo; import flash.events.Event; import flash.events.MouseEvent; import flash.events.NetStatusEvent; import flash.events.TimerEvent; import flash.text.TextField; import flash.text.TextFormat; import fl.transitions.Tween; import fl.transitions.easing.*; import flash.utils.Timer; import flash.ui.Mouse; import flash.ui.MouseCursor; import flash.filters.*; import flash.geom.Matrix; public class Member extends Sprite { public static const MAX_WIDTH = 400; public static const MAX_HEIGHT = 300; public static const INFOBAR_HEIGHT = 35; public var connection :NetConnection; public var userId :String; public var peerId :String; public var userName :String; protected var stream :NetStream; protected var video :Video; protected var infoBar :Sprite; protected var activity :MovieClip; protected var timeField :TextField; protected var nameField :TextField; protected var inConversation:Boolean = false; public function Member(userId:String, peerId:String, userName:String='Anonymous') { this.userId = userId; this.peerId = peerId; this.userName = userName; render(300, 225); addEventListener(MouseEvent.ROLL_OVER, hoverOver); addEventListener(MouseEvent.ROLL_OUT, hoverOut); addEventListener(MouseEvent.MOUSE_DOWN, clicked); addEventListener(Event.ENTER_FRAME, updateActivity); var timer = new Timer(1000); timer.addEventListener(TimerEvent.TIMER, updateTime); timer.start(); } public function render(w:Number, h:Number):void { w = Math.min(Member.MAX_WIDTH, w); h = Math.min(Member.MAX_HEIGHT, h); if (!video) { video = new Video(); addChild(video); } video.width = w; video.height = h; video.x = -w/2; video.y = -h/2; drawInfoBar(); if (!activity) { activity = new ActivityIndicator(); activity.visible = false; addChild(activity); } activity.x = -w/2; activity.y = h/2; } public function hideVideo():void { Sqwiggle.trace('Pausing video for', userId); stream.receiveVideo(false); } public function showVideo():void { stream.receiveVideo(true); } private function drawInfoBar():void { if (infoBar) removeChild(infoBar); infoBar = new Sprite(); infoBar.y = (video.height/2)-Member.INFOBAR_HEIGHT; infoBar.x = -(video.width/2); infoBar.alpha = 0; addChild(infoBar); var opaqueBar = new Sprite(); var gradMatrix = new Matrix(); gradMatrix.createGradientBox(video.width, Member.INFOBAR_HEIGHT, -1.57079633); // -90degrees in radians opaqueBar.graphics.beginGradientFill(GradientType.LINEAR, [0x000000,0x000000], [1, 0], [0,255], gradMatrix); opaqueBar.graphics.drawRect(0,0,video.width,Member.INFOBAR_HEIGHT); opaqueBar.graphics.endFill(); infoBar.addChild(opaqueBar); var textFormat:TextFormat = new TextFormat(); textFormat.font = "Arial"; textFormat.size = 16; nameField = new TextField(); nameField.x = 10; nameField.y = 6; // padding nameField.defaultTextFormat = textFormat; nameField.width = video.width-50; nameField.height = infoBar.height; nameField.selectable = false; nameField.textColor = 0xFFFFFF; nameField.text = userName; nameField.alpha = 0.8; infoBar.addChild(nameField); var timeTextFormat:TextFormat = new TextFormat(); timeTextFormat.font = "Arial"; timeTextFormat.size = 16; timeTextFormat.align = "right"; timeField = new TextField(); timeField.x = video.width-85; timeField.y = 6; // padding timeField.defaultTextFormat = timeTextFormat; timeField.width = 80; timeField.height = infoBar.height; timeField.selectable = false; timeField.textColor = 0xFFFFFF; timeField.text = Utils.getFormattedTime(); timeField.alpha = 0.8; infoBar.addChild(timeField); } public function connectToVideo(connection:NetConnection):void { Sqwiggle.trace('Connecting to video of: ' + userId + ', stream: ' + peerId); stream = new NetStream(connection, peerId); stream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); stream.receiveAudio(false); stream.play(userId); video.attachNetStream(stream); } public function disconnectFromVideo():void { removeChild(video); stream.close(); } protected function updateActivity(event:Event):void { /* var bytes:Number = stream.info.audioBytesPerSecond; if (bytes) { var maxActivityLevel = 5500; var activityLevel = (Math.max(0, bytes-1000)/100) / (maxActivityLevel/100) activity.alpha = 0.5 + (activityLevel/2); } */ } protected function updateTime(event:Event):void { timeField.text = Utils.getFormattedTime(); } public function addToConversation(conversation:Conversation):void { stream.receiveAudio(true); activity.gotoAndStop(conversation.color_id); activity.visible = true; nameField.x = 40; inConversation = true; } public function removeFromConversation(conversation:Conversation):void { stream.receiveAudio(false); activity.visible = false; nameField.x = 10; inConversation = false; } public function onNetStatus(event:NetStatusEvent):void { Sqwiggle.trace('Net status ' + peerId + ': ' + event.info.code); } private function hoverOver(event:MouseEvent):void { parent.setChildIndex(this, parent.numChildren-1); Mouse.cursor = MouseCursor.BUTTON; new Tween(infoBar, "alpha", Regular.easeIn, 0, 1, 0.3, true); new Tween(this, "scaleX", Regular.easeOut, 1, 1.02, 0.2, true); new Tween(this, "scaleY", Regular.easeOut, 1, 1.02, 0.2, true); } private function hoverOut(event:MouseEvent):void { Mouse.cursor = MouseCursor.AUTO; new Tween(infoBar, "alpha", Regular.easeIn, 1, 0, 0.3, true); new Tween(this, "scaleX", Regular.easeOut, 1.02, 1, 0.2, true); new Tween(this, "scaleY", Regular.easeOut, 1.02, 1, 0.2, true); } private function clicked(event:MouseEvent):void { this.scaleX = 0.8; this.scaleY = 0.8; new Tween(this, "scaleX", Regular.easeOut, 0.9, 1, 0.3, true); new Tween(this, "scaleY", Regular.easeOut, 0.9, 1, 0.3, true); } } } \ No newline at end of file +package com.sqwiggle { import flash.display.Sprite; import flash.display.Shape; import flash.display.MovieClip; import flash.display.GradientType; import flash.media.Video; import flash.net.NetConnection; import flash.net.NetStream; import flash.net.NetStreamInfo; import flash.events.Event; import flash.events.MouseEvent; import flash.events.NetStatusEvent; import flash.events.TimerEvent; import flash.text.TextField; import flash.text.TextFormat; import fl.transitions.Tween; import fl.transitions.easing.*; import flash.utils.Timer; import flash.ui.Mouse; import flash.ui.MouseCursor; import flash.filters.*; import flash.geom.Matrix; public class Member extends Sprite { public static const MAX_WIDTH = 400; public static const MAX_HEIGHT = 300; public static const INFOBAR_HEIGHT = 35; public var connection :NetConnection; public var userId :String; public var peerId :String; public var userName :String; protected var stream :NetStream; protected var video :Video; protected var infoBar :Sprite; protected var micSymbol :Sprite; protected var activity :MovieClip; protected var timeField :TextField; protected var nameField :TextField; protected var inConversation:Boolean = false; public function Member(userId:String, peerId:String, userName:String='Anonymous') { this.userId = userId; this.peerId = peerId; this.userName = userName; render(300, 225); addEventListener(MouseEvent.ROLL_OVER, hoverOver); addEventListener(MouseEvent.ROLL_OUT, hoverOut); addEventListener(MouseEvent.MOUSE_DOWN, clicked); addEventListener(Event.ENTER_FRAME, updateActivity); var timer = new Timer(1000); timer.addEventListener(TimerEvent.TIMER, updateTime); timer.start(); } public function render(w:Number, h:Number):void { w = Math.min(Member.MAX_WIDTH, w); h = Math.min(Member.MAX_HEIGHT, h); if (!video) { video = new Video(); addChild(video); } video.width = w; video.height = h; video.x = -w/2; video.y = -h/2; drawInfoBar(); drawMicSymbol(); if (!activity) { activity = new ActivityIndicator(); activity.visible = false; addChild(activity); } activity.x = -w/2; activity.y = h/2; } public function hideVideo():void { Sqwiggle.trace('Pausing video for', userId); stream.receiveVideo(false); } public function showVideo():void { stream.receiveVideo(true); } private function drawMicSymbol():void { micSymbol = new MicrophoneSymbol(); micSymbol.alpha = 0; } private function drawInfoBar():void { if (infoBar) removeChild(infoBar); infoBar = new Sprite(); infoBar.y = (video.height/2)-Member.INFOBAR_HEIGHT; infoBar.x = -(video.width/2); infoBar.alpha = 0; addChild(infoBar); var opaqueBar = new Sprite(); var gradMatrix = new Matrix(); gradMatrix.createGradientBox(video.width, Member.INFOBAR_HEIGHT, -1.57079633); // -90degrees in radians opaqueBar.graphics.beginGradientFill(GradientType.LINEAR, [0x000000,0x000000], [1, 0], [0,255], gradMatrix); opaqueBar.graphics.drawRect(0,0,video.width,Member.INFOBAR_HEIGHT); opaqueBar.graphics.endFill(); infoBar.addChild(opaqueBar); var textFormat:TextFormat = new TextFormat(); textFormat.font = "Arial"; textFormat.size = 16; nameField = new TextField(); nameField.x = 10; nameField.y = 6; // padding nameField.defaultTextFormat = textFormat; nameField.width = video.width-50; nameField.height = infoBar.height; nameField.selectable = false; nameField.textColor = 0xFFFFFF; nameField.text = userName; nameField.alpha = 0.8; infoBar.addChild(nameField); var timeTextFormat:TextFormat = new TextFormat(); timeTextFormat.font = "Arial"; timeTextFormat.size = 16; timeTextFormat.align = "right"; timeField = new TextField(); timeField.x = video.width-85; timeField.y = 6; // padding timeField.defaultTextFormat = timeTextFormat; timeField.width = 80; timeField.height = infoBar.height; timeField.selectable = false; timeField.textColor = 0xFFFFFF; timeField.text = Utils.getFormattedTime(); timeField.alpha = 0.8; infoBar.addChild(timeField); } public function connectToVideo(connection:NetConnection):void { Sqwiggle.trace('Connecting to video of: ' + userId + ', stream: ' + peerId); stream = new NetStream(connection, peerId); stream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); stream.receiveAudio(false); stream.play(userId); video.attachNetStream(stream); } public function disconnectFromVideo():void { removeChild(video); stream.close(); } protected function updateActivity(event:Event):void { /* var bytes:Number = stream.info.audioBytesPerSecond; if (bytes) { var maxActivityLevel = 5500; var activityLevel = (Math.max(0, bytes-1000)/100) / (maxActivityLevel/100) activity.alpha = 0.5 + (activityLevel/2); } */ } protected function updateTime(event:Event):void { timeField.text = Utils.getFormattedTime(); } public function audioOn():void { stream.receiveAudio(true); } public function audioOff():void { stream.receiveAudio(false); } public function addToConversation(conversation:Conversation):void { activity.gotoAndStop(conversation.color_id); activity.visible = true; nameField.x = 40; inConversation = true; } public function removeFromConversation(conversation:Conversation):void { activity.visible = false; nameField.x = 10; inConversation = false; audioOff(); } public function onNetStatus(event:NetStatusEvent):void { Sqwiggle.trace('Net status ' + peerId + ': ' + event.info.code); } private function hoverOver(event:MouseEvent):void { parent.setChildIndex(this, parent.numChildren-1); Mouse.cursor = MouseCursor.BUTTON; new Tween(micSymbol, "alpha", Regular.easeIn, 0, 1, 0.3, true); new Tween(infoBar, "alpha", Regular.easeIn, 0, 1, 0.3, true); new Tween(this, "scaleX", Regular.easeOut, 1, 1.02, 0.2, true); new Tween(this, "scaleY", Regular.easeOut, 1, 1.02, 0.2, true); } private function hoverOut(event:MouseEvent):void { Mouse.cursor = MouseCursor.AUTO; new Tween(micSymbol, "alpha", Regular.easeIn, 1, 0, 0.3, true); new Tween(infoBar, "alpha", Regular.easeIn, 1, 0, 0.3, true); new Tween(this, "scaleX", Regular.easeOut, 1.02, 1, 0.2, true); new Tween(this, "scaleY", Regular.easeOut, 1.02, 1, 0.2, true); } private function clicked(event:MouseEvent):void { this.scaleX = 0.8; this.scaleY = 0.8; new Tween(this, "scaleX", Regular.easeOut, 0.95, 1, 0.3, true); new Tween(this, "scaleY", Regular.easeOut, 0.95, 1, 0.3, true); } } } \ No newline at end of file diff --git a/com/sqwiggle/Self.as b/com/sqwiggle/Self.as index 21daa4d..c8aa7cc 100644 --- a/com/sqwiggle/Self.as +++ b/com/sqwiggle/Self.as @@ -1 +1 @@ -package com.sqwiggle { import flash.events.NetStatusEvent; import flash.media.Camera; import flash.net.NetConnection; import flash.net.NetStream; import flash.media.Microphone; import flash.media.SoundCodec; import flash.events.MouseEvent; import flash.events.Event; import fl.transitions.Tween; import fl.transitions.easing.*; import com.sqwiggle.Member; public class Self extends Member { var cam:Camera; var mic:Microphone; var noc; public function Self(userId:String) { cam = Camera.getCamera(); mic = Microphone.getEnhancedMicrophone(); if (cam) { cam.setQuality(0, 100); cam.setMode(160,120, 4); } if (mic) { mic.enableVAD = true; mic.codec = SoundCodec.SPEEX; mic.encodeQuality = 8; mic.setSilenceLevel(100); mic.setUseEchoSuppression(true); mic.framesPerPacket = 1; } super(userId, '', 'Me'); } public override function render(w:Number, h:Number):void { super.render(w, h); if (!cam) { w = Math.min(Member.MAX_WIDTH, w); h = Math.min(Member.MAX_HEIGHT, h); noc = new NoCameraDetected(); noc.x = w/2; noc.y = h/2; noc.visible = true; addChild(noc); } } public function updatePeerId():void { var options = new Object(); options.peer_id = peerId; // make an API request to save the new peerID var request = Api.putRequest('/users/update', options); // dispatch an event once the peerID is updated request.addEventListener(Event.COMPLETE, function(event:Event) { dispatchEvent(new Event("Connected")); }); } protected override function updateActivity(event:Event):void { if (mic) { activity.alpha = 100 / (50 + (mic.activityLevel/2)); } } public override function connectToVideo(connection:NetConnection):void { peerId = connection.nearID; Sqwiggle.trace('Publishing video to: ' + userId + ', stream: ' + peerId); stream = new NetStream(connection, NetStream.DIRECT_CONNECTIONS); stream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); stream.attachCamera(this.cam); stream.publish(this.userId); var client:Object = new Object(); client.onPeerConnect = function(caller:NetStream):Boolean { Sqwiggle.trace('Member connecting to your video stream: ' + caller.farID); return true; }; stream.client = client; video.attachCamera(this.cam); video.smoothing = true; video.deblocking = 5; // ensure we update this users peerID on the server for // other clients to connect to. updatePeerId(); } public override function addToConversation(conversation:Conversation):void { mic.setSilenceLevel(0); stream.attachAudio(mic); activity.gotoAndStop(conversation.color_id); activity.visible = true; nameField.x = 40; Sqwiggle.trace('Your audio is now on'); } public override function removeFromConversation(conversation:Conversation):void { mic.setSilenceLevel(100); stream.attachAudio(null); activity.visible = false; nameField.x = 10; Sqwiggle.trace('Your audio is now off'); } } } \ No newline at end of file +package com.sqwiggle { import flash.events.NetStatusEvent; import flash.media.Camera; import flash.net.NetConnection; import flash.net.NetStream; import flash.media.Microphone; import flash.media.SoundCodec; import flash.events.MouseEvent; import flash.events.Event; import fl.transitions.Tween; import fl.transitions.easing.*; import com.sqwiggle.Member; public class Self extends Member { var cam:Camera; var mic:Microphone; var noc; public function Self(userId:String) { cam = Camera.getCamera(); mic = Microphone.getEnhancedMicrophone(); if (cam) { cam.setQuality(0, 100); cam.setMode(160,120, 4); } if (mic) { mic.enableVAD = true; mic.codec = SoundCodec.SPEEX; mic.encodeQuality = 8; mic.setSilenceLevel(100); mic.setUseEchoSuppression(true); mic.framesPerPacket = 1; } super(userId, '', 'Me'); } public override function render(w:Number, h:Number):void { super.render(w, h); if (!cam) { w = Math.min(Member.MAX_WIDTH, w); h = Math.min(Member.MAX_HEIGHT, h); noc = new NoCameraDetected(); noc.x = w/2; noc.y = h/2; noc.visible = true; addChild(noc); } } public function updatePeerId():void { var options = new Object(); options.peer_id = peerId; // make an API request to save the new peerID var request = Api.putRequest('/users/update', options); // dispatch an event once the peerID is updated request.addEventListener(Event.COMPLETE, function(event:Event) { dispatchEvent(new Event("Connected")); }); } protected override function updateActivity(event:Event):void { if (mic) { activity.alpha = 100 / (50 + (mic.activityLevel/2)); } } public override function connectToVideo(connection:NetConnection):void { peerId = connection.nearID; Sqwiggle.trace('Publishing video to: ' + userId + ', stream: ' + peerId); stream = new NetStream(connection, NetStream.DIRECT_CONNECTIONS); stream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); stream.attachCamera(this.cam); stream.publish(this.userId); var client:Object = new Object(); client.onPeerConnect = function(caller:NetStream):Boolean { Sqwiggle.trace('Member connecting to your video stream: ' + caller.farID); return true; }; stream.client = client; video.attachCamera(this.cam); video.smoothing = true; video.deblocking = 5; // ensure we update this users peerID on the server for // other clients to connect to. updatePeerId(); } public override function audioOn():void { mic.setSilenceLevel(0); stream.attachAudio(mic); Sqwiggle.trace('Your audio is now on'); } public override function audioOff():void { mic.setSilenceLevel(100); stream.attachAudio(null); Sqwiggle.trace('Your audio is now off'); } } } \ No newline at end of file diff --git a/p2p.fla b/p2p.fla index 070b4742ebdaeef7011861031dbf7b4598b899fa..89472990cbd826454d0d71f0bb0302753ad6a11b 100644 GIT binary patch delta 7930 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 CFsoAl delta 4353 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 Rf46{N1sV(hR|jxa{R;_@3hMv>