�빫�� ����� ���

Archive for the ‘FLEX’ Category.

Gatherer – Online Collaboration tool 만들기 by 13th warrior (5) – 객채공유


다섯번째 강좌

가장 많이 사용하는 white boardPanel.mxml을 먼저 살펴봅시다. whiteboard 아래쪽에 위치한 ToolDock.mxml에서 설정된 객체가 whiteboard의 drag & drop에 의해서 생성되고 생성된 객체는 이동, 크기변환 등의 기능을 수행할 수 있게 됩니다. ToolDock에서 사각박스를 선택하고 whitelboard에 drag & drop을 하면 mouseDown(), mouseMove(), mouseUp() 이벤트가 차례대로 일어나게 됩니다.(이건 이전 강좌 어디선가에서도 설명한 것 같은데요. -_-;)

1. mouseDown()

91
92
93
94
95
96
97
98
99
100
101
102
103
104
			private function mouseDown(event:MouseEvent):void{
				//trace("mouseDown");
				startX = drawArea.mouseX;
				startY = drawArea.mouseY;
 
				//if(buttonState == DRAW || LINE){
				if(toolDock.getButtonState() == DRAW || LINE){				
					drawSurface.graphics.moveTo(startX, startY);					
				}
 
				trace("target :" + event.target);
				drawArea.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);
				drawArea.addEventListener(MouseEvent.MOUSE_UP, mouseUp);				
			}

위치를 설정하고 tooldock으로부터 어떤 객체를 그릴 것인지 선책한 후 MOUSE_MOVE, MOUSE_UP 이벤트핸들러를 설정하죠. 지난번에 설명하였듯이 drag & drop의 처음 부분은 항상 저렇게 간단합니다.

2. mouseMove()

106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
			private function mouseMove(event:MouseEvent):void{
				//trace("MouseMove");				
				var posX:int = drawArea.mouseX;
				var posY:int = drawArea.mouseY;
 
				drawSurface.graphics.lineStyle(0x000000, 2);
 
				//switch(buttonState){
				switch(toolDock.getButtonState()){
					case SELECT:
					break;
					case RECTANGLE:
						drawSurface.graphics.clear();
						drawSurface.graphics.lineStyle(0x000000, 2);
						drawSurface.graphics.drawRect(startX, startY, posX-startX, posY-startY);
					break;
					case CIRCLE:
						drawSurface.graphics.clear();
						drawSurface.graphics.lineStyle(0x000000, 2);
						drawSurface.graphics.drawEllipse(startX, startY, posX-startX, posY-startY);
					break;
					case DRAW:						
						drawSurface.graphics.lineTo(posX, posY);
						linePoints.push({x:posX, y:posY});
					break;
					case TEXT:
						drawSurface.graphics.clear();
						drawSurface.graphics.lineStyle(0x000000, 2);
						drawSurface.graphics.drawRect(startX, startY, posX-startX, posY-startY);
					break;
					case LINE:
						drawSurface.graphics.clear();
						drawSurface.graphics.lineStyle(0x000000, 2);
						drawSurface.graphics.moveTo(startX, startY);
						drawSurface.graphics.lineTo(posX, posY);						
					break;
					case LIGHTPEN:
						drawSurface.graphics.lineStyle(15, 0xfae61e, 0.25);
						drawSurface.graphics.lineTo(posX, posY);
						linePoints.push({x:posX, y:posY});						
					break;
					case MEN:					
						drawSurface.graphics.clear();
						drawSurface.graphics.lineStyle(0x000000, 2);
						drawSurface.graphics.drawRect(startX, startY, posX-startX, posY-startY);							
					break;
				}
			}

쓸데없이 길죠? 설정된 객체에 따라서 화면에 마우스가 이동하는 만큼 생성될 객체의 크기를 보여주기 위함입니다. 여기도 별일은 없죠.

3. mouseUp()

