Section 17.2. Bandwidth

17.2. Bandwidth

Bandwidth is often described as the carrying capacity of the network between two nodes. It is usually measured in bits per second, and the usual multiples of kilo (K) or mega (M) are often used. The multiples when referring to bits per second in a network refer to 1000 and 1,000,000 bits, respectively. A kilobit (1 Kb) is 1000 binary digits and a megabit (1 Mb) is 1000 kilobits or 1,000,000 bits.

Programmers often prefer to work with bytes instead of bits because bytes are the typical unit of data stored and retrieved in a computer. Since each byte contains 8 bits, a kilobit is equal to 125 bytes. To translate from bits to bytes, divide by 8; to go from bytes to bits, multiply by 8. However, byte prefixes such as kilo and mega refer to powers of 2. So 1 kilobyte (KB) contains 1024 bytes or 8192 bits. To translate between bits and kilobytes or megabytes is therefore a little more complicated. One method is to take the number of bits and divide by 8 to find the number of bytes. Divide the result by 1024 to get kilobytes. To get megabytes, divide kilobytes by 1024. Table 17-2 shows some common bandwidth values for an ideal connection.

The actual throughput of a network connection can be much less than, but will never match, the ideal figure in Table 17-2. And a given connection's throughput can vary significantly over time due to changing network and usage conditions. Some of the many factors affecting actual throughput are described at http://www. symantec .com/smallbiz/library/need.html.


Table 17-2. Bandwidth

Connection type

Ideal kilobits/sec (Kbps)

Ideal kilobytes/sec (KB/sec)

56 Kbps modem

56

6.8

1 Mbps ADSL

1000 downstream; 300 upstream

122.1 downstream; 37 upstream

1 Mbps SDSL

1000

122.1

3 Mbps DSL

3000

366.2

10 Mbps LAN

10,000

1220.7 (1.19 MB/sec)

100 Mbps LAN

100,000

122207.0 (11.92 MB/sec)


Network equipment and communication systems are described by the highest bandwidth they can achieve under ideal conditions. For example, a 100 Mbps network card in full-duplex mode is supposed to be able to simultaneously send and receive 100 megabits of data per second. In everyday use, the network card will not be able to achieve its full 100 Mbps rating due to the limitations of the computer it is installed in and network overhead. However, a more serious constraint is that the same computer with a 100 Mbps network card may be connected via ADSL (asymmetric DSL) to an ISP that provides only 1 Mbps downstream bandwidth and 300 Kbps upstream bandwidth. Even these figures are optimistic. Noise on the lines that connect the computer to the ISP's network may reduce bandwidth to much lower levels. It is not uncommon that a home with a 1 Mbps downstream connection will achieve only 400 Kbps or less downstream bandwidth.

Things get worse if the networks available between one computer (node) and another one on the Internet are congested. One node may realize throughput of 320 Kbps when downloading data from one server but a much lower throughput when connecting to another server. The slowest part of the network will determine the overall throughput. Another factor to consider is that available bandwidth changes over timesometimes quite rapidlyas networks become more or less congested . A network connection to an ISP that often achieves 300 Kbps can easily fluctuate over a few minutes between 40 Kbps and 100 Kbps.

17.2.1. Adapting to Bandwidth

Let's consider a scenario in which four participantseach on an ADSL connection with 400 Kbps downstream bandwidth and 150 Kbps upstreamare participating in a video conference. Each participant's camera is set to the default values of a 160 x 120 frame size , 15 fps, and a bandwidth limit of 16 KB/sec. Each microphone is set to its default 8 kHz sampling rate. If each person were constantly moving in front of the camera and always speaking, each could produce more than 16 KB of video data and send more than 2 KB of audio data per second. However, on average, a less-animated participant might produce about 16 KB of audio and video data combined. In other words, each person will send 132 Kbps to the server. Each participant's connection may be able to send that much data but must also receive the three streams from the other participants. With four participants , each will need to receive about 396 Kbps. This is barely manageable with their current bandwidth. Even if two more ADSL users join the video conference, all will not be lost. Each of the six participants will need to receive 5 times 132 Kbps or about 660 Kbps. Since each has only 400 Kbps downstream bandwidth, FlashCom will drop the number of video frames it sends to each participant as necessary in order to maintain audio quality. Also, many applications allow users to turn off some or all video streams if video quality deteriorates.

