8.11. A Simple Video and Text Chat ApplicationChapter 5 described a very simple two-way chat that required each user to know that the other user was online and to manually subscribe to the other user 's stream by name . Shared objects provide the facilities required to make movies aware of all the users connected to a chat or conferencing application instance and to notify the movies about all the streams that are available. We've already seen in Example 8-5 a server-side script that forces users to log in with a unique username. As each user logs in, his username is used as the name of a shared object property that contains a unique path assigned to him by the script. For example, if three users named " rreinhardt ", " blesser ", and " peldi " are logged in, the public/userList shared object contains the following properties: blesser: public/chat/blesser peldi: public/chat/peldi rreinhardt: public/chat/rreinhardt Movies that connect to the application instance can read the shared object and present to each user a list of usernames. Each movie can also use the unique path associated with its user's name to publish an audio and video stream and can subscribe to the stream of all the other users by retrieving the unique path for each user from the shared object. The complete version of Example 8-11 on the book's web site contains all the files necessary to implement a chat/conference room with the following features:
Example 8-5 already lists the server-side script that forces each user to log in with a unique username and updates the public/userList shared object, and earlier code snippets showed excerpts from the Chat class that implements the text chat feature. The remainder of this chapter describes how the user list is implemented and how shared objects and streams work together to provide an audio/video chat facility. The entire application, though not complex, is too large to describe in detail here. The application has three states: Init, Login, and Chat. Each state is represented by a separate labeled sequence of frames on the timeline, as shown in Figure 8-3. Figure 8-3. Timeline state labels for the chat applicationThe Init frame contains a global trim( ) function, a NetConnection subclass, and an application object that controls changing state between the Login and Chat frames. When the movie starts, these objects are created and initialized , and the playhead is sent to the Login frame. When the user submits a username, the application object attempts to connect to an application instance. If successful, the playhead is sent to the Chat frame where the real work of the movie is done. The Chat frame contains an onstage instance of the Chat movie clip symbol with the following elements:
More importantly, there are two movie clip symbols in the Library that are attached to the Chat movie clip as needed:
Both movie clip symbols are designed to be attached to another movie clip and expect to be passed a streamName variable containing the relative URI of a stream to either publish or subscribe to. Example 8-11 is the complete listing of the Chat class including comments that describe each method. See the book's web site for the complete .fla including the code for the StreamController and StreamViewer symbols. Example 8-11. Chat class for the video chat application#initclip // Chat constructor function. Most of the setup work for this class // is done in the init( ) method. function Chat( ) { this.enabled = false; // Current enabled state--see setEnabled( ) method this.streamLevel = 100; // Level to place stream viewer and controller clips this.streamClipCount = 0; // Number of clips--for pop-up clip positioning } Chat.prototype = new MovieClip( ); Object.registerClass("Chat", Chat); // onLoad( ) sets component properties that must wait for an onLoad event. Chat.prototype.onLoad = function ( ) { this.send_pb.setEnabled(this.enabled); this.send_txt.selectable = this.enabled; }; // setEnabled( ) locks or unlocks the text chat's input text field. Chat.prototype.setEnabled = function (flag) { if (this.enabled == flag) { return; } else { this.enabled = !this.enabled; } this.send_pb.setEnabled(this.enabled); this.send_txt.selectable = this.enabled; if (!this.enabled) { this.send_txt.text = ""; } }; /* init( ) is called when the playhead reaches a frame containing the Chat * movie clip to set up the chat. It must be passed the user's current * username and a reference to a NetConnection object that has already * established a connection to the application instance. */ Chat.prototype.init = function (nc, userName) { this.nc = nc; this.userName = userName; this.setEnabled(false); this.welcome_txt.html = true; this.welcome_txt.htmlText = "Welcome <B>" + userName + "</B> to the chat area."; // Set up the userList_so shared object so that it notifies the Chat clip when // it needs to redraw the people list and show/delete stream clips. this.userList_so = SharedObject.getRemote("public/userList", nc.uri); this.userList_so.owner = this; this.userList_so.onSync = function (list) { this.owner.redrawPeopleList(this, list); this.owner.displayStreams(this, list); }; // Set up the messages_so shared object to receive text messages. this.messages_so = SharedObject.getRemote("public/textchat/messages", nc.uri); this.messages_so.owner = this; this.messages_so.ready = false; this.messages_so.onSync = function (list) { if (!this.ready) { this.ready = true; this.owner.onMessagesReady(this); } }; this.messages_so.showMessage = function (msg) { this.owner.showMessage(msg); }; // Connect the shared objects. this.userList_so.connect(nc); this.messages_so.connect(nc); // Publish a stream for this user with video and audio. this.ns = new NetStream(nc); this.cam = Camera.get( ); this.ns.attachVideo(this.cam); this.mic = Microphone.get( ); this.ns.attachAudio(this.mic); // Use the username to set the URI for the stream to publish. this.ns.publish("public/chat/" + userName); }; // redrawPeopleList( ) is called by userList_so.onSync( ) . Chat.prototype.redrawPeopleList = function (so, list) { this.peopleList_lb.removeAll( ); for (var p in so.data) { this.peopleList_lb.addItem(p, so.data[p]); } }; /* displayStreamController( ) attaches a StreamController clip to the Chat clip * and passes it the name of the stream and screen position to appear in. * See the displayStreams( ) method. */ Chat.prototype.displayStreamController = function (name, streamName, x, y) { this.attachMovie("StreamController", name, this.streamLevel++, {_x:x, _y:y, name:name, streamName:streamName, ns:this.ns, cam:this.cam, mic:this.mic}); }; // displayStreamView( ) attaches a StreamViewer clip to the chat. Chat.prototype.displayStreamViewer = function (name, streamName, x, y) { this.attachMovie("StreamViewer", name, this.streamLevel++, {_x:x, _y:y, name:name, streamName:streamName}); }; /* displayStreams( ) is called by userList_so.onSync( ) whenever a user * is added or removed from the user list. It is passed both a * reference to the userList_so shared object and the information * object list passed to onSync( ) . */ Chat.prototype.displayStreams = function (so, list) { for (var p in list) { // Get the information object. var obj = list[p]; // If the code value is "delete" and there is a stream clip attached // to the chat clip, delete the stream clip. if (obj.code == "delete" && this[obj.name]) { this[obj.name].removeMovieClip( ); this.streamClipCount--; } // Otherwise, if a "change" code is received, add a stream clip for new user. else if (obj.code == "change") { var name = obj.name; if (!this[name]) { var x = (this.streamClipCount % 4) * 180; var y = (((this.streamClipCount % 8) < 4) ? 0 : 1) * 185; this.streamClipCount++; // Add a StreamController clip if the name is this user's username. if (name == this.userName) { this.displayStreamController(name, so.data[name], x, y); } else { this.displayStreamViewer(name, so.data[name], x, y); } } } // End else-if. } }; // onMessagesReady( ) is called when the messages shared object is first // synchronized and is used to visibly enable the text chat fields. Chat.prototype.onMessagesReady = function (so) { this.setEnabled(true); }; // showMessage( ) is called from messages_so.showMessage( ) when text chat arrives. Chat.prototype.showMessage = function (msg) { this.chat_txt.text += msg + "\n"; this.chat_txt.scroll = this.chat_txt.maxscroll; }; // sendMessage( ) uses the messages_so shared object to send( ) a message. Chat.prototype.sendMessage = function ( ) { var msg = trim(this.send_txt.text); if (msg == "") { return; } this.send_txt.text = ""; this.messages_so.send("showMessage", this.userName + ": " + msg); }; #endinitclip send_pb.setClickHandler("sendMessage", this); send_txt.enterChar = String.fromCharCode(Key.ENTER); send_txt.onChanged = function ( ) { var lastChar = this.text.charAt(Selection.getEndIndex( ) - 1); if (lastChar == this.enterChar) { sendMessage( ); } }; If you read through the code, you'll see that the Chat class's init( ) method sets up both the messages_so and the userList_so shared objects. The earlier "Broadcasting Remote Method Calls with send( )" section describes how the messages_so shared object is used to implement a text chat. The userList_so shared object's onSync( ) method is very simple: this.userList_so.onSync = function (list) { this.owner.redrawPeopleList(this, list); this.owner.displayStreams(this, list); }; It calls the Chat object's redrawPeopleList( ) and displayStreams( ) methods and passes a reference to the shared object and the information list to them. The displayStreams( ) method examines each information object in the list array looking for code values of "change" or "delete". When a user connects to the chat, her username is added to the user list by the server-side script and deleted when her client disconnects. After each of these events, the userList_so.onSync( ) method is called. If the code value of an information object is "delete", then a user has left the chat application. If a StreamViewer movie clip for that user already exists, it must be deleted: var obj = list[p]; if (obj.code == "delete" && this[obj.name]) { this[obj.name].removeMovieClip( ); this.streamClipCount--; } Each StreamViewer movie clip is named after the unique username that each person logged in with and is attached to the Chat movie clip. Since, in this case, obj.name contains the username, this[obj.name] returns a reference to a StreamViewer movie clip if it exists. When the information object's code value is "change", a user has logged in. However the code checks whether the user is someone connecting from another Flash movie or is the person logged in using this Flash movie: var name = obj.name; if (name == this.userName) { this.displayStreamController(name, so.data[name], x, y); } else { this.displayStreamViewer(name, so.data[name], x, y); } If the user connected via another Flash movie, a StreamViewer movie clip is attached to the Chat movie clip by the displayStreamViewer( ) method. Otherwise, we're dealing with the user who logged in with this Flash movie, so a StreamController movie clip is attached to the Chat clip. In either case, the relative URI to the stream will be retrieved from the user list shared object and used by the StreamViewer to subscribe to the stream or by the StreamController to control the stream: so.data[name] 8.11.1. Designing with Shared ObjectsIn this chapter, you've seen two related ways to design applications using shared objects in some detail. In the shared ball example, the SharedBall class uses a shared object to share its x and y coordinates with other instances of itself in other movies. In that case, shared objects simply extend the idea of containing data within an object to containing data within the same object running in several clients . Internal data in an object determines the current state of an object. The shared ball position is an example of object state, and shared objects provide a mechanism for manipulating the state of every ball object in every movie. The user list shared object does more than extend data within an object into instances of the object in several Flash movies. When the user list is updated in one place (on the server), the information in it is used by more than one object. When data in the user list changes, the people list in every client is updated and stream viewers are created or deleted. |