155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
			private function mouseUp(event:MouseEvent):void{				
				drawArea.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMove);
				drawArea.removeEventListener(MouseEvent.MOUSE_UP, mouseUp);				
				drawSurface.graphics.clear();				
				createShape();				
			} 
 
			private function createShape():void
			{									
				var tempNum1:int = 0;
				var tempNum2:int = 0;
				var tempShape:ShapeWithoutPoint;
 
				tempNum1 = Math.abs(drawArea.mouseX-startX);				
				tempNum2 = Math.abs(drawArea.mouseY-startY);
 
				if(tempNum1 > 3 || tempNum2 > 3){
					isSmall = false;
				}else{ 
					isSmall = true;
				}
 
				switch(toolDock.getButtonState()){
					case SELECT:
 
					break;
					case RECTANGLE:			
						if(!isSmall)
						{			
							getShapeOption();
							tempShape = new Square(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha, shapeColor, shapeAlpha);
							drawArea.addChild(tempShape);
							entityID = tempShape.getEntityID();
							sendMessageToCreateSquare();
						}														
					break;
					case CIRCLE:
						if(!isSmall)
						{
							getShapeOption();
							tempShape = new Circle(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha, shapeColor, shapeAlpha);
							drawArea.addChild(tempShape);
							entityID = tempShape.getEntityID();
							sendMessageToCreateCircle();
						}
					break;
					case DRAW:
						if(!isSmall)
						{
							getShapeOption();						
							tempShape = new Line(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, linePoints, lineThickness, lineColor, lineAlpha);
							drawArea.addChild(tempShape);
							entityID = tempShape.getEntityID();
							sendMessageToCreateLine();
							clearArray(linePoints);
						}
					break;
					case TEXT:
						if(!isSmall)
						{
							getShapeOption();
 
							tempShape = new FontBox(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha, lineColor, lineAlpha, fontName, lineThickness);
							drawArea.addChild(tempShape);
							entityID = tempShape.getEntityID();
							sendMessageToCreateFontBox();
						}
						//canvas.addChild(new FontBox(canvas, userID, connector, basisX, basisY, posX-basisX, posY-basisY, lineThickness, lineColor, lineAlpha, shapeColor, shapeAlpha, fontName, fontSize));						
					break;
					case LINE:
						if(!isSmall)
						{
							getShapeOption();						
							tempShape = new StLine(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha);
							drawArea.addChild(tempShape);
							entityID = tempShape.getEntityID();
							sendMessageToCreateStLine();
							clearArray(linePoints);
						}
						//canvas.addChild(new StLine(canvas, userID, connector, basisX, basisY, posX-basisX, posY-basisY, lineThickness, lineColor, lineAlpha));
					break;					
 
					case LIGHTPEN:
						getShapeOption();
						tempShape = new LightLine(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, linePoints);
						drawArea.addChild(tempShape);
						entityID = tempShape.getEntityID();
						sendMessageToCreateLightLine();
						clearArray(linePoints);
					break;
 
					case MEN:
						getShapeOption();
						tempShape = new Men(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha);
						drawArea.addChild(tempShape);
						entityID = tempShape.getEntityID();
						sendMessageToCreateMen();
					break;
 
				}
			}

case 문 덕분에 여전히 깁니다. -_-; 꼭 리팩토링하고 싶은 부분이기도하죠. 어쨋든 이부분이 객체공유의 시작입니다. getShapeOption() 함수는 마우스로 그린 데이터를 복사하는 거라 별로 신경쓰지 않으셔도 됩니다. 그렇게 복사한 후 case문에 따라 객체 타입에 맞춰서 객체를 생성하고 현재 화면에 나타나게 합니다. 그 다음에 sendMessageToXXXXXXX() 요 부분이 중요한 겁니다. 좀 더 자세히 보자면

181
182
183
184
185
186
187
188
189
					case RECTANGLE:			
						if(!isSmall)
						{			
							getShapeOption();
							tempShape = new Square(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha, shapeColor, shapeAlpha); // 객체를 생성하고
							drawArea.addChild(tempShape); // 화면에 표시한 다음
							entityID = tempShape.getEntityID(); 
							sendMessageToCreateSquare(); // 공유!
						}

공유! 라고 한 저 함수를 살펴보면

283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
			public function sendMessageToCreateSquare():void
			{
				//trace("sendMessage");
				var posX:int = drawArea.mouseX;
				var posY:int = drawArea.mouseY;
 
				var commandData:Object;
				var event:CommandEvent;
				//if(isConnected)
				//{					
					commandData = {userID:this.userID, entityID:this.entityID, entityType:"square", eventType:CommandEvent.MAKESQUARE_EVENT
						, startX:startX, startY:startY, entityWidth:posX-startX, entityHeight:posY-startY
						, shapeColor:shapeColor, lineColor:lineColor, lineThick:lineThickness, lineAlpha:lineAlpha, shapeAlpha:shapeAlpha};
					event = new CommandEvent(CommandEvent.SEND_EVENT, commandData);
					connector.dispatchEvent(event);					
				//}
			}

commandData 를 생성하고 CommandEvent 라는 이벤트를 만들어 connector에 알려줍니다. gatherer는 모든 사용자가 같은 화면을 보고 있기를 원했습니다. 실제 현실에서 한 회의실에 한 화이트보드에 펜을 들고 회의를 한다면 당연한 이야기지요. 그걸 구현하려고 하니 모든 사용자가 같은 화면을 보고 있어야 합니다. whiteboardPanel에서는 그저 화면에 그리는 부분을 담당하고 공유시키는(통신하는) 부분은 Connector라는 클래스가 담당하게 됩니다. dispatchEvent() 함수를 호출하면 해당 객체는 넘겨받은 이벤트가 발생하지요. Flex의 이벤트 구조상 이벤트 핸들러가 있다면 어떤 인스턴스라도 이벤트가 일어날 수 있는 구조를 갖고 있습니다. 또 꼭 이벤트 구조를 활용한 이유는 각 클래스간의 커플링을 낮추기 위함인데, gatherer에 사용된 모든 객체는 UIComponent를 상속해서 만들었고 따라서 EventDispatcher의 자식 클래스이기도 합니다. 결국 모든 객체들은 이벤트를 받을 수 있다고(.dispatchEvent() 함수가 존재한다고) 가정한 것이지요.
또한 중요한 점은 전달/발생하는 이벤트는 제가 정의한 CommandEvent 클래스라는 점입니다. CommandEvent는 객체간의 통신 데이터를 래핑한 수준인데요, 객체에 필요한 동작(함수)이나 데이터를 이 클래스의 인스턴스에 포장해서 보내는 것이죠. 받는 측에서도 전송받은(공유받은) 데이터를 토대로 CommandEvent의 인스턴스를 생성하여 필요한 객체에 전달해주면 됩니다. 그렇게 되면 전달하는 객체는 전달받을 객체의 내부구조를 전혀 알 필요가 없이 그냥 전송하는 것으로 책임을 다 하게 되며, 동작에 대한 책임은 해당 전달받은 객체가 지게 됩니다.
gatherer_diagram1
간단히 표현하면 위와같은 형태가 될 것입니다.
Connector 클래스와 CommandEvent 클래스를 살펴보면 좀 더 이해가 잘 갈 것입니다.
Connector.as 파일을 열어보세요. 경로는 PROJECT_ROOT/src/com/gatherer/network/Connector.as 입니다. SharedObject를 위한 NetConnection에 대한 부분이 있구요, 데이터 전송에는 sendHandler()가, 수신에는 syncEventHanlder()가 있습니다.

