diff --git a/tests/playback.js b/tests/playback.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ce1210c25c1319bc43d9346eea8cb1698de5cef
--- /dev/null
+++ b/tests/playback.js
@@ -0,0 +1,79 @@
+var rfb, mode, test_state, frame_idx, frame_length,
+    iteration, iterations, istart_time;
+
+// Override send_array
+send_array = function (arr) {
+    // Stub out send_array
+}
+
+function next_iteration () {
+    var time, iter_time, end_time;
+
+    if (iteration === 0) {
+        frame_length = VNC_frame_data.length;
+        test_state = 'running';
+    } else {
+        rfb.disconnect();
+    }
+    
+    if (test_state !== 'running') { return; }
+
+    iteration++;
+    if (iteration > iterations) {
+        finish();
+        return;
+    }
+
+    frame_idx = 0;
+    istart_time = (new Date()).getTime();
+    rfb.connect('test', 0, "bogus");
+
+    queue_next_packet();
+
+}
+
+function queue_next_packet () {
+    var frame, now, foffset, toffset, delay;
+    if (test_state !== 'running') { return; }
+
+    frame = VNC_frame_data[frame_idx];
+    while ((frame_idx < frame_length) && (frame.charAt(0) === "}")) {
+        //Util.Debug("Send frame " + frame_idx);
+        frame_idx += 1;
+        frame = VNC_frame_data[frame_idx];
+    }
+
+    if (frame === 'EOF') {
+        Util.Debug("Finished, found EOF");
+        next_iteration();
+        return;
+    }
+    if (frame_idx >= frame_length) {
+        Util.Debug("Finished, no more frames");
+        next_iteration();
+        return;
+    }
+
+    if (mode === 'realtime') {
+        foffset = frame.slice(1, frame.indexOf('{', 1));
+        toffset = (new Date()).getTime() - istart_time;
+        delay = foffset - toffset;
+        if (delay < 1) {
+            delay = 1;
+        }
+
+        setTimeout(do_packet, delay);
+    } else {
+        setTimeout(do_packet, 1);
+    }
+}
+
+function do_packet () {
+    //Util.Debug("Processing frame: " + frame_idx);
+    frame = VNC_frame_data[frame_idx];
+    rfb.recv_message({'data' : frame.slice(frame.indexOf('{', 1)+1)});
+    frame_idx += 1;
+
+    queue_next_packet();
+}
+
diff --git a/tests/vnc_playback.html b/tests/vnc_playback.html
index 5f3af416e20c2c7f57e71cf4e8beb40b96d2adb4..1d070079fc06f77d4d3d06fc85264be760f69398 100644
--- a/tests/vnc_playback.html
+++ b/tests/vnc_playback.html
@@ -5,13 +5,20 @@
     </head>
     <body>
 
-        Iterations: <input id='iterations' style='width:50' value="3">&nbsp;
+        Iterations: <input id='iterations' style='width:50'>&nbsp;
+        Perftest:<input type='radio' id='mode1' name='mode' checked>&nbsp;
+        Realtime:<input type='radio' id='mode2' name='mode'>&nbsp;&nbsp;
 
         <input id='startButton' type='button' value='Start' style='width:100px'
             onclick="start();" disabled>&nbsp;
 
         <br><br>
 
+        Results:<br>
+        <textarea id="messages" style="font-size: 9;" cols=80 rows=25></textarea>
+
+        <br><br>
+
         <div id="VNC_screen">
             <div id="VNC_status_bar" class="VNC_status_bar" style="margin-top: 0px;">
                 <table border=0 width=100%><tr>
@@ -23,9 +30,6 @@
             </canvas>
         </div>
 
-        <br>
-        Results:<br>
-        <textarea id="messages" style="font-size: 9;" cols=80 rows=25></textarea>
     </body>
 
     <!--
@@ -34,21 +38,19 @@
     -->
 
     <script src="include/vnc.js"></script>
+    <script src="playback.js"></script>
 
     <script>
-        var rfb, fname, test_state, frame_idx, frame_length, iteration,
-            iterations, start_time, packetID, waitTimer;
+        var fname, start_time;
 
         function message(str) {
             console.log(str);
-            cell = $('messages');
+            var cell = $('messages');
             cell.innerHTML += str + "\n";
             cell.scrollTop = cell.scrollHeight;
         }
 