Now consider what happens when someone using a dial-up modem attempts to join the video conference. Even with a 56 Kbps modem, most dial-up users will get no more than 30 Kbps downstream and upstream bandwidth. Not nearly enough to send or receive a single 132 Kbps stream containing both audio and video. A single, audio-only stream will consume 2 KB/sec (16 Kbps), more than half of the user's upstream bandwidth. The user with a 56 Kbps modem has no hope of receiving four audio streams, which require 16 Kbps each or 64 Kbps total, twice the modem's realistic throughput. In this case, the only hope for a good user experience is to cut off all video and attempt to reduce the amount of audio coming from each participant who is not speaking. We've already seen in Chapter 5 and Chapter 6 how to control sending and receiving audio and video. But how do we know what capacity each client has?

17.2.2. Measuring Bandwidth

On the server, each Client object's getStats( ) method can be used to determine the number of bytes received from the client and the number of bytes sent out to the client since the client connected. Table 17-3 describes the information returned by getStats( ) . Provided enough data has been sent or received, the information returned by getStats( ) also includes the outbound and inbound bandwidth of the client.

Using setInterval( ) on the server, it is possible to regularly monitor the bandwidth consumption of the client over time, as shown in Example 17-4.

Table 17-3. The properties of the information object returned by the client object's getStats( ) method

Information object property

Description

audio_queue_bytes

The number of bytes currently in the outgoing audio queue.

audio_queue_msgs

The number of messages in the outgoing audio queue.

bw_in

The current client-to-server bandwidth being used in bytes/sec.

bw_out

The current server-to-client bandwidth being used in bytes/sec. Note: the value is calculated based on data delivered to the TCP stack and not data actually delivered to the client.

bytes_in

The number of bytes received from the client since it connected.

bytes_out

The number of bytes sent to the client since it connected.

data_queue_bytes

The number of bytes currently in the outgoing data queue.

data_queue_msgs

The number of data messages in the outgoing data queue.

dropped_audio_bytes

The number of audio bytes that could not be sent to the client.

dropped_audio_msgs

The number of audio messages that could not be sent to the client.

dropped_video_bytes

The number of video bytes that could not be sent to the client.

dropped_video_msgs

The number of video messages that could not be sent to the client.

msg_dropped

Total number of outgoing messages that have not been sent to the client including audio, video, shared object, and data messages.

msg_in

Total number of messages received from the client since it connected.

msg_out

Total number of messages sent to the client since it connected.

ping_rtt

The round-trip time (in milliseconds ) to send an empty message to the client and get it back.

so_queue_bytes

The number of bytes in the outgoing shared object queue.

so_queue_msgs

The number of messages in the outgoing shared object queue.

tunnel_bytes_in

Total number of bytes received from the client through the tunnel.

tunnel_bytes_out

Total number of bytes sent to the client through the tunnel.

tunnel_idle_requests

Current number of idle requests .

tunnel_idle_responses

Current number of idle responses.

tunnel_requests

Current number of tunnel requests.

tunnel_responses

Current number of tunnel responses.

video_queue_bytes

Current length of the outgoing video queue for this client in bytes.

video_queue_msgs

Current number of video messages in the outgoing queue.


In theory, the easiest way to check the actual client's bandwidth consumption is to use setInterval( ) on the server to regularly call getStats( ) and extract the bw_in and bw_out properties from the returned info object. The bandwidth values are updated at least after every 3 seconds, so a testing interval of 3 seconds is ideal. However, as of FlashCom version 1.5.2, the values returned are not correct if the actual bandwidth in or out drops to zero.

You can get a good calculation of the bandwidth being used over any time interval by using the bytes_in and bytes_out properties.

If you want to know the bandwidth used since the last time you checked, you can calculate it by keeping track of the number of milliseconds between tests and the change in the bytes_in and bytes_out properties. Example 17-4 traces out some basic bandwidth statistics every 2 seconds.