93
94
95
96
97
98
99
100
101
102
		public function sendHandler(event:CommandEvent):void
		{
			send(event.command);
		}
 
		private function send(command:Object):void 
		{
			//so.setProperty("command", command);
			connection.call("sendCommand", null, this.userID, command);
		}

엄청 간단하죠? 데이터를 전송한다는 게 꽤 어렵다고 생각될 수 있는데 gatherer 소스는 꽤나 간단합니다. SharedObject.setProperty()와 NetConnection.call()이 거의 비슷한 일을 수행하는데요, userid를 전송하는 부분이 채팅 부분과 결합되어 있어서, 해당 데이터를 서버에서 핸들링하려고하다보니 NetConnection.call()을 사용하게 되었습니다.
하는 역할이라곤 command를 서버에 전송하는 것이고, 서버에서도 전송받은 command라는 데이터를 각 클라이언트에게 broadcast하는 것에 지나지 않습니다. command라는 데이터는 위의 “공유!”라고 써있는 부분을 설명한 whiteboardPanel.sendMessageToCreateSquare()에서 commandData입니다.

					commandData = {userID:this.userID, entityID:this.entityID, entityType:"square", eventType:CommandEvent.MAKESQUARE_EVENT
						, startX:startX, startY:startY, entityWidth:posX-startX, entityHeight:posY-startY
						, shapeColor:shapeColor, lineColor:lineColor, lineThick:lineThickness, lineAlpha:lineAlpha, shapeAlpha:shapeAlpha};
					event = new CommandEvent(CommandEvent.SEND_EVENT, commandData);
					connector.dispatchEvent(event);

commandData 또한 Object 클래스의 인스턴스일 뿐입니다. Flex에서 Object 클래스는 꽤나 특이한 구조를 갖고 있습니다. JSON과 같이 key:value 페어의 데이터를 계속 셋팅할 수 있습니다. 그래서 위와 같이 userID:this.userID 로 셋팅할 수 있는 것이죠. 문제는 해당 데이터를 핸들링하기 위해서는 데이터 구조를 잘 알고있어야 한다는 점입니다. 그것을 좀 더 자동화한 구조를 계획했었으나 구현하지 못했죠. 그래서 저 장황한 case 문을 쓰게 된 겁니다. 만약 그냥 데이터를 전달한다는 생각만 한다면 굳이 저렇게 구성하지 않고 배열이나 벡터와 같은 자료구조에 데이터를 넣어서 필요한 함수를 호출하면 땡이지만, 그러면 해당 클래스 또는 함수들은 호출->호출 등 아주 확실하게 결합하게 됩니다. gatherer에서의 목표는 결합도를 낮추는 것이였지요. CommandEvent를 찾아보면 해답에 근접하게 될 것입니다.

CommandEvent.as 파일을 살펴봅시다. 경로는 PROJECT_ROOT/src/com/gatherer/event/CommandEvent.as 입니다. 정말 간단히 되어 있는데 사실 Flex에서 이벤트 객체가 저렇게 간단합니다. -_-; 버블링을 한다면 좀 더 복잡해지겠지만, gatherer 특성상 버블링은 일어나지 않는게 좋습니다. 잘못했다간 무한 이벤트 전달이 일어나거든요. static으로 선언된 수많은 이벤트 타입을 정의한 부분을 보실 수 있습니다. whiteboard에 필요한 기능들을 총망라한 것이라고 보면 됩니다. 한 이벤트로 동작들을 표현하기 위함입니다. 이것으로 모든 객체는 같은 이벤트 객체를 전달받을 수 있고 전달받은 객체가 그 이벤트 타입이 있으면 동작하고 없으면 무시됩니다. 에러메세지 같은 건 나타나지 않지요. 그게 포인트입니다. -_-