-        fname = (document.location.href.match(
-                 /data=([A-Za-z0-9\._\-]*)/) ||
-                 ['', ''])[1];
+        fname = Util.getQueryVar('data', null);
 
         if (fname) {
             message("Loading " + fname);
@@ -57,11 +59,6 @@
             message("Must specify data=FOO in query string.");
         }
 
-        // Override send_array
-        send_array = function (arr) {
-            // Stub out send_array
-        }
-
         updateState = function (rfb, state, oldstate, msg) {
             switch (state) {
                 case 'failed':
@@ -69,7 +66,7 @@
                     message("noVNC sent '" + state + "' state during iteration " + iteration);
                     test_state = 'failed';
                     break;
-                case 'loaded': 
+                case 'loaded':
                     $('startButton').disabled = false;
                     break;
             }
@@ -78,90 +75,53 @@
             }
         }
 
-        function start () {
+        function start() {
             $('startButton').value = "Running";
             $('startButton').disabled = true;
-            test_state = 'running';
 
             iterations = $('iterations').value;
             iteration = 0;
-            frame_length = VNC_frame_data.length;
-            total_time = 0;
             start_time = (new Date()).getTime();
 
-            setTimeout(next_iteration, 1);
-        }
-
-        function next_iteration () {
-            var time, iter_time, end_time;
-
-            if (test_state !== 'running') { return; }
-
-            if (iteration !== 0) {
-                rfb.disconnect();
-            }
-            
-            iteration++;
-            if (iteration > iterations) {
-                // Finished with all iterations
-                var end_time = (new Date()).getTime();
-                total_time = end_time - start_time;
-
-                iter_time = parseInt(total_time / iterations, 10);
-                message(iterations + " iterations took " + total_time + "ms, " +
-                        iter_time + "ms per iteration");
-                rfb.get_canvas().stop();   // Shut-off event interception
-                $('startButton').disabled = false;
-                $('startButton').value = "Start";
-                return;
+            if ($('mode1').checked) {
+                message("Starting performance playback (fullspeed) [" + iterations + " iteration(s)]");
+                mode = 'perftest';
+            } else {
+                message("Starting realtime playback [" + iterations + " iteration(s)]");
+                mode = 'realtime';
             }
 
-            frame_idx = 0;
-            rfb.connect('test', 0, "bogus");
-
-            setTimeout(do_packet, 1);
-
+            next_iteration();
         }
 
-        function do_packet () {
-            var frame;
-            if (test_state !== 'running') { return; }
-
-            frame = VNC_frame_data[frame_idx];
-            while (frame.charAt(0) === "}") {
-                //message("Send frame " + frame_idx);
-                frame_idx ++;
-                frame = VNC_frame_data[frame_idx];
-                if (frame_idx >= frame_length) {
-                    break;
-                }
-            }
+        function finish() {
+            // Finished with all iterations
+            var total_time, end_time = (new Date()).getTime();
+            total_time = end_time - start_time;
 
+            iter_time = parseInt(total_time / iterations, 10);
+            message(iterations + " iterations took " + total_time + "ms, " +
+                    iter_time + "ms per iteration");
+            rfb.get_canvas().stop();   // Shut-off event interception
+            $('startButton').disabled = false;
+            $('startButton').value = "Start";
 
-            //message("Processing frame: " + frame_idx);
-            if (frame) {
-                if (frame === 'EOF') {
-                    //message("Found EOF");
-                } else {
-                    rfb.recv_message({'data' : frame.slice(frame.indexOf('{', 1)+1)});
-                }
-                frame_idx++;
-            }
-
-            if (frame_idx >= frame_length) {
-                next_iteration();
-            } else {
-                setTimeout(do_packet, 1);
-            }
         }
 
         window.onload = function() {
+            iterations = Util.getQueryVar('iterations', 3);
+            $('iterations').value = iterations;
+            mode = Util.getQueryVar('mode', 3);
+            if (mode === 'realtime') {
+                $('mode2').checked = true;
+            } else {
+                $('mode1').checked = true;
+            }
             if (fname) {
                 message("VNC_frame_data.length: " + VNC_frame_data.length);
                 rfb = RFB({'target': 'VNC_canvas',
                         'updateState': updateState});
                 rfb.testMode(send_array);
-                rfb.init();
             }
         }
     </script>