Example 17-4. Monitoring throughput over time
 Client.prototype.checkBandwidth = function (  ) {   var stats = this.getStats( );   var now = (new Date( )).getTime( );   var elapsedTime = now - this.lastTime;   // Elapsed time in milliseconds.   var deltaBytesOut = stats.bytes_out - this.lastBytesOut;   var deltaBytesIn  = stats.bytes_in  - this.lastBytesIn;   msg = "Bandwidth stats for client: " + this.ip + ":\n";   msg += (deltaBytesOut/elapsedTime)* 1000 + " downstream bytes per second.\n";   msg += (deltaBytesIn /elapsedTime)* 1000 + " upstream bytes per second.\n";   this.lastBytesOut = stats.bytes_out;   this.lastBytesIn  = stats.bytes_in;   this.lastTime = now;   trace(msg); }; application.onConnect = function (client) {   // Remember the initial time and bytes in and out.   client.lastTime = (new Date).getTime( );   var stats = client.getStats( );   client.lastBytesOut = stats.bytes_out;   client.lastBytesIn  = stats.bytes_in;   // Test the bandwidth every 2 seconds.   client.bwIntervalID = setInterval(client, "checkBandwidth", 2000);   return true; }; application.onDisconnect = function (client) {   clearInterval(client.bwIntervalID); }; 

While this code is somewhat useful for testing purposes, bandwidth detection and monitoring are more productively built into a component or object that can be used as needed. For example, see the server-side code of Macromedia's ConnectionLight component in the .../scriptlib/ components /connectionlight.asc file. The client.ping( ) method sends the client bandwidth statistics at a set interval.

Monitoring the bandwidth used by a client can be useful if you know the amount of data a client is trying to send or receive. For example, if a client is attempting to send 16 KB/sec of audio and video data but is actually sending only 4 KB/sec, then it should stop trying to send any video. However, while you can test the activity level of both the Camera and Microphone objects to determine if they are sending data, you cannot tell exactly how much data each is attempting to send.

Testing the maximum bandwidth available to a client may often be necessary. One approach to determining each client's bandwidth is to have the client and server attempt to send each other a large amount of data during a short test period and then measure the actual data sent and received during that time. Since the FlashCom Server and Flash Player will attempt to adapt to bandwidth limitations by dropping video frames and audio messages, it is difficult to use live audio or video to test bandwidth. What is needed is a reliable way to send a known amount of test data. Fortunately, ActionScript data is never dropped. It can be sent as a remote method invocation using the send( ) method of a shared object or stream or with the call( ) method of a NetConnection object. Since each client may have to be tested at a different time, the call( ) method is normally used (there is no point in broadcasting test data to more than one client at a time).

Macromedia has made available at least two different bandwidth measurement methods . The first one, the bwcheck utility, is available as two files named bwcheck.as and bwcheck.asc . They measure both downstream (server-to-client) bandwidth and upstream (client-to-server) bandwidth. The source files are available at:

http://www.peldi.com/archives/2004/01/automatically_c.html

The FLVPlayer project uses bandwidth detection code that is optimized to return the downstream bandwidth to the Flash Player in less time than the bwcheck utility. You can download the FLVPlayer source here:

http://www.peldi.com/blog/archives/2004/05/fvplayer_new_r.html

Look at the main.asc file in the FLVPlayer distribution, especially the calculateClientBw( ) function.

Both the bwcheck utility and FLVPlayer use a similar technique to measure bandwidth. The basic steps are:

  1. Determine latency.

  2. Get the current time and the current number of bytes in or out for the client.

  3. Send a sequence of messages containing a large text string or array of random numbers from client to server or server to client, but test in only one direction at a time.

  4. After a certain amount of time or when all the messages have been received, get the current time and the current number of bytes in or out for the client.

  5. Calculate the number of bytes in or out during the test period.

  6. Calculate the test duration and subtract the latency time.

  7. Divide the number of bytes by the adjusted time to get bytes/sec.

There are a number of challenges in making the test work. In bwcheck and the initial version of FLVPlayer, a text string is used. For example, to create a text string that is 1 KB (1024 bytes) in length, you can use:

 sampleData = "12345678"; for (var i = 0; i < 7; i++) sampleData += sampleData; 

The data sent cannot be too small or too large. If the data is too small, the time it takes to send it will be not much different than the results of a simple latency test. For example, if the round-trip time for a few bytes of data over a connection is 70 ms, and it takes only 70 ms to send a 1 KB string, then it is impossible to measure bandwidth because the larger sample size had no measurable effect on transmission time. Even if the string took 80 ms and the round-trip latency was 70 ms, the 10 ms difference is statistically insignificant.

Round-trip measurements are always used because after the data is sent, the only way to know it has arrived at the other end is to wait for an acknowledgment from the receiver.


The test string has to be large enough that it takes much longer than the latency round-trip time to transmit it and get back an acknowledgment of receipt. However, if the string is too large, the test will take too long and risk alienating the user. The problem is compounded by the fact that a bandwidth test has to work for high-bandwidth low-latency connections, high-bandwidth high-latency connections, low-bandwidth connections, and everything in between.

The initial FLVPlayer dealt with the problem by sending strings in separate messages of constantly increasing size. Each string is twice the size of the previous one. It will not send more than six messages in less than 2 seconds. The test is over and the results are calculated after acknowledgment of receipt of the last string is received.

The most recent version of FLVPlayer repeatedly sends a 16 KB array of random numbers instead of strings. It was changed because the older tests with repeating string patterns produced inflated bandwidth numbers under some circumstances. Some modems and virtual private networks (VPNs) automatically compress data before sending it over the network. The earlier string patterns were highly compressed compared with typical compression rates for audio and video streams. In my own initial tests, sending an array of random numbers improves , but does not completely remove, the problem of inflated bandwidth numbers when compression is used. FlashCom bandwidth testing methods have been evolving since the first bwcheck code was released and can be expected to improve further over time.

The latency and bandwidth tests in the bwcheck and FLVPlayer code employ slightly different measurement algorithms and are not designed as a general purpose component. Even if they were, they would have to be adapted to provide the type of information required by different applications.

If someone has already written a bandwidth tester, why do you need to know the technical details? So you can write your own of course! This isn't merely an exercise in geekdom. Your specific configuration will typically allow or require a more specific test than the generic ones publicly available.

For my purposes, I created a ConnectionTester component. The source is available on the book's web site. It is a derivation of the algorithm used in FLVPlayer's main.asc file. Instead of setting the maximum number of messages at an arbitrary number and time limit, ConnectionTester sends increasingly large messages until the total test time exceeds twice the latency round-trip time. Then, four more increasingly large messages are sent. When receipt of the last message is acknowledged , the bandwidth is calculated. The ConnectionTester tests both downstream and upstream bandwidth, latency, and the difference in milliseconds between the client and server's clocks. The information is both sent back to the client and stored in a shared object. The ConnectionTester class does very little of the actual testing. Instead, it coordinates the work of three other objects: DownstreamBWTester , UpstreamBWTester , and RMILatencyTester . Example 17-5 lists the source for the DownstreamBWTester class.

Example 17-5. The DownstreamBWTester class
 function DownstreamBWTester (owner, client, rtt) {   this.owner = owner;   this.client = client;   this.rtt = rtt;   this.minMessageDelta = rtt * 2 + 10;   this.messageCount = 0;   this.maxMessages = 4;   this.validMessages = 0;   this.sendCount = 0 ;   // Create a 512 byte length string.   this.sampleData = "12345678";   for (var i = 0; i < 6; i++) {     this.sampleData += this.sampleData;   }   this.startStats = client.getStats( );   this.startStats.startTime = (new Date).getTime( );   this.sendData( ); // 1K   this.sendData( ); // 2K   this.sendData( ); // 4K } DownstreamBWTester.prototype.sendData = function ( ) {   this.sendCount++;   this.sampleData += this.sampleData;   this.client.call(this.owner.path + "/echoRequest", this, this.sampleData); }; DownstreamBWTester.prototype.onResult = function (info) {   var receiveTime = (new Date).getTime( );   this.messageCount++;   var delta = receiveTime - this.startStats.startTime;   var pendingMessageCount = this.sendCount - this.messageCount;   if (delta > this.minMessageDelta) {     this.validMessages++;     var requiredMessages = this.maxMessages - this.validMessages;     if (requiredMessages > pendingMessageCount) {       this.sendData( );     }   }   else if (pendingMessageCount < 3) this.sendData( );   if (this.sendCount - this.messageCount == 0) {     var endStats = this.client.getStats( );     var startStats = this.startStats;     var deltaBytes = endStats.bytes_out - startStats.bytes_out;     var deltaTime = receiveTime - startStats.startTime - this.rtt;     var bytesPerSecond = 1000 * deltaBytes/deltaTime;     this.owner.onDownstreamBW(this.client, bytesPerSecond);   } }; 

A DownstreamBWTester instance is created by the ConnectionTester component when it needs to test a client's downstream bandwidth:

 var proxy = this.getProxy(client); proxy.downstreamBWTester = new DownstreamBWTester(this, client, rtt); 

At the beginning of the test, the client.getStats( ) method is used to get the amount of data the client has already been sent before the test started:

 this.startStats = client.getStats(  ); this.startStats.startTime = (new Date).getTime(  ); 

When the test is over, client.getStats( ) is called again to get the total amount of data the client has been sent since connecting. The difference in bytes out between the beginning and end of the test is calculated, as is the elapsed test time:

 var endStats = this.client.getStats(  ); var startStats = this.startStats; var deltaBytes = endStats.bytes_out - startStats.bytes_out; var deltaTime = receiveTime - startStats.startTime - this.rtt; 

Since the time difference is calculated in milliseconds, we multiply the bytes/ms by 1000 to gets bytes/sec:

 var bytesPerSecond = 1000 * deltaBytes/deltaTime; 

You can tweak my ConnectionTester's parameters to suit your needs or use it as the basis for a custom latency and bandwidth measuring component of your own. The current version uses a repeating string pattern. Future versions will use different test data to reduce the effects of modem and VPN compression and will be available from the book's web site.

17.2.3. Latency, Bandwidth, and Performance

Latency is the minimum time to deliver each packet and is unrelated to the capacity or bandwidth of the network. However, as a network becomes congested, it will drop more and more packetseach of which will have to be resentso that the average time to deliver packets will increase. Therefore, when designing and tuning applications for performance, both latency and bandwidth need to be accounted for. There is no point in sending messages more frequently than the network's ability to deliver them, and there is no point in trying to send more data than the network can handle. In fact, both are counterproductive.

You should minimize the total number of messages that every client sends or receives and their size. Send the minimum data necessary and batch messages together when possible.


Sending too many, too large messages via send( ) and call( ) will fill up each application's message queue and slow down the responsiveness of an application instance's ActionScript thread. Frequently sending many small messages will increase the total overhead of the network and server and therefore reduce performance. Sending a batch of messages less frequently will improve performance. A good example of minimizing the load on the network and server is the way the PFCSLogger class, introduced in Chapter 11, caches log messages and then sends them in batches rather than sending each one immediately. A number of techniques can be of use in increasing performance by reducing the frequency and size of data being sent.

17.2.3.1 Latency and SharedObject updates

The frequency of SharedObject updates can often be decreased even further than described in Section 8.3 in Chapter 8. Consider an application that must work well for users with latencies of up to 200 ms. Since an acknowledgment is required for each shared object update, 400 ms is the effective frame rate. That's just 2.5 fps! So it makes sense to set the frame rate on shared objects to 3 or 4 fps (using SharedObject.setFps( ) ) to reduce the number of messages that will be sent, even when local updates to the shared object are more frequent. For some things, like updates in business data, slower frame rates may be fine. For other purposes, such as sending real-time presence information or positioning a visible entity in a game, it will produce distracting and spasmodic motion that visibly lags behind the current state of the game. However, there are techniques that can smooth motion when updates are infrequent, and even ways to extrapolate motion between updates that often make reducing update frequency practical and, therefore, improve overall application performance.

To illustrate smoothing and extrapolating motion between updates, I'll use the PersonalCursors component first introduced in Chapter 15 as a demonstration platform. When a user moves his cursor, an onMouseMove event is generated. When the mouse is moving, events may occur as often as every 5 milliseconds. For the owner of the local cursor, the hardware cursor will move smoothly on the screen. However, if the mouse position in a shared object is updated at only 3 or 4 fps, remote users will see the cursor jerk and jump discontinuously. One solution is to slow the remote cursor down! If some additional delay can be tolerated, a series of intermediate steps between its previous position and the current position in the shared object can be calculated so that the mouse moves smoothly between steps.

My version, the full version of which is available on the book's web site, is based on a presentation at the 2004 Flash in the Can Festival by Brian Robbins of Fuel Industries. The sample files from his presentation are available at:

http://www.dubane.com/cons

Example 17-6 shows the source code for the update( ) method of my InterpolatedPersonalCursor movie clip subclass that implements a very simple motion-smoothing algorithm.

Example 17-6. InterpolatedPersonalCursor.update( ) method
 function update (  ) {   var requiresUpdate = false;   var cursor = so.data[userName];   var delta_x = cursor.x - _x;   if (Math.abs(delta_x) > maxTravel) {     if (delta_x > 0) _x += maxTravel;     else _x -= maxTravel;     requiresUpdate = true;   }   else {     _x = cursor.x;   }   var delta_y = cursor.y - _y;   if (Math.abs(delta_y) > maxTravel) {     if (delta_y > 0) _y += maxTravel;     else _y -= maxTravel;     requiresUpdate = true;   }   else {     _y = cursor.y;   }   if (requiresUpdate) {     if (!onEnterFrame) onEnterFrame = enterFrameHandler;   }   else {     onEnterFrame = null;   } } 

The update( ) method is called whenever the position of the cursor it represents has been updated in the so shared object. The movie clip is allowed to travel no more than the number of pixels specified by maxTravel . If that means the clip will not reach its position in the shared object, it arranges to have update( ) called for every onEnterFrame event until it reaches the position specified in the shared object.

When update( ) is called, the distance between the x coordinate in the shared object and the current _x position of the movie clip is calculated:

 var delta_x = cursor.x - _x; 

If the absolute value of the distance is greater than maxTravel , the movie clip is moved maxTravel pixels to the left or right. Otherwise, it is moved directly to the x coordinate in the shared object:

 if (Math.abs(delta_x) > maxTravel) {   if (delta_x > 0) _x += maxTravel;   else _x -= maxTravel;   requiresUpdate = true; } else {   _x = cursor.x; } 

The same process is repeated for the y coordinate. If either value has been constrained to move only maxTravel pixels, an onEnterFrame( ) handler that simply calls update( ) again on the next frame is set up:

 if (requiresUpdate) {   if (!onEnterFrame) onEnterFrame = enterFrameHandler; } else {   onEnterFrame = null; } 

When the next frame is reached, the movie clip will again be moved no more than maxTravel pixels. When the clip is moved to its coordinates in the shared object, the onEnterFrame property is set to null and the position of the cursor movie clip will not be updated again until the coordinates in the cursor's slot in the shared object change. For the PersonalCursors component, the additional delay is not a problem because the user also sees a movie clip of her cursor following her hardware cursor and so has a good idea what other people are seeing. Other more sophisticated animation algorithms can be used to smooth motion. See Robert Penner's easing equations available at:

http://robertpenner.com

In multiuser computer games , lag is the enemy of every game developer. Instead of delaying visible changes between updates, an approach is required to try to predict an entity's position before the next update arrives. In Flash, that means each movie clip that represents a moving game entity must be repositioned when each onEnterFrame event occurs. Each entity must continue moving rather than stop to wait for the next update to arrive . If the prediction of movement is slightly off, the entity's position can be corrected when the next update arrives. Extrapolating the position of entities in between updates is often done using a dead reckoning algorithm . Dead reckoning was originally employed to improve military simulations. Jesse Aronson's article at Gamasutra provides a good overview of dead reckoning (free membership required):

http://www.gamasutra.com/features/19970919/aronson_01.htm

Dead reckoning works best for game entities such as tanks and aircraft that behave according to a set of predictable physical rules. For example, while a jet can change direction, it cannot suddenly stop in midair and reverse direction. A simple version of dead reckoning works to minimize the number of updates sent on the network this way:

  1. The current position, velocity, acceleration, orientation, and other attributes of an entity under the control of a user are sent to all the other game or simulation clients .

  2. When the information arrives at each client, the entity's position is moved to the position specified in the message.

  3. At regular intervals, the change in time between the last position update and the current time is calculated, and each entity's position is extrapolated based on the entity's orientation, velocity, and acceleration. Then, the entity is moved to the extrapolated position. As time proceeds, each entity's position is continually extrapolated until another update arrives.

  4. An owner of an entity receives back its own messages about its entity and also uses them to extrapolate its position. When the owner changes the direction of his entity, the extrapolated position and the real position will diverge. When they diverge by a certain threshold, the system sends a new message with updated position and movement information to every client.

In other words, dead reckoning is designed to send updates only when the real entity's position is significantly different from its extrapolated position.

Shared object updates work a little differently. If a slot in a shared object is constantly changed, the shared object will attempt to send updates at the regular interval defined by its frame rate. However, it is not difficult to adapt dead reckoning to work with shared objects with a fixed update interval. One technique is to regularly update the shared object with position and velocity information. To return to the PersonalCursors component as an example, whenever an onMouseMove event is received, the shared object is updated with the mouse's _x and _y position and its velocity in x and y. When the mouse stops moving, each velocity value will be set to zero. When other clients receive updates, they will move a cursor movie clip to the position defined in the update. On every subsequent frame, they will use the position and velocity as well as the time elapsed since the last update to extrapolate a position for the cursor and move it accordingly . Example 17-7 shows the source code for the DeadReckoningPersonalCursor class.

Example 17-7. The DeadReckoningPersonalCursor class definition
 class com.oreilly.pfcs.framework.components.DeadReckoningPersonalCursor    extends MovieClip {   // Connect component class and symbol.   var className:String = "DeadReckoningPersonalCursor";   static var symbolName:String = "DeadReckoningPersonalCursor";   static var symbolOwner:Object = DeadReckoningPersonalCursor;   // initObject variables.   var homeInstance:Boolean; // Indicates if this is the user's cursor.   var userName:String;      // The user's userName and slot name for this cursor.   var so:SharedObject;      // Shared object to update with the cursor's position.   var lastPos:Object;            // Previous position of the mouse.   var lastTime:Number;           // Last time the mouse position was determined.   var lastUpdateTime:Number = 0; // Last time the shared object changed.   var intervalID;                // To get a mouse position after it stops.   // Subcomponents and movie clips.   var userName_txt:TextField;   function DeadReckoningPersonalCursor ( ) {     super( );     // If this instance belongs to the user.     if (homeInstance) {       lastTime = getTimer( );       onMouseMove = onMouseMoveHandler;       onEnterFrame = homeEnterFrameHandler;       // Set initial position in the shared object:       var cursorRecord = {x: _root._xmouse - _parent._x,                           y: _root._ymouse - _parent._y,                           vx: 0, vy: 0, ax: 0, ay: 0}       lastPos = cursorRecord;       so.data[userName] = cursorRecord;     }     else {       onEnterFrame = enterFrameHandler;     }   }   function onLoad ( ) {     userName_txt.text = userName;   }   function onMouseMoveHandler ( ) {     clearInterval( intervalID );     var now = getTimer( );     var dt = now - lastTime;     if (dt < 2) return;     lastTime = now;     var current = {x: _root._xmouse - _parent._x,                    y: _root._ymouse - _parent._y,                    vx: 0, vy: 0, ax: 0, ay: 0}     if (!lastPos) return;     current.vx = (current.x -lastPos.x)/dt;     current.vy = (current.y -lastPos.y)/dt;     current.ax = (current.vx -lastPos.vx)/dt;     current.ay = (current.vy -lastPos.vy)/dt;     lastPos = current;     so.data[userName] = current;     intervalID = setInterval( this, "onMouseMove", dt*2 );   }   function homeEnterFrameHandler ( ) {     _x = _root._xmouse - _parent._x;     _y = _root._ymouse - _parent._y;   }   function enterFrameHandler ( ) {     var now = getTimer( );     var dt = now - lastUpdateTime;     if (dt < 5) return;     var lastUpdate = so.data[userName];     if (!lastUpdate) return;     if ((Math.abs(lastUpdate.vx) < 0.1) && (Math.abs(lastUpdate.vy) < 0.1)) {       _x = lastUpdate.x;       _y = lastUpdate.y     }     else {       _x = lastUpdate.x + lastUpdate.vx * dt ;       _y = lastUpdate.y + lastUpdate.vy * dt;     }   }   function update ( ) {     var cursorRecord = so.data[userName];     _x = cursorRecord.x;     _y = cursorRecord.y;     lastUpdateTime = getTimer( );   }   function deleteClip ( ) {     Mouse.removeListener(this);     delete so.data[userName];     this.removeMovieClip( );   } } 

The onMouseMoveHandler( ) method is assigned to the onMouseMove( ) method of the clip if it is the owner's instance. Whenever the mouse moves, it determines its current local position within its parent clip, calculates its velocity and acceleration, and places all the information in an object inside its shared object slot. The property names are _x and _y for position, vx and vy for velocity, and ax and ay for acceleration. Putting all the properties in a single object increases bandwidth because the entire object is sent when any property changes. However, we assume that most properties are changing simultaneously; even if they are not, updating them in one bunch at low frame rates such as 3 fps seems reasonable. You are welcome to customize the component to transmit the properties in separate slots rather than combined into one object.

The code also starts a timer, so the onMouseMove( ) method will be called again after the mouse stops. Otherwise, the cursor would keep moving indefinitely, as it is assumed to continue along an extrapolated path.

The update( ) method is called whenever the shared object is updated and moves the movie clip cursor to the position specified in the shared object. It also remembers the time of the last update.

The enterFrameHandler( ) is designed to be assigned to the onEnterFrame( ) method if the clip represents a cursor that is not owned by the current user. For each onEnterFrame event, the elapsed time since the last shared object update is calculated:

 var now = getTimer(  ); var dt = now - lastUpdateTime; 

If enough time has elapsed since the last shared object update, it extrapolates a new position for the clip and moves it with these two statements:

 _x = lastUpdate.x + lastUpdate.vx * dt ; _y = lastUpdate.y + lastUpdate.vy * dt; 

The statements do not use acceleration. However, depending on the application, acceleration can also be used:

 var halfdtdt = 0.5 * dt * dt; _x = lastUpdate.x + lastUpdate.vx * dt + lastUpdate.ax * halfdtdt; _y = lastUpdate.y + lastUpdate.vy * dt + lastUpdate.ay * halfdtdt; 

If you try out the DeadReckoningPersonalCursor class, you will see that, when you move the mouse relatively slowly and without very sudden changes in direction, the remote versions of the cursor will follow it remarkably well and with a much better feeling of simultaneity. On the other hand, if you move the cursor quickly and with sharp changes in direction, the remote cursor will often noticeably diverge from the real cursor's position. When the real cursor changes direction sharply, the difference is most noticeable as the remote cursor overshoots the inflection point and then snaps back to the correct position. Large errors are a problem with rapidly changing entities but, in some cases, can be compensated for by enforcing other rules on each entity. For example, a vehicle should not be able to "overshoot" and move through an impenetrable wall or drive at the same speed on the shoulder of a road as it can on a paved road.

Regardless of dead reckoning's accuracy, the updates that travel from the owner's client to the server and on to everyone else's clients still arrive some time after they were sent, so each entity still lags behind its owner's copy. We'll return to this problem in the next section.



Programming Flash Communication Server
Programming Flash Communication Server
ISBN: 0596005040
EAN: 2147483647
Year: 2003
Pages: 203

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net