데이터를 수신하는 부분을 살펴보면 또 한걸음 명확해질 겁니다. 다시 Connector.as로 돌아가서 수신하는 쪽을 살펴봅시다. 계속 이야기하지만 gatherer에서는 통신을 SharedObject로 구현했으며 SharedObject는 broadcast로 동작합니다. 이것은 데이터를 송신하면 수신이 자동적으로 일어난다는 겁니다. A객체가 데이터를 송신하면 해당 연결을 갖고 있는 객체 A, B, C, D.. (A도 포함입니다!)가 데이터를 수신합니다. 꽤 빠릅니다. 문제는 A가 보낸 데이터를 바로 A가 수신한다는 것인데요, 그건 데이터에 포함된 userid로 구분하는 것을 구현하면 됩니다. Connector.as에서

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
		public function syncEventHanlder(event:SyncEvent):void
		{
			var i:Number;
			trace("sync first");
			if(isFirst == true)
			{ 
				if(so.data.commandArray != null){
					trace("sync first2");
					commandArray = so.data.commandArray;
					if(commandArray != null){
						for(i=0;i<commandArray.length;i++){
							trace("initDraw");
							receive(commandArray[i]);
						}
					}
				}
				isFirst = false;
			}
 
			var command:Object = so.data.command;
			trace("sync: "+this.userID);
			if(command != null && command.userID != this.userID)
			{
				trace("OnSync=======================================================");				
				trace("command.userID:"+command.userID);
				trace("this.userID:"+this.userID);
				receive(command);
			}
		}
 
		private function receive(command:Object):void
		{
			//receiver 
			Owner.dispatchEvent(new CommandEvent(command.eventType, command));
		}

이 부분이 데이터를 수신하는 부분입니다. SharedObject에 SyncEvent의 핸들러를 설정하면 저절로 호출되겠죠. syncEventHanlder()는 자신의 아이디를 확인해서 자신이 보낸 데이터가 아니라면 receive()를 호출합니다. receive() 함수라고 복잡하지는 않습니다. 딱 한줄. -_- 전송받은 데이터를 그대로(진짜 그~대~로) CommandEvent의 인스턴스로 만들어서 Connector 클래스 인스턴스를 소유하고 있는 객체에 이벤트로 보내버립니다. 그게 끝이죠. 소유자라고 해봐야 whiteboardPanel입니다. 꽤 단순해 보이지만 저 단순한 구조 덕분에 클래스간의 결합도는 팍~~~~~~~~~~ 떨어지게 되었습니다. whiteboardPanel이 저 CommandEvent를 받아도 할 건 거의 없습니다. 보시면 압니다.

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
			public function init():void{				
				super.positionChildren();				
				drawSurface = new UIComponent();
				drawArea.addChild(drawSurface);
				drawArea.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown);				
				linePoints = new Array();
 
				connector = new Connector(drawArea);
				connector.connect("rtmp://192.168.10.99/board", userID, roomName);				
				/*
				drawArea.addEventListener(CommandEvent.MAKESQUARE_EVENT, makeSquareHandler);
				drawArea.addEventListener(CommandEvent.MOVE_EVENT, moveSquareHandler);
				drawArea.addEventListener(CommandEvent.DELETE_EVENT, deleteSquareHandler);
				drawArea.addEventListener(CommandEvent.RESIZE_EVENT, resizeSquareHandler);
				drawArea.addEventListener(CommandEvent.ROTATE_EVENT, rotateSquareHandler);
				*/
 
				drawArea.addEventListener(CommandEvent.MAKESQUARE_EVENT, makeSquareHandler);
				drawArea.addEventListener(CommandEvent.MAKECIRCLE_EVENT, makeCircleHandler);
				drawArea.addEventListener(CommandEvent.MAKELINE_EVENT, makeLineHandler);
				drawArea.addEventListener(CommandEvent.MAKELIGHTLINE_EVENT, makeLightLineHandler);
				drawArea.addEventListener(CommandEvent.MAKESTLINE_EVENT, makeStLineHandler);
				drawArea.addEventListener(CommandEvent.MAKEFONTBOX_EVENT, makeFontBoxHandler);
				drawArea.addEventListener(CommandEvent.MAKEMEN_EVENT, makeMenHandler);
				drawArea.addEventListener(CommandEvent.MOVE_EVENT, squareHandler);
				drawArea.addEventListener(CommandEvent.DELETE_EVENT, squareHandler);
				drawArea.addEventListener(CommandEvent.RESIZE_EVENT, squareHandler);
				drawArea.addEventListener(CommandEvent.ROTATE_EVENT, squareHandler);
				drawArea.addEventListener(CommandEvent.CHANGETEXTCONTENT_EVENT, squareHandler);
			}

init() 함수에는 CommandEvent 핸들러가 꽤 많이 지정되어 있습니다. 저 이벤트 핸들러가 호출되는 건 아까 Connector.receive()에서 이벤트를 전달해주면 자동 호출되는데 전송받은 데이터 안에 eventType도 들어있기 때문에 whiteboardPanel에서 어떤 이벤트인지 알 필요는 없습니다. 그저 알맞는게 호출되는 거지요. 호출되는 핸들러 또한 간단합니다. 이벤트 안에 데이터가 다 들어있으니까요. makeSquareHandler() 요거 하나만 살펴볼까요?

455
456
457
458
459
460
461
			public function makeSquareHandler(event:CommandEvent):void
			{				
				var commandData:Object = event.command;
				trace("draw: "+commandData.userID);
				createReceivedShape(RECTANGLE, drawArea, this.userID, commandData.startX,commandData.startY, 
				commandData.entityWidth, commandData.entityHeight, commandData.lineThick, commandData.lineColor, commandData.lineAlpha, commandData.entityID, commandData.shapeColor, commandData.shapeAlpha);
			}

