diff --git a/20240425/yesvnc-wc-6.html b/20240425/yesvnc-wc-6.html
new file mode 100644
index 0000000000000000000000000000000000000000..e039f5ca712a2ae9526ec112428ef7b8ea74a997
--- /dev/null
+++ b/20240425/yesvnc-wc-6.html
@@ -0,0 +1,1282 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Strict//EN">
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf8">
+    <title>yesVNC Web Connector</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta charset="utf-8">
+    <link rel="stylesheet" href="stylesheets/yesvnc-wc.css" type="text/css">
+  </head>
+  <body>
+    <main class="main">
+      <header class="main__header">
+        <h2 class="main__header-title">yesVNC Web Connector</h2>
+        <h3 id="room-indicator" class="main__header-subtitle">
+          Channel&nbsp;6
+        </h3>
+      </header>
+      <hr class="main__seperator" />
+      <div id="preview-section" class="hidden">
+        <div id="preview-container">
+          <video id="video" style="display: none;" autoplay></video>
+          <canvas id="canvas0" style="display:none;"></canvas>
+          <canvas class="preview" id="canvas1"></canvas>
+        </div>
+        <hr class="main__seperator" />
+      </div>
+      <div class="main__controls">
+        <button id="start">Start</button>
+        <button id="stop" disabled>Stop</button>
+      </div>
+    </main>
+    <script>
+
+      const debugging = 0;
+
+      const combine_rects_x_min_distance = 24;
+      const combine_rects_y_min_distance = 4;
+
+      const scroll_detection_x_min = -64;
+      const scroll_detection_x_max = 64;
+      const scroll_detection_y_min = -64;
+      const scroll_detection_y_max = 64;
+      const scroll_detection_r_max = 16;
+      const scroll_detection_min_width = 256;
+      const scroll_detection_min_height = 256;
+      const scroll_detection_min_stripe_width = 16;
+      const scroll_detection_min_stripe_height = 16;
+      const scroll_detection_point_divisor = 1024;
+      const scroll_detection_length_divisor = 8;
+      const scroll_detection_same_color_threshold = 0.995;
+      const scroll_detection_hit_threshold = 0.995;
+
+      let video = document.getElementById ("video");
+      const canvas0 = document.getElementById ("canvas0");
+      const canvas1 = document.getElementById ("canvas1");
+      let ctx0 = undefined;
+      let ctx1 = undefined;
+
+      function hideElement (el)
+      {
+        el.classList.add ('hidden');
+      }
+
+      function showElement (el)
+      {
+        el.classList.remove ('hidden');
+      }
+
+      const displayMediaOptions = { video: { cursor: "always" }, audio: false };
+
+      async function startCapture ()
+      {
+        video.srcObject = await navigator.mediaDevices.getDisplayMedia (displayMediaOptions);
+        showElement (document.getElementById ('preview-section'));
+      }
+
+      function stopCapture (evt)
+      {
+        hideElement (document.getElementById ('preview-section'));
+        let tracks = video.srcObject.getTracks ();
+        tracks.forEach (
+          function (track)
+          {
+            track.stop ();
+          });
+        video.srcObject = null;
+      }
+
+      const startButton = document.getElementById ("start");
+      const stopButton = document.getElementById ("stop");
+
+      startButton.addEventListener ("click",
+        function (evt)
+        {
+          startCapture ();
+          startButton.setAttribute ('disabled', '');
+          stopButton.removeAttribute ('disabled');
+        },
+        false);
+
+      stopButton.addEventListener ("click",
+        function (evt)
+        {
+          stopCapture ();
+          start.removeAttribute ('disabled');
+          stopButton.setAttribute ('disabled', '');
+          window.location.reload();
+        },
+        false);
+
+      let socket = undefined;
+
+      function buf2hex (buffer)
+      {
+        const b = new Uint8Array (buffer);
+        const hexdump = Array.prototype.map.call (b,
+          function (x)
+          {
+            return ("00" + x.toString (16)).slice (-2);
+          }).join (" ");
+        return hexdump + " (" + buffer.byteLength.toString ()
+                       + (buffer.byteLength == 1 ? " byte)" : " bytes)");
+      }
+
+      let wScreen = 0;
+      let hScreen = 0;
+
+      const rfb_padding = 0;
+      const rfb_encoding_raw = 0;
+      const rfb_encoding_copyrect = 1;
+      const rfb_encoding_rre = 2;
+      const rfb_encoding_tight = 7;
+      const rfb_name = "wc:6";
+      const rfb_name_length = rfb_name.length;
+
+      class Rect
+      {
+        constructor (x, y, w, h, encoding)
+        {
+          this.x = x;
+          this.y = y;
+          this.w = w;
+          this.h = h;
+          this.encoding = encoding;
+        }
+
+        send ()
+        {
+          const buffer = new ArrayBuffer (12);
+          const b = new Uint8Array (buffer);
+          b[0] = this.x >>> 8;
+          b[1] = this.x & 0xff;
+          b[2] = this.y >>> 8;
+          b[3] = this.y & 0xff;
+          b[4] = this.w >>> 8;
+          b[5] = this.w & 0xff;
+          b[6] = this.h >>> 8;
+          b[7] = this.h & 0xff;
+          b[8] = this.encoding >>> 24
+          b[9] = (this.encoding >>> 16) & 0xff;
+          b[10] = (this.encoding >>> 8) & 0xff;
+          b[11] = this.encoding & 0xff;
+          if (debugging)
+            console.log ("sending Rect object: " + buf2hex (buffer));
+          socket.send (buffer);
+        }
+      }
+
+      class RawRect extends Rect
+      {
+        constructor (x, y, w, h, data)
+        {
+          super (x, y, w, h, rfb_encoding_raw);
+          this.data = data;
+        }
+
+        send ()
+        {
+          super.send ();
+          if (debugging)
+            console.log ("sending RawRect object data (" + this.data.byteLength.toString () + " pixel bytes)");
+          socket.send (this.data);
+        }
+      }
+
+      class ScrollRect extends Rect
+      {
+        constructor (x, y, w, h, dx, dy)
+        {
+          super (x, y, w, h, rfb_encoding_copyrect);
+          this.dx = dx;
+          this.dy = dy;
+        }
+
+        send ()
+        {
+          super.send ();
+          const buffer = new ArrayBuffer (4);
+          const b = new Uint8Array (buffer);
+          b[0] = this.dx >>> 8;
+          b[1] = this.dx & 0xff;
+          b[2] = this.dy >>> 8;
+          b[3] = this.dy & 0xff;
+          if (debugging)
+            console.log ("sending ScrollRect object: " + buf2hex (buffer));
+          socket.send (buffer);
+        }
+      }
+
+      class SolidRect extends Rect
+      {
+        constructor (x, y, w, h, color)
+        {
+          super (x, y, w, h, rfb_encoding_rre);
+          this.color = color;
+        }
+
+        send ()
+        {
+          super.send ();
+          const buffer = new ArrayBuffer (8);
+          const b = new Uint8Array (buffer);
+          b[0] = 0;  // number of sub-rectangles
+          b[1] = 0;
+          b[2] = 0;
+          b[3] = 0;
+          b[4] = this.color >>> 16;
+          b[5] = (this.color >>> 8) & 0xff;
+          b[6] = this.color & 0xff;
+          b[7] = rfb_padding;
+          if (debugging)
+            console.log ("sending SolidRect object: " + buf2hex (buffer));
+          socket.send (buffer);
+        }
+      }
+
+      class TightRect extends Rect
+      {
+        constructor (x, y, w, h, data)
+        {
+          super (x, y, w, h, rfb_encoding_tight);
+          this.data = data;
+        }
+
+        send ()
+        {
+          super.send ();
+          const data_length = this.data.byteLength;
+          let data_length_length = undefined;
+          if (data_length <= 0x7f)
+            data_length_length = 1;
+          else if (data_length <= 0x3fff)
+            data_length_length = 2;
+          else
+            data_length_length = 3;
+          const buffer = new ArrayBuffer (1 + data_length_length);
+          const b = new Uint8Array (buffer);
+          b[0] = 0x90; // Tight-JPEG
+          if (data_length <= 0x7f)
+            b[1] = data_length & 0x7f;
+          else if (data_length <= 0x3fff)
+            {
+              b[1] = (data_length & 0x7f) | 0x80;
+              b[2] = (data_length & 0x3f80) >>> 7;
+            }
+          else
+            {
+              b[1] = (data_length & 0x7f) | 0x80;
+              b[2] = ((data_length & 0x3f80) >>> 7) | 0x80;
+              b[3] = (data_length & 0x3fc000) >>> 14;
+            }
+          if (debugging)
+            console.log ("sending TightRect object header: " + buf2hex (buffer));
+          socket.send (buffer);
+          if (debugging)
+            console.log ("sending TightRect object data (" + data_length.toString () + " pixel bytes)");
+          socket.send (this.data);
+        }
+      }
+
+      let rects = new Array ();
+
+      class ScrollParams
+      {
+        constructor (x, y, w, h, dx, dy)
+        {
+          this.x = x;
+          this.y = y;
+          this.w = w;
+          this.h = h;
+          this.dx = dx;
+          this.dy = dy;
+        }
+      }
+
+      let scrollRects = new Array ();
+
+      let rfb_bits_per_pixel = 32;
+      let rfb_depth = 24;
+      let rfb_big_endian_flag = 0;
+      let rfb_true_color_flag = 1;
+      let rfb_red_maximum = 255;
+      let rfb_green_maximum = 255;
+      let rfb_blue_maximum = 255;
+      let rfb_red_shift = 24;
+      let rfb_green_shift = 16;
+      let rfb_blue_shift = 8;
+
+      let isFirstFrame = 1;
+      let processing = 0;
+      const rfb_aborted = 42;
+
+      function fillRectBuffer (px, x, y, w, h, color)
+      {
+        for (let yy = 0; yy < h; yy++)
+          {
+            let o = (y + yy) * wScreen + x;
+            for (let xx = 0; xx < w; xx++)
+              px[o++] = color;
+          }
+      }
+
+      function scrollRectBuffer (px, x, y, w, h, dx, dy)
+      {
+        const ww = w - Math.abs (dx);
+        const hh = h - Math.abs (dy);
+        const do0 = dy > 0 ? -wScreen : wScreen;
+        let o10 = (dy > 0 ? y + h - 1 : y) * wScreen + (dx > 0 ? x + w - 1 : x);
+        let o00 = ((dy > 0 ? y + h - 1 : y) - dy) * wScreen + (dx > 0 ? x + w - 1 : x) - dx;
+        for (let i = 0; i < hh; i++)
+          {
+            o1 = o10;
+            o0 = o00;
+            if (dx > 0)
+              for (let j = 0; j < ww; j++)
+                px[o1--] = px[o0--];
+            else
+              for (let j = 0; j < ww; j++)
+                px[o1++] = px[o0++];
+            o10 += do0;
+            o00 += do0;
+          }
+      }
+
+      function sleep (delay)
+      {
+        return new Promise (
+          function (resolve)
+          {
+            setTimeout (resolve, delay)
+          });
+      }
+
+      function canvasToBlob (canvas, type, options)
+      {
+        return new Promise (
+          function (resolve)
+          {
+            canvas.toBlob (resolve, type, options);
+          });
+      }
+
+      function pushRectRaw (x, y, w, h)
+      {
+        const data_length = w * h * 4;
+        const buffer = new ArrayBuffer (data_length);
+        const b = new Uint8Array (buffer);
+        const id1 = ctx1.getImageData (x, y, w, h);
+        const px1 = new Uint8Array (id1.data.buffer);
+        let bo = 0;
+        let po = 0;
+        for (let yy = 0; yy < h; yy++)
+          for (let xx = 0; xx < w; xx++)
+            {
+              b[bo + 0] = px1[po + 2];
+              b[bo + 1] = px1[po + 1];
+              b[bo + 2] = px1[po + 0];
+              b[bo + 3] = rfb_padding;
+              bo += 4;
+              po += 4;
+            }
+        if (debugging)
+          console.log ("storing RawRect (" + data_length.toString () + " pixel bytes)");
+        rects.push (new RawRect (x, y, w, h, buffer));
+      }
+
+      function pushRectScroll (x, y, w, h, dx, dy)
+      {
+        if (debugging)
+          console.log ("storing ScrollRect");
+        rects.push (new ScrollRect (x, y, w, h, dx, dy));
+      }
+
+      function pushRectSolid (x, y, w, h, color)
+      {
+        if (debugging)
+          console.log ("storing SolidRect");
+        rects.push (new SolidRect (x, y, w, h, color));
+      }
+
+      async function pushRectTight (x, y, w, h)
+      {
+        if (debugging)
+          console.log ("pushRectTight: x = " + x.toString () + ", y = " + y.toString () + ", w = " + w.toString () + ", h = " + h.toString ());
+        let canvas = undefined;
+        if (x == 0 && y == 0 && w == wScreen && h == hScreen)
+          canvas = canvas1;
+        else
+          {
+            canvas = document.createElement ('canvas');
+            canvas.width = w;
+            canvas.height = h;
+            canvas.getContext ('2d').drawImage (canvas1, -x, -y);
+          }
+        // if (debugging)
+        //   console.log ("calling canvasToBlob()");
+        const blob = await canvasToBlob (canvas, "image/jpeg");
+        // if (debugging)
+        //   console.log ("calling blob.arrayBuffer()");
+        const jpeg_buffer = await blob.arrayBuffer ();
+        // if (debugging)
+        //   console.log ("storing TightRect (" + jpeg_buffer.byteLength.toString () + " instead of " + (w * h * 4).toString () + " pixel bytes)");
+        rects.push (new TightRect (x, y, w, h, jpeg_buffer));
+      }
+
+      function detectScrolling (px0, px1, x, y, w, h)
+      {
+        if (debugging)
+          console.log ("scrolling: detection started");
+        const pixels = w * h;
+        const points = Math.floor (pixels / scroll_detection_point_divisor);
+        const qxBuffer = new ArrayBuffer (2 * points);
+        const qx = new Uint16Array (qxBuffer);
+        const qyBuffer = new ArrayBuffer (2 * points);
+        const qy = new Uint16Array (qyBuffer);
+        const qc1Buffer = new ArrayBuffer (4 * points);
+        const qc1 = new Uint32Array (qc1Buffer);
+        const qc0Buffer = new ArrayBuffer (4 * points);
+        const qc0 = new Uint32Array (qc0Buffer);
+        for (let i = 0; i < points; i++)
+          {
+            const p = Math.floor (Math.random () * pixels);
+            // I lost a whole day to find out that p / w is a floating point division,
+            // which causes crazy things to happen when we use yy as an array index.
+            // I hate JavaScript. -- PG 20201222
+            const yy = Math.floor (p / w);
+            const xx = p % w;
+            const o = (y + yy) * wScreen + x + xx;
+            qy[i] = yy;
+            qx[i] = xx;
+            qc1[i] = px1[o];
+            qc0[i] = px0[o];
+          }
+        const xDensityBuffer = new ArrayBuffer (2 * w);
+        const xDensity = new Uint16Array (xDensityBuffer);
+        const yDensityBuffer = new ArrayBuffer (2 * h);
+        const yDensity = new Uint16Array (yDensityBuffer);
+        for (let xx = 0; xx < w; xx++)
+          {
+            xDensity[xx] = 0;
+            for (let i = 0; i < points; i++)
+              if (qx[i] < xx && qc1[i] != qc0[i])
+                xDensity[xx]++;
+          }
+        const lx = Math.floor (w / scroll_detection_length_divisor);
+        const halflx = Math.floor (lx / 2);
+        const averageDensity = Math.floor (xDensity[w - 1] / scroll_detection_length_divisor);
+        if (debugging)
+          console.log ("scrolling: averageDensity =", averageDensity);
+        let x0 = 0;
+        while (x0 + lx < w)
+          {
+            while (x0 + lx < w && xDensity[x0 + lx] - xDensity[x0] < averageDensity)
+              x0++;
+            let x1 = x0;
+            while (x1 + lx < w && xDensity[x1 + lx] - xDensity[x1] >= averageDensity)
+              x1++;
+            if (x1 - x0 >= scroll_detection_min_stripe_width)
+              {
+                if (debugging)
+                  console.log ("scrolling: vertical stripe:", x0 + halflx, x1 - x0);
+                for (let yy = 0; yy < h; yy++)
+                  {
+                    yDensity[yy] = 0;
+                    for (let i = 0; i < points; i++)
+                      if (qy[i] < yy && qc1[i] != qc0[i] && qx[i] >= x0 && qx[i] < x1)
+                        yDensity[yy]++;
+
+                  }
+                const ly = Math.floor (h / scroll_detection_length_divisor);
+                const halfly = Math.floor (ly / 2);
+                const averageDensity = Math.floor (yDensity[h - 1] / scroll_detection_length_divisor);
+                if (debugging)
+                  console.log ("scrolling: averageDensity =", averageDensity);
+                let y0 = 0;
+                while (y0 + ly < h)
+                  {
+                    while (y0 + ly < h && yDensity[y0 + ly] - yDensity[y0] < averageDensity)
+                      y0++;
+                    let y1 = y0;
+                    while (y1 + ly < h && yDensity[y1 + ly] - yDensity[y1] >= averageDensity)
+                      y1++;
+                    if (y1 - y0 >= scroll_detection_min_stripe_height)
+                      {
+                        if (debugging)
+                          console.log ("scrolling: horizontal stripe:", y0 + halfly, y1 - y0);
+                        let sx = x0 + halflx;
+                        let sy = y0 + halfly;
+                        let sw = x1 - x0;
+                        let sh = y1 - y0;
+                        if (debugging)
+                          {
+                            console.log ("scrolling: checking area:", sx, sy, sw, sh);
+                            console.log ("scrolling: " + scrollRects.length.toString () + " rects");
+                          }
+                        const redundant = scrollRects.find (
+                          function (r)
+                          {
+                            if (debugging)
+                              console.log ("scrolling: checking rect", sx, sy, sw, sh, "against", r.x, r.y, r.w, r.h, r.dx, r.dy);
+                            if (sx + sw <= r.x || sx >= r.x + r.w)
+                              return 0;
+                            else if (sy + sh <= r.y || sy >= r.y + r.h)
+                              return 0;
+                            else
+                              {
+                                if (debugging)
+                                  console.log ("scrolling: new rect", sx, sy, sw, sh, "is redundant with", r.x, r.y, r.w, r.h, r.dx, r.dy);
+                                return 1;
+                              }
+                          });
+                        if (!redundant)
+                          {
+                            const color = new Array;
+                            for (let i = 0; i < points; i++)
+                              if (qx[i] >= sx && qx[i] < sx + sw && qy[i] >= sy && qy[i] < sy + sh)
+                                color.push (qc1[i]);
+                            const points_inside = color.length;
+                            color.sort (
+                              function (a, b)
+                              {
+                                return a - b;
+                              });
+                            let color_abundance = 1;
+                            let i0 = 0;
+                            let i1 = 1;
+                            while (i1 < points_inside)
+                              {
+                                if (color[i1] != color[i0])
+                                  {
+                                    const a = i1 - i0;
+                                    if (a > color_abundance)
+                                      {
+                                        color_abundance = a;
+                                        i0 = i1;
+                                      }
+                                  }
+                                i1++
+                              }
+                            const a = i1 - i0;
+                            if (a > color_abundance)
+                              color_abundance = a;
+                            if (debugging)
+                              console.log ("scrolling: color_abundance = " + color_abundance.toString () + ", points_inside = " + points_inside.toString ()
+                                           + ", threshold = " + (points_inside * scroll_detection_same_color_threshold).toString ());
+                            if (color_abundance < points_inside * scroll_detection_same_color_threshold)
+                              {
+                                let min_mishits = points_inside;
+                                let sdx = 0;
+                                let sdy = 0;
+                                // We intentionally include the case dx == dy == 0.
+                                // Without this, we get false positives where unchanged content
+                                // is scrolled by 1 pixel because the "repair" after the scrolling
+                                // is minimal.
+                                for (let dy = scroll_detection_y_max; dy >= scroll_detection_y_min && min_mishits > 0; dy--)
+                                  for (let dx = scroll_detection_x_max; dx >= scroll_detection_x_min && min_mishits > 0; dx--)
+                                    if ((dx == 0 || dy == 0 || dx * dx + dy * dy < scroll_detection_r_max * scroll_detection_r_max))
+                                      {
+                                        let i = 0;
+                                        let mishits = 0;
+                                        while (i < points && mishits < min_mishits)
+                                          {
+                                            if (qx[i] >= sx && qx[i] < sx + sw && qy[i] >= sy && qy[i] < sy + sh)
+                                              {
+                                                const o0 = (y + qy[i] - dy) * wScreen + x + qx[i] - dx;
+                                                if (o0 >= 0 && o0 < pixels && qc1[i] != px0[o0])
+                                                  mishits++;
+//                                                if (i < 10)
+//                                                  console.log ("scrolling: point:", qx[i], qy[i], "rect:", sx, sx + sw, sy, sy + sh,
+//                                                               "o0, px:", o0, qc1[i], px0[o0]);
+                                              }
+                                            i++;
+                                          }
+                                        if (mishits < min_mishits)
+                                          {
+                                            min_mishits = mishits;
+                                            sdx = dx;
+                                            sdy = dy;
+                                          }
+                                      if (debugging)
+                                        console.log ("scrolling: dx = " + dx.toString () + ", dy = " + dy.toString ()
+                                                     + ", mishits = " + mishits.toString () + ", points_inside = " + points_inside.toString ());
+                                      }
+                                if (debugging)
+                                  console.log ("scrolling: sdx = " + sdx.toString () + ", sdy = " + sdy.toString ()
+                                               + ", min_mishits = " + min_mishits.toString () + ", points_inside = " + points_inside.toString ());
+                                if (min_mishits < points_inside * (1 - scroll_detection_hit_threshold))
+                                  {
+                                    if (debugging)
+                                      console.log ("scrolling: expanding rect:", sx, sy, sw, sh, sdx, sdy);
+                                    let xx = sx - 1;
+                                    let scrollable = 1;
+                                    while (xx > 0 && scrollable)
+                                      {
+                                        let yy = sy;
+                                        let scrollable_points = 0;
+                                        let o1 = (y + yy) * wScreen + x + xx;
+                                        let o0 = (y + yy - sdy) * wScreen + x + xx - sdx;
+                                        while (yy < sy + sh)
+                                          {
+                                            if (px1[o1] == px0[o0])
+                                              scrollable_points++;
+                                            yy++;
+                                            o1 += wScreen;
+                                            o0 += wScreen;
+                                          }
+                                        if (scrollable_points < sh * scroll_detection_hit_threshold)
+                                          scrollable = 0;
+                                        xx--;
+                                      }
+                                    if (!scrollable)
+                                      sx = xx + 1;
+                                    else
+                                      sx = xx;
+                                    xx = sx + sw;
+                                    scrollable = 1;
+                                    while (xx < w && scrollable)
+                                      {
+                                        let yy = sy;
+                                        let scrollable_points = 0;
+                                        let o1 = (y + yy) * wScreen + x + xx;
+                                        let o0 = (y + yy - sdy) * wScreen + x + xx - sdx;
+                                        while (yy < sy + sh)
+                                          {
+                                            if (px1[o1] == px0[o0])
+                                              scrollable_points++;
+                                            yy++;
+                                            o1 += wScreen;
+                                            o0 += wScreen;
+                                          }
+                                        if (scrollable_points < sh * scroll_detection_hit_threshold)
+                                          scrollable = 0;
+                                        xx++;
+                                      }
+                                    if (!scrollable)
+                                      sw = xx - sx - 1;
+                                    else
+                                      sw = xx - sx;
+                                    let yy = sy - 1;
+                                    scrollable = 1;
+                                    while (yy > 0 && scrollable)
+                                      {
+                                        let xx = sx;
+                                        let scrollable_points = 0;
+                                        let o1 = (y + yy) * wScreen + x + xx;
+                                        let o0 = (y + yy - sdy) * wScreen + x + xx - sdx;
+                                        while (xx < sx + sw)
+                                          {
+                                            if (px1[o1] == px0[o0])
+                                              scrollable_points++;
+                                            xx++;
+                                            o1++;
+                                            o0++;
+                                          }
+                                        if (scrollable_points < sw * scroll_detection_hit_threshold)
+                                          scrollable = 0;
+                                        yy--;
+                                      }
+                                    if (!scrollable)
+                                      sy = yy + 1;
+                                    else
+                                      sy = yy;
+                                    yy = sy + sh;
+                                    scrollable = 1;
+                                    while (yy < h && scrollable)
+                                      {
+                                        let xx = sx;
+                                        let scrollable_points = 0;
+                                        let o1 = (y + yy) * wScreen + x + xx;
+                                        let o0 = (y + yy - sdy) * wScreen + x + xx - sdx;
+                                        while (xx < sx + sw)
+                                          {
+                                            if (px1[o1] == px0[o0])
+                                              scrollable_points++;
+                                            xx++;
+                                            o1++;
+                                            o0++;
+                                          }
+                                        if (scrollable_points < sw * scroll_detection_hit_threshold)
+                                          scrollable = 0;
+                                        yy++;
+                                      }
+                                    if (!scrollable)
+                                      sh = yy - sy - 1;
+                                    else
+                                      sh = yy - sy;
+//                                    if (debugging)
+                                      console.log ("scrolling: pushing rect:", sx, sy, sw, sh, sdx, sdy);
+                                    scrollRects.forEach (
+                                      function (r)
+                                      {
+                                        if (!(sx + sw <= r.x || sx >= r.x + r.w || sy + sh <= r.y || sy >= r.y + r.h))
+                                          {
+                                            if (debugging)
+                                              console.log ("scrolling: neutralizing redundant rect:", r.x, r.y, r.w, r.h, r.dx, r.dy);
+                                            r.dx = 0;
+                                            r.dy = 0;
+                                          }
+                                      });
+                                    scrollRects.push (new ScrollParams (sx, sy, sw, sh, sdx, sdy));
+                                  }
+                              }
+                          }
+                      }
+                    y0 = y1;
+                  }
+              }
+            x0 = x1;
+          }
+        scrollRects.forEach (
+          function (r)
+          {
+            if (r.dx != 0 || r.dy != 0)
+              {
+                const adx = Math.abs (r.dx);
+                const ady = Math.abs (r.dy);
+                if (r.w > adx && r.h > ady)
+                  {
+//                    pushRectSolid (r.x, r.y, r.w, r.h, 0xff0000);
+//                    fillRectBuffer (px0, r.x, r.y, r.w, r.h, 0xffff0000);
+                    const xx = r.dx > 0 ? r.x + r.dx : r.x;
+                    const yy = r.dy > 0 ? r.y + r.dy : r.y;
+                    pushRectScroll (xx, yy, r.w - adx, r.h - ady, xx - r.dx, yy - r.dy);
+                    scrollRectBuffer (px0, r.x, r.y, r.w, r.h, r.dx, r.dy);
+                  }
+              }
+          });
+        if (debugging)
+          console.log ("scrolling: " + scrollRects.length.toString ()
+                       + " rectangle" + (scrollRects.length == 1 ? "" : "s") + " detected");
+        scrollRects.length = 0;
+      }
+
+      function determineBackgroundColor (px, x, y, w, h)
+      {
+        const pixels = w * h;
+        const max_points = Math.floor (pixels / 16);
+        const points = max_points > 256 ? 256 : max_points;
+        const color = new Array (points);
+        for (let i = 0; i < points; i++)
+          {
+            const p = Math.floor (Math.random () * pixels);
+            const yy = Math.floor (p / w);
+            const xx = p % w;
+            const o = yy * wScreen + x;
+            color[i] = px[o];
+          }
+        color.sort (
+          function (a, b)
+          {
+            return a - b;
+          });
+        let background_index = 0;
+        let background_abundance = 1;
+        let i0 = 0;
+        let i1 = 1;
+        while (i1 < points)
+          {
+            if (color[i1] != color[i0])
+              {
+                const a = i1 - i0;
+                if (a > background_abundance)
+                  {
+                    background_index = i0;
+                    background_abundance = a;
+                    i0 = i1;
+                  }
+              }
+            i1++
+          }
+        const a = i1 - i0;
+        if (a > background_abundance)
+          background_index = i0;
+        return color[background_index];
+      }
+
+      async function updateFullRectangle (x, y, w, h)
+      {
+        if (w * h * 4 > 1024)
+          await pushRectTight (x, y, w, h);
+        else
+          pushRectRaw (x, y, w, h);
+      }
+
+      async function updateInnerStructureHorizontal (px0, px1, x, y, w, h, final)
+      {
+        // if (debugging)
+        //   console.log ("updateInnerStructureHorizontal: x = " + x.toString () + ", y = " + y.toString ()
+        //                + ", w = " + w.toString () + ", h = " + h.toString () + ", final = ", final.toString ());
+        const empty = new Array (w);
+        const pixel_unchanged = 0x01000000;
+        const same_color = 0x2000000;
+        let xx = 0;
+        while (xx < w)
+          {
+            let yy = 0;
+            let o = y * wScreen + x + xx;
+            const start_color = px1[o];
+            empty[xx] = (start_color & 0x00ffffff) | (pixel_unchanged | same_color);
+            while (yy < h && empty[xx] & (pixel_unchanged | same_color))
+              {
+                if (px1[o] != px0[o])
+                  empty[xx] &= ~pixel_unchanged;
+                if (px1[o] != start_color)
+                  empty[xx] &= ~same_color;
+                o += wScreen;
+                yy++;
+              }
+            xx++;
+          }
+        let x0 = 0;
+        while (x0 < w && empty[x0] & (pixel_unchanged | same_color))
+          x0++;
+        while (x0 < w)
+          {
+            while (x0 < w && !(empty[x0] & (pixel_unchanged | same_color)))
+              x0++;
+            let x1 = x0;
+            while (x1 < w && empty[x1] & (pixel_unchanged | same_color))
+              x1++;
+            if (x1 < w && x1 - x0 < combine_rects_x_min_distance)
+              for (let xx = x0; xx < x1; xx++)
+                empty[xx] &= ~(pixel_unchanged | same_color);
+            x0 = x1;
+          }
+        let nothing_empty = 1;
+        for (let xx = 0; xx < w; xx++)
+          if (empty[xx] & (pixel_unchanged | same_color))
+            nothing_empty = 0;
+        // if (debugging)
+        //   console.log ("nothing_empty = " + nothing_empty.toString () + ", empty =", empty);
+        x0 = 0;
+        while (x0 < w)
+          {
+            let x1 = x0;
+            if (empty[x0] & pixel_unchanged)
+              {
+                while (x1 < w && empty[x1] & pixel_unchanged)
+                  x1++;
+              }
+            else if (empty[x0] & same_color)
+              {
+                while (x1 < w && empty[x1] & same_color && (empty[x1] & 0x00ffffff) == (empty[x0] & 0x00ffffff))
+                  x1++;
+                x1--;
+                while (x1 > x0 && empty[x1] & pixel_unchanged)
+                  x1--;
+                x1++;
+                const start_color = empty[x0] | 0xff000000;
+                pushRectSolid (x + x0, y, x1 - x0, h, debugging ? start_color | 0x00ffff : start_color);
+                fillRectBuffer (px0, x + x0, y, x1 - x0, h, start_color);
+              }
+            else
+              {
+                while (x1 < w && !(empty[x1] & (pixel_unchanged | same_color)))
+                  x1++;
+                if (final)
+                  await updateFullRectangle (x + x0, y, x1 - x0, h);
+                else
+                  await updateInnerStructureVertical (px0, px1, x + x0, y, x1 - x0, h, nothing_empty);
+              }
+            x0 = x1;
+          }
+      }
+
+      async function updateInnerStructureVertical (px0, px1, x, y, w, h, final)
+      {
+        // if (debugging)
+        //   console.log ("updateInnerStructureVertical: x = " + x.toString () + ", y = " + y.toString ()
+        //                + ", w = " + w.toString () + ", h = " + h.toString () + ", final = ", final.toString ());
+        const empty = new Array (h);
+        const pixel_unchanged = 1;
+        const same_color = 2;
+        let yy = 0;
+        while (yy < h)
+          {
+            let xx = 0;
+            let o = (y + yy) * wScreen + x;
+            const start_color = px1[o];
+            empty[yy] = (start_color & 0x00ffffff) | (pixel_unchanged | same_color);
+            while (xx < w && empty[yy] & (pixel_unchanged | same_color))
+              {
+                if (px1[o] != px0[o])
+                  empty[yy] &= ~pixel_unchanged;
+                if (px1[o] != start_color)
+                  empty[yy] &= ~same_color;
+                o++;
+                xx++;
+              }
+            yy++;
+          }
+        let y0 = 0;
+        while (y0 < h && empty[y0] & (pixel_unchanged | same_color))
+          y0++;
+        while (y0 < h)
+          {
+            while (y0 < h && !(empty[y0] & (pixel_unchanged | same_color)))
+              y0++;
+            let y1 = y0;
+            while (y1 < h && empty[y1] & (pixel_unchanged | same_color))
+              y1++;
+            if (y1 < h && y1 - y0 < combine_rects_y_min_distance)
+              for (let yy = y0; yy < y1; yy++)
+                empty[yy] &= ~(pixel_unchanged | same_color);
+            y0 = y1;
+          }
+        let nothing_empty = 1;
+        for (let yy = 0; yy < h; yy++)
+          if (empty[yy] & (pixel_unchanged | same_color))
+            nothing_empty = 0;
+        // if (debugging)
+        //   console.log ("nothing_empty = " + nothing_empty.toString () + ", empty =", empty);
+        y0 = 0;
+        while (y0 < h)
+          {
+            let y1 = y0;
+            if (empty[y0] & pixel_unchanged)
+              {
+                while (y1 < h && empty[y1] & pixel_unchanged)
+                  y1++;
+              }
+            else if (empty[y0] & same_color)
+              {
+                while (y1 < h && empty[y1] & same_color && (empty[y1] & 0x00ffffff) == (empty[y0] & 0x00ffffff)) 
+                  y1++;
+                y1--;
+                while (y1 > y0 && empty[y1] & pixel_unchanged)
+                  y1--;
+                y1++;
+                const start_color = empty[y0] | 0xff000000;
+                pushRectSolid (x, y + y0, w, y1 - y0, debugging ? start_color | 0xffff00 : start_color);
+                fillRectBuffer (px0, x, y + y0, w, y1 - y0, start_color);
+              }
+            else
+              {
+                while (y1 < h && !(empty[y1] & (pixel_unchanged | same_color)))
+                  y1++;
+                if (final)
+                  await updateFullRectangle (x, y + y0, w, y1 - y0);
+                else
+                  await updateInnerStructureHorizontal (px0, px1, x, y + y0, w, y1 - y0, nothing_empty);
+              }
+            y0 = y1;
+          }
+      }
+
+      async function decomposeRect (px0, px1, x, y, w, h)
+      {
+        await updateInnerStructureVertical (px0, px1, x, y, w, h, 0);
+      }
+
+      async function rfbFramebufferUpdate (isIncremental, x, y, w, h)
+      {
+        if (debugging)
+          console.log ("rfbFramebufferUpdate: isIncremental = " + isIncremental.toString ()
+                       + ", x = " + x.toString () + ", y = " + h.toString ()
+                       + ", w = " + w.toString () + ", h = " + h.toString ());
+        let id1 = ctx1.getImageData (x, y, w, h);
+        ctx0.putImageData (id1, x, y, x, y, w, h);
+        const id0 = ctx0.getImageData (0, 0, wScreen, hScreen);
+        const px0 = new Uint32Array (id0.data.buffer);
+        ctx1.drawImage (video, 0, 0, wScreen, hScreen);
+        id1 = ctx1.getImageData (0, 0, wScreen, hScreen);
+        let px1 = new Uint32Array (id1.data.buffer);
+        if (!isIncremental)
+          {
+            const color = determineBackgroundColor (px1, x, y, w, h);
+            if (debugging)
+              console.log ("background color = " + color.toString ());
+            pushRectSolid (x, y, w, h, debugging ? color | 0x7f00ff : color);
+            fillRectBuffer (px0, x, y, w, h, color);
+          }
+        do
+          {
+            const length = px0.length;
+            let o = 0;
+            while (o < length && px0[o] == px1[o])
+              o++;
+            if (o < length)
+              {
+//                if (isIncremental && w >= scroll_detection_min_width && h >= scroll_detection_min_height)
+//                  detectScrolling (px0, px1, x, y, w, h);
+                await decomposeRect (px0, px1, x, y, w, h);
+              }
+            if (rects.length == 0)
+              {
+                await sleep (100);
+                ctx1.drawImage (video, 0, 0, wScreen, hScreen);
+                id1 = ctx1.getImageData (x, y, w, h);
+                px1 = new Uint32Array (id1.data.buffer);
+              }
+          }
+        while (rects.length == 0);
+      }
+
+      function rfbReceivePixelFormat (buffer, offset)
+      {
+        if (debugging)
+          console.log ("received: SetPixelFormat");
+        const b = new Uint8Array (buffer);
+        rfb_bits_per_pixel = b[4];
+        rfb_depth = b[5];
+        rfb_big_endian_flag = b[6];
+        rfb_true_color_flag = b[7];
+        rfb_red_maximum = b[8] << 8 + b[9];
+        rfb_green_maximum = b[10] << 8 + b[11];
+        rfb_blue_maximum = b[12] << 8 + b[13];
+        rfb_red_shift = b[14];
+        rfb_green_shift = b[15];
+        rfb_blue_shift = b[16];
+        return 20;
+      }
+
+      function rfbReceiveEncodings (buffer, offset)
+      {
+        if (debugging)
+          console.log ("received: SetEncodings");
+        const b = new Uint8Array (buffer);
+        const encodings = b[offset + 2] << 8 + b[offset + 3];
+        return 4 + 4 * encodings;
+      }
+
+      function rfbAbort ()
+      {
+        if (debugging)
+          console.log ("Aborting RFB transmission ...");
+        processing += rfb_aborted;
+      }
+
+      async function rfbFramebufferUpdateRequest (buffer, offset)
+      {
+        if (debugging)
+          console.log ("received: FramebufferUpdateRequest");
+        if (processing)
+          {
+//            if (debugging)
+              if (processing >= rfb_aborted)
+                console.log ("aborted");
+              else
+                console.log ("already being processed");
+          }
+        else
+          {
+            processing++;
+            let b = new Uint8Array (buffer);
+            let isIncremental = b[1];
+            const x = b[2] * 256 + b[3];
+            const y = b[4] * 256 + b[5];
+            const w = b[6] * 256 + b[7];
+            const h = b[8] * 256 + b[9];
+            if (debugging)
+              {
+                console.log ("FramebufferUpdateRequest: Initiating processing ...");
+                console.log ("wScreen = ", wScreen, ", hScreen = ", hScreen + ", w = ", w, ", h = ", h);
+              }
+            rects.length = 0;
+            if (processing < rfb_aborted)
+              {
+                if (isFirstFrame)
+                  {
+                    await rfbFramebufferUpdate (0, 0, 0, wScreen, hScreen);
+                    isFirstFrame = 0;
+                  }
+                else if (isIncremental)
+                  await rfbFramebufferUpdate (1, x, y, w, h);
+                else
+                  await rfbFramebufferUpdate (0, x, y, w, h);
+                if (debugging)
+                  console.log ("FramebufferUpdateRequest: Processing completed. Sending data ... ("
+                               + rects.length.toString ()
+                               + " rectangle" + (rects.length != 1 ? "s" : "") + ")");
+                const header_buffer = new ArrayBuffer (4);
+                b = new Uint8Array (header_buffer);
+                const num_rects = rects.length;
+                b[0] = 0;
+                b[1] = rfb_padding;
+                b[2] = num_rects >>> 8;
+                b[3] = num_rects & 0xff;
+                if (debugging)
+                  console.log ("sending rfbFramebufferUpdate header: " + buf2hex (header_buffer));
+                socket.send (header_buffer);
+                rects.forEach (
+                  function sendIt (r)
+                  {
+                    r.send ();
+                  });
+              }
+            processing--;
+          }
+        return 10;
+      }
+
+      function rfbReceiveKeyEvent (buffer, offset)
+      {
+        if (debugging)
+          console.log ("received: KeyEvent");
+        return 8;
+      }
+
+      function rfbReceivePointerEvent (buffer, offset)
+      {
+        if (debugging)
+          console.log ("received: PointerEvent");
+        return 6;
+      }
+
+      function rfbReceiveCutText (buffer, offset)
+      {
+        if (debugging)
+          console.log ("received: ClientCutText");
+        const b = new Uint8Array (buffer);
+        const length = b[offset + 4] << 24 + b[offset + 5] << 16 + b[offset + 6] << 8 + b[offset + 7];
+        return 8 + length;
+      }
+
+      function rfbServer (event)
+      {
+        if (debugging)
+          console.log ("received: " + buf2hex (event.data));
+        const b = new Uint8Array (event.data);
+        let offset = 0;
+        while (offset < event.data.byteLength)
+          {
+            if (debugging)
+              console.log ("received RFB packet type " + b[offset].toString ());
+            if (b[offset] == 0)
+              offset += rfbReceivePixelFormat (event.data, offset);
+            else if (b[offset] == 2)
+              offset += rfbReceiveEncodings (event.data, offset);
+            else if (b[offset] == 3)
+              offset += rfbFramebufferUpdateRequest (event.data, offset);
+            else if (b[offset] == 4)
+              offset += rfbReceiveKeyEvent (event.data, offset);
+            else if (b[offset] == 5)
+              offset += rfbReceivePointerEvent (event.data, offset);
+            else if (b[offset] == 6)
+              offset += rfbReceiveCutText (event.data, offset);
+            else
+              {
+                if (debugging)
+                  console.log ("unknown RFB packet");
+                offset = event.data.byteLength;
+              }
+          }
+      }
+
+      function rfbFramebufferHandshake (event)
+      {
+        if (debugging)
+          console.log ("received: " + buf2hex (event.data));
+        socket.removeEventListener ("message", rfbFramebufferHandshake);
+        socket.addEventListener ("message", rfbServer);
+        const buffer = new ArrayBuffer (24 + rfb_name_length);
+        const b = new Uint8Array (buffer);
+        b[0] = wScreen >>> 8;
+        b[1] = wScreen & 0xff;
+        b[2] = hScreen >>> 8;
+        b[3] = hScreen & 0xff;
+        b[4] = rfb_bits_per_pixel;
+        b[5] = rfb_depth;
+        b[6] = rfb_big_endian_flag;
+        b[7] = rfb_true_color_flag;
+        b[8] = rfb_red_maximum >>> 8;
+        b[9] = rfb_red_maximum & 0xff;
+        b[10] = rfb_green_maximum >>> 8;
+        b[11] = rfb_green_maximum & 0xff;
+        b[12] = rfb_blue_maximum >>> 8;
+        b[13] = rfb_blue_maximum & 0xff;
+        b[14] = rfb_red_shift;
+        b[15] = rfb_green_shift;
+        b[16] = rfb_blue_shift;
+        b[17] = rfb_padding;
+        b[18] = rfb_padding;
+        b[19] = rfb_padding;
+        b[20] = rfb_name_length >>> 24;
+        b[21] = rfb_name_length >>> 16;
+        b[22] = rfb_name_length >>> 8;
+        b[23] = rfb_name_length & 0xff;
+        for (let i = 0; i < rfb_name_length; i++)
+          b[24 + i] = rfb_name[i];
+        if (debugging)
+          console.log ("sending framebuffer parameters: " + buf2hex (buffer));
+        socket.send (buffer);
+      }
+
+      function rfbAuthenticate (event)
+      {
+        if (debugging)
+          console.log ("received: " + buf2hex (event.data));
+        socket.removeEventListener ("message", rfbAuthenticate);
+        socket.addEventListener ("message", rfbFramebufferHandshake);
+        if (debugging)
+          console.log ("sending: 00 00 00 00");
+        const buffer = new ArrayBuffer (4);
+        const b = new Uint8Array (buffer);
+        b[0] = 0;
+        b[1] = 0;
+        b[2] = 0;
+        b[3] = 0;
+        socket.send (buffer);
+      }
+
+      function rfbSecurityHandshake (event)
+      {
+        if (debugging)
+          console.log ("received: " + buf2hex (event.data));
+        socket.removeEventListener ("message", rfbSecurityHandshake);
+        socket.addEventListener ("message", rfbAuthenticate);
+        if (debugging)
+          console.log ("sending: 01 01");
+        const buffer = new ArrayBuffer (2);
+        const b = new Uint8Array (buffer);
+        b[0] = 1;
+        b[1] = 1;
+        socket.send (buffer);
+      }
+
+      function rfbConnect (event)
+      {
+        socket.addEventListener ("message", rfbSecurityHandshake);
+        if (debugging)
+          console.log ("sending: RFB 003.008\\n");
+        socket.send ("RFB 003.008\n");
+      }
+
+      document.addEventListener ("DOMContentLoaded",
+        function ()
+        {
+          video.addEventListener ("play",
+            function ()
+            {
+              wScreen = video.videoWidth;
+              hScreen = video.videoHeight;
+              canvas1.width = wScreen;
+              canvas1.height = hScreen;
+              ctx1 = canvas1.getContext ("2d");
+              canvas0.width = wScreen;
+              canvas0.height = hScreen;
+              ctx0 = canvas0.getContext ("2d");
+              ctx0.beginPath ();
+              ctx0.rect (0, 0, wScreen, hScreen);
+              ctx0.fillStyle = "black";
+              ctx0.fill ();
+              if (debugging)
+                console.log ("Opening WebSocket ...");
+              socket = new WebSocket ("wss://streaming.cvh-server.de/websock/wc-6/", ['binary', 'base64']);
+              socket.binaryType = "arraybuffer";
+              socket.addEventListener ("open", rfbConnect);
+            },
+            false);
+          video.addEventListener ("abort", rfbAbort, false);
+          video.addEventListener ("emptied", rfbAbort, false);
+          video.addEventListener ("ended", rfbAbort, false);
+          video.addEventListener ("error", rfbAbort, false);
+          video.addEventListener ("pause", rfbAbort, false);
+          video.addEventListener ("stalled", rfbAbort, false);
+//          video.addEventListener ("suspend", rfbAbort, false);
+        });
+
+    </script>
+  </body>
+</html>
diff --git a/termine.txt b/termine.txt
index a6b276956cdf2294c07406d321d376872f39348e..b1c7381f34f9b59aab01faba79e89f6edea74f54 100644
--- a/termine.txt
+++ b/termine.txt
@@ -10,4 +10,6 @@ Regelungstechnik-Praktika (13:15-16:15 Uhr): 25.4.2024, 23.5.2024, 6.6.2024, 20.
 
 (Evtl. am 25.4.2024: Präsentation zu "Eingebettete Systeme")
 
+25.4.2024, ca. 17:00 Uhr: Lasttest PULT (Mumble, VNC, CVHCamera)
+
 Ringvorlesung: 16.5.2024: ab 17:30 Uhr Vortrag "Was ist dunkle Materie?" in 3-30