전송받은 데이터로 객체를 생성하는 겁니다. 끝이죠. 만약에 데이터가 전달받을 목적지가 whiteboardPanel이 아니라 이미 생성되어 있는 네모나 동그라미 같은 객체라고 하더라도 어려울 것은 없습니다. 해당 객체에 전달받은 이벤트를 그대로 또 전달해주기만 하면 되거든요.

지금까지 설명한 것이 gatherer의 핵심입니다. 데이터를 주고 받고 그것을 토대로 객체들을 핸들링 하는 것이지요. 내용을 정리하면 각 역할별로 객체를 할당했고 서로 호출하는 것이 아니라 이벤트를 발생시키며, 이벤트 핸들러가 있으면 동작할 것이고 아니어도 에러나지 않습니다.

이번 강좌는 여기서 마치겠습니다. 다음은 보드 위에 그려지는 객체를 구현하는 방법에 대해서 설명하겠습니다.

Gatherer – Online Collaboration tool 만들기 by 13th warrior (4) – camPanel


네번째 강좌

camPanel.mxml입니다. 영상/음성 송수신 부분이지요. 이 모듈은 제가 전혀 관혀를 하지 않고 다른 팀원에게 일임했기 때문에 제대로 아는 것은 아닙니다. 그래서 이번 강좌는 소스 분석 형태로 진행하겠습니다.(여지것 다른 강좌도 계속 소스분석 정도 밖에 안되었지만서도.. -_-;;)
경로는 \PROJECT_ROOT\src\extenders\panel\camPanel.mxml 입니다. 바로 init()을 살펴보죠.

			private function init():void
			{
				var timeSet:Timer = new Timer(1000, 1);
				timeSet.addEventListener(TimerEvent.TIMER, streamSetting);
				netConnection();
				timeSet.start();
			}
 
			private function netConnection():void
			{
				entityWidth = camArea.width/3 - 20;
				entityHeight = entityWidth * 3/4;
				camArea.addEventListener(ResizeEvent.RESIZE, camSize);
				nc = new NetConnection;
				nc.objectEncoding = ObjectEncoding.AMF0;
				nc.connect("rtmp://192.168.10.99/video", userID, roomName);
				so = SharedObject.getRemote(roomName, nc.uri, true);
				so.addEventListener(SyncEvent.SYNC, syncHandler);
				so.connect(nc);
			}

어도비사에서는 flash와 FMS를 이용한 스트리밍 서비스를 제공하고 있습니다. 대충의 개요는 FMS에 스트리밍 서비스 어플리케이션을 띄우고 해당 서버 어플리케이션을 rtmp/NetConnection으로 접근한 후 NetStream을 SharedObject로 열어서 그 스트림을 VideoStream으로 보내는 것입니다.
위의 init()에서는 타이머를 하나 지정하였는데 그 타이머는 다른 사용자가 로그인/로그아웃하는 것을 캐치하여 연결하고 보여주기 위한 겁니다. netConnection()에서는 그저 서버에 연결하고 SharedObject를 하어 열고 Sync 이벤트 핸들러를 설정하는 것 뿐입니다. 지난번 chatPanel.mxml에서 텍스트 채팅을 위한 커넥션을 하나 설정하였는데 여기서도 다른 커넥션을 생성합니다. (gatherer에서는 총 3개의 NetConnection이 있습니다. 채팅에 하나, 음성/화상 스트리밍에 하나, 화이트보드에서 움직이는 객체들을 위한 연결 하나 이렇게 총 3가지 입니다. 이렇게 커넥션을 나눈 이유는 스트리밍 커넥션이 꽤 느려서 한 커넥션으로 모든 객체들을 공유(통신)해보았는데 너무 느리더군요. 그래서 나누어서 연결하여보니 괜찮았습니다. 아무래도 각 커넥션마다 다른 쓰레드가 생성되는 것이 아닌가 싶네요. FMS를 정식버전이 아닌 개발버전에서 사용하면 커넥션을 10개만 할 수 있다는 제한이 걸려 있는데, 그래서 gatherer를 FMS 개발버전으로 사용하면 고작 3명의 클라이언트만 사용할 수 있습니다. -_-; 추후 red5용으로 컨버전하고 싶지만 시간이 잘 허락하질 않네요.)
camArea:DrawArea는 그냥 SuperPanel을 mxml로 로딩한 것에 지나지 않습니다. Resize 이벤트 핸들러 또한 그냥 크기를 바꾸고 화면을 갱신하는 것이죠.

타이머 이벤트 핸들러인 streamSetting()을 살펴봅시다.

			private function streamSetting(event:TimerEvent):void
			{
				var i:Number;
				var cmp:String;
				userNum = so.data.userNum;
				idArray = so.data.idList;
				for(i=0;i<userNum;i++){
					cmp = idArray[i];
					if(userID == cmp){
						myNum = i;
						break;
					}
				}
				connectStream();
				viewStart();
			}	
 
			private function connectStream():void
			{
				sendCam = new NetStream(nc);
				sendCam.attachCamera(Camera.getCamera());
				sendCam.attachAudio(Microphone.getMicrophone());
				sendCam.publish(userID+"Stream");
			}
 
			private function viewStart():void
			{
				var i:Number;
				userCnt = 0;
				camClear();
				idArray = so.data.idList;
				for(i=0;i<so.data.userNum;i++){
					if(userID == idArray[i]){
						myNum = i;
					}
				}
				for(i=0;i<so.data.userNum;i++){
					userIn();
				}
			}
 
			private function camClear():void{
				var i:Number;
				var tmp:Object;
				if(childCnt >= 0){
					for(i=0;i<childCnt;i++){
						camArea.graphics.clear();
						camArea.removeChildAt(0);
					}
				}
				childCnt = -1;
			}
 
			private function userIn():void
			{
				if(userCnt < 8){
					if(userCnt == 0){
						startX = 10;
						startY = 30;
					}
					else if(userCnt%3 == 0){
						startX = 10;
						startY += entityHeight + 30;
					}
					else{
						startX += entityWidth + 10;					
					}
					camDraw();
				}
				if(userCnt < 8){
					userCnt++;
				}
			}
 
			private function camDraw():void
			{
				var tmp:String;
				var tmpNum:Number = userCnt;
				var tmpId:String;
				videoView = new Video;
				if(userCnt <= myNum){
					tmpNum = userCnt-1;
				}
				else{
					tmpNum = userCnt;
				}
				trace("canNum: "+userCnt);
				switch(userCnt){
					case 0:
						videoStream1 = new NetStream(nc);
						tmp = idArray[myNum]+"Stream";
						videoView.attachNetStream(videoStream1);
						videoStream1.play(tmp);
						tmpId = idArray[myNum];
					break;
					case 1:
						videoStream2 = new NetStream(nc);
						tmp = idArray[tmpNum]+"Stream";
						videoView.attachNetStream(videoStream2);
						videoStream2.play(tmp);
						tmpId = idArray[tmpNum];
					case 2:
						videoStream3 = new NetStream(nc);
						tmp = idArray[tmpNum]+"Stream";
						videoView.attachNetStream(videoStream3);
						videoStream3.play(tmp);
						tmpId = idArray[tmpNum];
					break;
					case 3:
						videoStream4 = new NetStream(nc);
						tmp = idArray[myNum]+"Stream";
						videoView.attachNetStream(videoStream4);
						videoStream4.play(tmp);
						tmpId = idArray[tmpNum];
					break;
					case 4:
						videoStream5 = new NetStream(nc);
						tmp = idArray[myNum]+"Stream";
						videoView.attachNetStream(videoStream5);
						videoStream5.play(tmp);
						tmpId = idArray[tmpNum];
					break;
					case 5:
						videoStream6 = new NetStream(nc);
						tmp = idArray[myNum]+"Stream";
						videoView.attachNetStream(videoStream6);
						videoStream6.play(tmp);
						tmpId = idArray[tmpNum];
					break;
					case 6:
						videoStream7 = new NetStream(nc);
						tmp = idArray[myNum]+"Stream";
						videoView.attachNetStream(videoStream7);
						videoStream7.play(tmp);
						tmpId = idArray[tmpNum];
					break;
					case 7:
						videoStream8 = new NetStream(nc);
						tmp = idArray[myNum]+"Stream";
						videoView.attachNetStream(videoStream8);
						videoStream8.play(tmp);
						tmpId = idArray[tmpNum];
					break;
				}
 
				camViewer = new CamViewer(startX, startY, entityWidth, entityHeight);
				camArea.addChild(camViewer);
				camViewer.addChild(videoView);
				videoView.width = entityWidth;
				videoView.height = entityHeight;
				videoView.x = startX;
				videoView.y = startY;
 
				idText = new Text;
				idText.setStyle("fontWeight", "bold");
				idText.setStyle("textAlign", "center");
				idText.setStyle("fontSize", 10);
				idText.setStyle("color", 0x000000);
				idText.text = tmpId;
				idText.x = startX;
				idText.y = startY+entityHeight+5;
				idText.width = entityWidth;
				idText.height = 50;
 
				camViewer.addChild(idText);				
 
				camArea.graphics.lineStyle(8, 0x666666, 0.7);
				camArea.graphics.drawRect(startX, startY, entityWidth, entityHeight);
 
				childCnt = camArea.getChildIndex(camViewer)+1;
			}

척 보시면 아시겠지만 핵심은 camDraw()에 있습니다. 제가 영 싫어하는 구조를 갖고 있는데요, 이 코드를 보시는 분은 저렇게 코딩하면 안됩니다. 쓸데없이 함수로 나누어져 있고 그 구분도 모호합니다. -_-; room단위로 나누고 한 room에 접속 가능한 인원을 최대 9명으로 결정했었습니다. 그랬더니.. 변수를 8개 만들었더군요. 이러면 기능이 변경되거나 추가할 때 고쳐야 하는 부분이 많아 집니다. 특정 데이터 구조(벡터나 해시 테이블 등)에 공간을 할당하고 거기에 각종 객체를 생성하는 방법을 써야 하는데 구현을 못하고 이렇게 되었네요. 게다가 camDraw()에는 사용자 접속에 따라 case를 사용해서 똑같은 기능을 반복하고 있습니다. 나쁜 코드의 전형이지요. 타산지석으로 삼기 바랍니다. -_-;
어쨌든 streamSetting()으로 다시 돌아갑시다. streamSetting() 함수는 타이머의 이벤트 핸들러이고 1초로 셋팅했으므로 매 초마다 streamSetting()이 호출될 겁니다. 서버 video 어플리케이션을 통하여 공유된(전송받은) userNum과 idList로 열어야 하는 상대방 사용자를 설정하고 connectStream(), viewStart()를 실행합니다. connectStream() 함수는 새로운 stream을 생성해서 자신의 캠과 음성을 연결합니다. 이건 명백히 오류군요. -_-;; 연결과 연결 설정은 한번만 하는 것인데 매초마다 갱신하니.. 느려질 수 밖에 없네요.
소스는 복잡해 보이지만 구현이 잘못되어 있는 것이고 제대로 한다면 꽤 단순하게 음성/화상 공유가 가능합니다. 수신 부분은 NetConnection -> NetStream -> Video 이렇게 계속 객체를 연결시켜주면 되고, 송신부분은 NetConnection -> NetStream 연결하고 NetStream.attachCamera(Camera.getCamera())와 NetStream.attachCamera((Microphone.getMicrophone())을 호출하면 끝입니다. 나머지는 위치를 조정하는 정도의 UI 작업이 끝이지요. 아.. 예전에 이 소스를 리뷰했어야 하는데 동작하는 걸 보고 그냥 넘어간게 너무 부끄럽네요.

화상/음성은 여기서 그냥 마치고 다음은 gatherer의 핵심인 화이트보드의 객체 공유부분을 알려드리겠습니다.

Gatherer – Online Collaboration tool 만들기 by 13th warrior (3) – chatPanel


세번째 강좌

chatPanel.mxml입니다. 경로는 \PROJECT_ROOT\src\extenders\panel\chatPanel.mxml 입니다.
testBoard.mxml보다 훨씬 간단한 구조를 갖고 있고, 실제 기능도 단순합니다. 선언부를 보면

<SuperPanel xmlns="extenders.panel.*" xmlns:mx="http://www.adobe.com/2006/mxml" width="304" height="300"
	 showControls="true" enableResize="true" title="CHAT" creationComplete="super.positionChildren(), init()">
	<mx:VDividedBox width="100%" height="100%">
		<mx:TextArea id="chatList" width="100%" height="85%" fontSize="12" editable="false" focusAlpha="0"/>
		<mx:HBox width="100%" height="15%">
			<mx:ColorPicker id="chatColor" width="10%" height="80%"/>
			<mx:TextInput id="chatInput" width="70%" height="80%" fontSize="12" focusAlpha="0"/>
			<mx:Button label="Ent" height="80%" width="18%" id="chatSend"/>
		</mx:HBox>
	</mx:VDividedBox>

gatherer에 사용된 Panel은 Panel 컴포넌트를 상속한 SuperPanel 클래스를 사용합니다.(\PROJECT_ROOT\src\extenders\panel\SuperPanel.as)
mx:TextArea, mx:ColorPicker, mx:TextInput, mx:Button 딱 이름만 봐도 어떤 기능으로 사용할 지 쉽게 알 수 있을 겁니다. 책 등의 예제와 다른 점은 이벤트 핸들러가 기술되어 있지 않다는 점인데, 그것은 최초 동작하는 이벤트 핸들러인 init()에서 직접 핸들러를 설정합니다.
gatherer 전반에 이러한 형태로 구현이 되어 있는데, 이벤트 핸들러가 인스턴스가 생성될 시점에는 설정되어 있지 않다가 특정 이벤트에 의해 다른 이벤트 핸들러도 설정되는 방법입니다. 예를 들면 drag & drop 기능은 mouse down -> mouse move -> mouse up 이렇게 세 개의 이벤트가 순차적으로 발생되게 됩니다. 그러면 최초 인스턴스 생성시 mouse down 이벤트 핸들러가 설정되어 있고, 이후 mouse down 이벤트 핸들러에 의해 mouse move, mouse up 핸들러가 설정되고 다시 mouse up 핸들러에 의해 mouse move, mouse up 핸들러는 해제됩니다. 물론 context switch 등의 오버헤드가 생길 수 있으나 bubbling 등 이벤트 핸들러가 많을 수록 사용자의 오류를 유발시킬 수 있고, 오류를 제어하거나 디버깅을 편하게 하기 위해 이렇게 구현했습니다.(bubbling이란 한 이벤트가 발생되었을 때 소유자 등에게 해당 이벤트가 전이되는 것을 말하는 것인데, 예를 들면 버튼 컴포넌트에 클릭 이벤트가 발생했을 때 버튼을 포함하고 있는 패널 등의 컴포넌트에도 클릭 이벤트가 전달되는 FLEX 특유의 이벤트 전달 형태를 의미합니다.)

실제 구현을 살펴보면 더 이해가 쉬울 겁니다.

			private function init():void
			{
				nc.objectEncoding = ObjectEncoding.AMF0;
				nc.connect("rtmp://192.168.10.99/chat", userID, roomName);
				so = SharedObject.getRemote(roomName, nc.uri, true);
				so.addEventListener(SyncEvent.SYNC, syncHandler);
				so.connect(nc);
 
				chatSend.addEventListener(MouseEvent.CLICK, mouseClick);
				chatInput.addEventListener(KeyboardEvent.KEY_DOWN, keyDown);
				chatColor.addEventListener(ColorPickerEvent.CHANGE, colorChange);
				chatColor.addEventListener(FocusEvent.FOCUS_IN, inputFocus);
				chatList.htmlText += "<font color='#FF6600'>" + roomName+ " 회의실에 입장하셨습니다." + "<br>";
			}

chatPanel이 활성화 되었을 때 동작하는 이벤트 핸들러인 init() 입니다. 여기서 addEventListener 메소드를 이용하여 각 이벤트 핸들러를 설정합니다. 그리고 더욱 중요한 건 서버와 네트웍을 연결하는 것인데, nc(NetConnection) 과 so(SharedObject)입니다. NetConnection은 다른 소켓 통신과 같이 tcp/ip 연결을 하지만 rtmp라는 어도비사의 스트리밍 프로토콜을 사용하여 연결하게 됩니다. 이는 FMS(Flash Media Server)와 연결이 될 것이고 데이터로 userID, roomName을 넘겨줍니다. 그러면 FMS의 application 중 chat이 동작하여 해당 데이터 처리를 하지요.(chat은 rtmp://192.168.10.99/chat와 같이 url로 선택되고 그러면 서버의 chat이라는 인스턴스가 해당 연결을 유지합니다.)

이후에 NetConnection으로부터 SharedObject를 생성하게 되는데요, 이 SharedObject가 어도비사의 flash, Flex의 객체간의 통신의 핵심입니다. SharedObject는 기본적으로 broadcast이므로 각 연결되 pear에 전송받은 데이터 모두를 매번 모두 전송하게 됩니다. 데이터가 전송되었을 때 기능을 동작시키기 위해 SyncEvent와 그에 맞는 핸들러가 필요하지요. SyncEvent의 SYNC 핸들러인 syncHandler 살펴봅시다.

			private function syncHandler(event:SyncEvent):void{
				if(userNumber != Number(so.data.userNum)){
						userInOutMsg();
						isFirst = true;
				}
				if(isFirst != true){	
					showMsg();
				}
				userNumber = so.data.userNum;
				isFirst = false;
			}
 
			private function showMsg():void
			{
				receiveChatObject = so.data.chatData;
				if(receiveChatObject != null){
					showMsgContent = "<font color='#" + receiveChatObject.fontColor + "'>" + receiveChatObject.userId + ": " + receiveChatObject.chatContent + "</font><br>";
					chatList.htmlText += showMsgContent;
				}
				chatList.validateNow();
				chatList.verticalScrollPosition = chatList.maxVerticalScrollPosition;
 
			}
 
			private function userInOutMsg():void
			{
				if(userNumber > so.data.userNum){
					chatList.htmlText += "<font color='#FF6600'>" + so.data.userID + " 님이 나가셨습니다." + "<br>";
				}
				else{
					chatList.htmlText += "<font color='#FF6600'>" + so.data.userID + " 님이 입장하셨습니다." + "<br>";
				}
			}

SharedObject는 말 그대로 객체를 공유하기 위한 클래스입니다. 그래서 이벤트 이름도 SyncEvent이고 데이터 전송하는 메소드의 이름도 send()가 아닌 setProperty()입니다. SharedObject의 SharedObejct.data에 공유되는 데이터가 Object 클래스와 같은 key/value의 쌍으로 공유됩니다. 이게 생각보다 꽤 편하더군요. 데이터를 브로드캐스팅하는 거야 채팅이니까 모든 피어에게 전달되는 것은 맞지만 SharedObject의 특징 중 하나인 다른 피어들이 통신하고 있는 중간에 새로운 피어가 접속되었을 때의 동작이 문제가 될 수 있습니다. 여러 피어들이 통신하고 있는 상태에서 새로운 피어가 접속을 한다면 그 새로운 피어에게는 다른 피어들이 공유 중인(서로 주고받은 데이터) 모든 데이터를 한번에 모두 받게 됩니다. 이건 채팅에 썩 좋지 않지요. 그래서 서버 프로그램인 chat.main.asc에서 매번 데이터에 sequential한 번호를 주어 해당 피어가 처음 접속한건지 아닌지를 구분하게 했습니다. 그리고 TextArea 컴포넌트는 html을 랜더링할 수 있고 그것을 활용한 코드를 위에서 볼 수 있을 겁니다.

			private function sendMsg():void
			{				
				chatMsgContent = chatInput.text;
				chatInput.text = "";
				sendChatObject = {userId:userID, fontColor:fontColor, chatContent:chatMsgContent};
				so.setProperty("chatData", sendChatObject);
				chatInput.setFocus();
			}

데이터를 전송하는 것은 쉽습니다. 해당 데이터(채팅 내용)를 key/value인 Object 타입으로 셋팅해서 setProperty() 메소드를 호출합면 됩니다. 간단하죠. sendChatObject = {userId:userID, fontColor:fontColor, chatContent:chatMsgContent}; 와 같이 셋팅하여 so.setProperty(“chatData”, sendChatObject);를 호출하면 데이터를 전송받은 측은(syncHandler() 메소드에서 실행되겠지만) so.data에 데이터가 들어있어서 so.data.userId 와 같이 간단히 사용할 수 있죠.

나머지 메소드와 변수들은 텍스트를 화면에 표시하고 버튼 클릭 이벤트나 색 조정 등 UI 핸들링 메소드로 간단간단하게 구현되어 있어서 설명은 생략하겠습니다.

이번 강좌는 여기서 마칩니다. 다음은 camPanel을 진행하겠습니다.