[Spice-devel,v2,10/11] server/video-streams: adjust mjpeg quality and frame rate according to the current bit rate

Submitted by Yonit Halperin on April 17, 2012, 10:12 a.m.

Details

Message ID 1334657556-5083-10-git-send-email-yhalperi@redhat.com
State New
Headers show

Not browsing as part of any series.

Commit Message

Yonit Halperin April 17, 2012, 10:12 a.m.
Previously, the mjpeg quality was always 70. The frame rate was tuned
according to the frames' congestion in the pipe.
This patch sets the mjpeg quality and frame rate according
to the compressed size of the frames and the currently available bit
rate.
The compression size is estimated for different jpeg qualities,
and the bit rate is evaluated using qos queries (see red_channel).
The bit rate and compression size are monitored for major changes, and
when they occur, the mjpeg settings are re-evaluated.
In addition, the settings are fine-tuned correspondingly to the frames
pipe congestion.
---
 server/mjpeg_encoder.c |  350 +++++++++++++++++++++++++++++++++++++++++++++++-
 server/mjpeg_encoder.h |   18 +++-
 server/red_worker.c    |   85 +++++-------
 3 files changed, 399 insertions(+), 54 deletions(-)

Patch hide | download patch | download mbox

diff --git a/server/mjpeg_encoder.c b/server/mjpeg_encoder.c
index b3685f8..c8c59de 100644
--- a/server/mjpeg_encoder.c
+++ b/server/mjpeg_encoder.c
@@ -24,27 +24,80 @@ 
 #include <jerror.h>
 #include <jpeglib.h>
 
+#define MJPEG_FPS_TEST_INTERVAL 1
+#define MJPEG_MAX_FPS 25
+#define MJPEG_MIN_FPS 1
+
+#define MJPEG_QUALITY_SAMPLE_NUM 4
+static const int mjpeg_quality_samples[MJPEG_QUALITY_SAMPLE_NUM] = {15, 25, 50, 70};
+
+#define MJPEG_FRAME_SIZE_CHANGE_TH 1.5
+#define MJPEG_BIT_RATE_CHANGE_TH 1.25
+#define MJPEG_AVERAGE_SIZE_WINDOW 3
+
+/*
+ * Adjusting the stream jpeg quality and frame rate (fps):
+ * When during_sampling=TRUE, we compress different frames with different
+ * jpeg quality. By considering (1) the resulting compression ratio, and (2) the available
+ * bit rate, we evaulate the max frame frequency for the stream with the given quality,
+ * and we choose the highest quality that will allow a reasonable frame rate.
+ * during_sampling is set for new streams and also when the bit rate and/or
+ * average compressed frame size significantly change.
+ */
+typedef struct MJpegEncoderRateControl {
+    int during_sampling;
+    uint64_t quality_sample_size[MJPEG_QUALITY_SAMPLE_NUM];
+    /* lower limit for the the current sampling */
+    int min_sample_quality_id;
+    int min_sample_quality_fps; // min fps for the given quality
+    /* tracking the best sampled fps so far */
+    int max_sampled_fps;
+    int max_sampled_fps_quality_id;
+    /* tracking the average frame size for the current jpeg quality */
+    uint64_t size_sum;
+    int size_summed_count;
+    uint64_t recent_size_sum;
+    int recent_size_summed_count;
+
+    uint32_t frames_dropped;
+    uint32_t frames_encoded;
+    uint32_t frames_encoded_consecutive; // without drops in between
+} MJpegEncoderRateControl;
+
 struct MJpegEncoder {
     uint8_t *row;
     int first_frame;
-    int quality;
 
     struct jpeg_compress_struct cinfo;
     struct jpeg_error_mgr jerr;
 
     unsigned int bytes_per_pixel; /* bytes per pixel of the input buffer */
     void (*pixel_converter)(uint8_t *src, uint8_t *dest);
+
+    MJpegEncoderCbs cbs;
+    void *opaque;
+
+    MJpegEncoderRateControl rate_control;
+
+    int quality_id;
+    uint32_t fps;
+    uint64_t byte_rate;
 };
 
-MJpegEncoder *mjpeg_encoder_new()
+MJpegEncoder *mjpeg_encoder_new(MJpegEncoderCbs *cbs, void *opaque)
 {
     MJpegEncoder *enc;
 
     enc = spice_new0(MJpegEncoder, 1);
 
+    spice_assert(cbs && cbs->get_bit_rate);
+
     enc->first_frame = TRUE;
-    enc->quality = 70;
     enc->cinfo.err = jpeg_std_error(&enc->jerr);
+    enc->fps = MJPEG_MAX_FPS;
+    enc->quality_id = MJPEG_QUALITY_SAMPLE_NUM / 2;
+    enc->cbs = *cbs;
+    enc->opaque = opaque;
     jpeg_create_compress(&enc->cinfo);
 
     return enc;
@@ -195,6 +248,274 @@  spice_jpeg_mem_dest(j_compress_ptr cinfo,
 }
 /* end of code from libjpeg */
 
+static inline void mjpeg_encoder_set_quality(MJpegEncoder *encoder, int quality_id, uint32_t fps)
+{
+    memset(&encoder->rate_control, 0, sizeof(MJpegEncoderRateControl));
+    encoder->quality_id = quality_id;
+    encoder->fps = fps;
+}
+
+/*
+ * Adjust the stream's jpeg quality and frame rate.
+ * We evaluate the compression ratio of different jpeg qualities;
+ * We compress successive frames with different qualities,
+ * and then we estimate the stream frame rate with the current jpeg quality
+ * and availalbe bit rate.
+*/
+static inline void mjpeg_encoder_do_quality_size_sampling(MJpegEncoder *encoder)
+{
+    MJpegEncoderRateControl *rate_control;
+    int fps;
+    uint64_t last_quality_sample_size;
+
+    rate_control = &encoder->rate_control;
+    spice_assert(rate_control->during_sampling);
+    if (rate_control->quality_sample_size[encoder->quality_id] == 0) {
+        return;
+    }
+
+    fps = encoder->byte_rate / rate_control->quality_sample_size[encoder->quality_id];
+    spice_debug("mjpeg %p: jpeg %d: %.2f (KB) fps %d",
+                encoder,
+                mjpeg_quality_samples[encoder->quality_id],
+                rate_control->quality_sample_size[encoder->quality_id] / 1000.0,
+                fps);
+
+    if (fps > rate_control->max_sampled_fps ||
+        (fps == rate_control->max_sampled_fps &&
+         encoder->quality_id > rate_control->max_sampled_fps_quality_id)) {
+        rate_control->max_sampled_fps = fps;
+        rate_control->max_sampled_fps_quality_id = encoder->quality_id;
+    }
+
+    /*
+     * Choosing whether to evaluate another quality, or to choose one of
+     * those that were already sampled. Assuming monotonicity w.r.t quality
+     * and compression ratio.
+     * The quality won't be improved if the fps assoicated with the better one is <
+     * 0.66 * min_sample_quality_fps
+     */
+    if (fps > 5 && fps >= 0.66 * rate_control->min_sample_quality_fps) {
+        if (encoder->quality_id + 1 == MJPEG_QUALITY_SAMPLE_NUM ||
+            rate_control->quality_sample_size[encoder->quality_id + 1] != 0) {
+            /* best quality has been reached, or the next (better) quality was
+             * already evaluated and didn't pass the fps threshold */
+            goto complete_sample;
+        } else {
+            /* evaluate the next quality as well*/
+            encoder->quality_id++;
+        }
+    } else {
+        if (encoder->quality_id == 0 ||
+            encoder->quality_id <= rate_control->min_sample_quality_id) {
+            goto complete_sample;
+        } else if (rate_control->quality_sample_size[encoder->quality_id - 1] != 0) {
+            encoder->quality_id--;
+            goto complete_sample;
+        } else {
+            encoder->quality_id--;
+        }
+    }
+    return;
+
+complete_sample:
+    encoder->quality_id = MAX(encoder->quality_id,
+                              rate_control->max_sampled_fps_quality_id);
+    encoder->fps = encoder->byte_rate /
+                   rate_control->quality_sample_size[encoder->quality_id];
+    if (encoder->quality_id == rate_control->min_sample_quality_id) {
+        encoder->fps = MAX(encoder->fps, rate_control->min_sample_quality_fps);
+    }
+    encoder->fps = MIN(MJPEG_MAX_FPS, encoder->fps);
+    encoder->fps = MAX(MJPEG_MIN_FPS, encoder->fps);
+    last_quality_sample_size = rate_control->quality_sample_size[encoder->quality_id];
+    /* reseting the rate_control struct */
+    mjpeg_encoder_set_quality(encoder, encoder->quality_id, encoder->fps);
+    rate_control->recent_size_sum = last_quality_sample_size;
+    rate_control->recent_size_summed_count = 1;
+    spice_debug("MJpeg quality sample end %p: quality %d fps %d",
+                encoder, mjpeg_quality_samples[encoder->quality_id], encoder->fps);
+}
+
+/*
+ * Fine tuning of the stream's frame rate and quality using
+ * the frames congestion in the pipe.
+ */
+static inline void mjpeg_encoder_update_quality_by_drops(MJpegEncoder *encoder)
+{
+    MJpegEncoderRateControl *rate_control = &encoder->rate_control;
+    uint32_t frames_total;
+
+    frames_total = rate_control->frames_dropped + rate_control->frames_encoded;
+
+    if (frames_total / encoder->fps < MJPEG_FPS_TEST_INTERVAL) {
+        return;
+    }
+
+    if (rate_control->frames_dropped) {
+        double drop_factor = (rate_control->frames_encoded + 0.0) / frames_total;
+        if (drop_factor <= 0.9) {
+            if (encoder->fps <= 10 && encoder->quality_id > 0 ) {
+                mjpeg_encoder_set_quality(encoder, encoder->quality_id - 1, encoder->fps);
+                spice_debug("mjpeg %p quality--: jpeg %d fps %d", encoder,
+                            mjpeg_quality_samples[encoder->quality_id],
+                                encoder->fps);
+            } else {
+                encoder->fps--;
+                encoder->fps = MAX(MJPEG_MIN_FPS, encoder->fps);
+                spice_debug("mjpeg %p fps--: jpeg %d fps %d", encoder,
+                            mjpeg_quality_samples[encoder->quality_id],
+                            encoder->fps);
+            }
+        }
+    } else {
+        if (encoder->fps >= 15 && encoder->quality_id < MJPEG_QUALITY_SAMPLE_NUM - 1) {
+            /* being more strict when we want to increase quality */
+            if (encoder->rate_control.frames_encoded_consecutive / encoder->fps >=
+                2 * MJPEG_FPS_TEST_INTERVAL) {
+                if (encoder->fps < 20) {
+                    rate_control->min_sample_quality_id = encoder->quality_id;
+                    rate_control->min_sample_quality_fps = encoder->fps;
+                    rate_control->during_sampling = TRUE;
+                    spice_debug("mjpeg %p quality resampling: jpeg %d fps %d", encoder,
+                                mjpeg_quality_samples[encoder->quality_id],
+                                encoder->fps);
+                    mjpeg_encoder_do_quality_size_sampling(encoder);
+                } else {
+                    mjpeg_encoder_set_quality(encoder, encoder->quality_id + 1,
+                                              0.5 * encoder->fps);
+                    spice_debug("mjpeg %p quality++: jpeg %d fps %d", encoder,
+                                mjpeg_quality_samples[encoder->quality_id],
+                                encoder->fps);
+                }
+            } else {
+                encoder->fps++;
+                encoder->fps = MIN(MJPEG_MAX_FPS, encoder->fps);
+                spice_debug("mjpeg %p fps++: jpeg %d fps %d", encoder,
+                            mjpeg_quality_samples[encoder->quality_id],
+                            encoder->fps);
+            }
+        } else {
+            encoder->fps++;
+            encoder->fps = MIN(MJPEG_MAX_FPS, encoder->fps);
+            spice_debug("mjpeg %p fps++: jpeg %d fps %d", encoder,
+                        mjpeg_quality_samples[encoder->quality_id],
+                        encoder->fps);
+        }
+    }
+    rate_control->frames_encoded = 0;
+    rate_control->frames_dropped = 0;
+}
+
+/*
+ * Monitor changes in the available byte rate for the stream,
+ * and/or in the stream's compressed frames size. If the changes
+ * pass a predefined threshold, we re-evaluate the stream's jpeg
+ * quality and frame rate.
+ */
+static void mjpeg_encoder_update_quality(MJpegEncoder *encoder)
+{
+    MJpegEncoderRateControl *rate_control;
+    uint64_t stream_byte_rate;
+    double byte_rate_change;
+    double size_change = 1;
+    double size_avg_old = 0.0;
+    double size_avg_new = 0.0;
+
+    rate_control = &encoder->rate_control;
+    stream_byte_rate = encoder->cbs.get_bit_rate(encoder->opaque) / 8;
+
+    if (stream_byte_rate == 0) {
+        spice_assert(encoder->byte_rate == 0);
+        mjpeg_encoder_update_quality_by_drops(encoder);
+        return;
+    }
+
+    if (encoder->byte_rate == 0) { // new stream
+        spice_debug("zero byte rate");
+        encoder->byte_rate = stream_byte_rate;
+        rate_control->during_sampling = TRUE;
+        rate_control->min_sample_quality_id = 0;
+        rate_control->min_sample_quality_fps = 0;
+    }
+
+    if (rate_control->during_sampling) {
+        mjpeg_encoder_do_quality_size_sampling(encoder);
+        return;
+    }
+    spice_assert(rate_control->quality_sample_size[encoder->quality_id]);
+    byte_rate_change = (stream_byte_rate + 0.0) / encoder->byte_rate;
+
+    rate_control->recent_size_sum += rate_control->quality_sample_size[encoder->quality_id];
+    rate_control->recent_size_summed_count++;
+    size_avg_new = (rate_control->recent_size_sum + 0.0) / rate_control->recent_size_summed_count;
+    if (rate_control->recent_size_summed_count >= MJPEG_AVERAGE_SIZE_WINDOW &&
+        rate_control->size_summed_count > 0) {
+        size_avg_old = (rate_control->size_sum + 0.0) / rate_control->size_summed_count;
+        size_change = size_avg_new / size_avg_old;
+    }
+
+    if (byte_rate_change > MJPEG_BIT_RATE_CHANGE_TH) {
+        spice_debug("mjpeg %p BYTE RATE CHANGE >>: %.2f (%lu-->%lu prev-quality %d prev-fps %d",
+                    encoder, byte_rate_change, encoder->byte_rate, stream_byte_rate,
+                    mjpeg_quality_samples[encoder->quality_id],
+                    encoder->fps);
+        rate_control->during_sampling = TRUE;
+        /* byte rate has improved --> don't allow stream to deteriorate */
+        rate_control->min_sample_quality_id = encoder->quality_id;
+        rate_control->min_sample_quality_fps = encoder->fps;
+    } else if (1.0 / byte_rate_change > MJPEG_BIT_RATE_CHANGE_TH) {
+        spice_debug("mjpeg %p BYTE RATE CHANGE <<: %.2f (%lu-->%lu) prev-quality %d prev-fps %d",
+                    encoder, byte_rate_change, encoder->byte_rate, stream_byte_rate,
+                    mjpeg_quality_samples[encoder->quality_id],
+                    encoder->fps);
+        if (encoder->fps * size_avg_new > stream_byte_rate) {
+            rate_control->during_sampling = TRUE;
+            rate_control->min_sample_quality_id = 0;
+            rate_control->min_sample_quality_fps = 0;
+        } else {
+            spice_debug("stream byte rate is not limiting the current setting");
+        }
+    }
+
+    if (size_change > MJPEG_FRAME_SIZE_CHANGE_TH) {
+        spice_debug("mjpeg %p SIZE CHANGE >>: %.2f (%.2f-->%.2f) prev-quality %d prev-fps %d",
+                    encoder, size_change, size_avg_old, size_avg_new,
+                    mjpeg_quality_samples[encoder->quality_id],
+                    encoder->fps);
+        if (encoder->fps * size_avg_new > stream_byte_rate) {
+            rate_control->during_sampling = TRUE;
+            rate_control->min_sample_quality_id = 0;
+            rate_control->min_sample_quality_fps = 0;
+        } else {
+            spice_debug("stream frame size is not limiting current setting");
+        }
+    } else if (1.0 / size_change > MJPEG_FRAME_SIZE_CHANGE_TH) {
+        spice_debug("mjpeg %p SIZE CHANGE <<: %.2f (%.2f-->%.2f) prev-quality %d prev-fps %d",
+                    encoder, size_change, size_avg_old, size_avg_new,
+                    mjpeg_quality_samples[encoder->quality_id],
+                    encoder->fps);
+        rate_control->during_sampling = TRUE;
+        /* compression ratio has improved --> don't allow stream to deteriorate */
+        rate_control->min_sample_quality_id = encoder->quality_id;
+        rate_control->min_sample_quality_fps = encoder->fps;
+    }
+    encoder->byte_rate = stream_byte_rate;
+
+    if (rate_control->recent_size_summed_count >= MJPEG_AVERAGE_SIZE_WINDOW) {
+        rate_control->size_sum += rate_control->recent_size_sum;
+        rate_control->size_summed_count += rate_control->recent_size_summed_count;
+        rate_control->recent_size_sum = 0;
+        rate_control->recent_size_summed_count = 0;
+    }
+    if (rate_control->during_sampling) {
+        rate_control->quality_sample_size[encoder->quality_id] = size_avg_new;
+        mjpeg_encoder_do_quality_size_sampling(encoder);
+    } else {
+        mjpeg_encoder_update_quality_by_drops(encoder);
+    }
+}
+
 int mjpeg_encoder_start_frame(MJpegEncoder *encoder, SpiceBitmapFmt format,
                               int width, int height,
                               uint8_t **dest, size_t *dest_len)
@@ -244,7 +565,8 @@  int mjpeg_encoder_start_frame(MJpegEncoder *encoder, SpiceBitmapFmt format,
     encoder->cinfo.image_height     = height;
     jpeg_set_defaults(&encoder->cinfo);
     encoder->cinfo.dct_method       = JDCT_IFAST;
-    jpeg_set_quality(&encoder->cinfo, encoder->quality, TRUE);
+    mjpeg_encoder_update_quality(encoder);
+    jpeg_set_quality(&encoder->cinfo, mjpeg_quality_samples[encoder->quality_id], TRUE);
     jpeg_start_compress(&encoder->cinfo, encoder->first_frame);
 
     return TRUE;
@@ -279,9 +601,27 @@  int mjpeg_encoder_encode_scanline(MJpegEncoder *encoder, uint8_t *src_pixels,
 size_t mjpeg_encoder_end_frame(MJpegEncoder *encoder)
 {
     mem_destination_mgr *dest = (mem_destination_mgr *) encoder->cinfo.dest;
+    size_t out_size;
 
     jpeg_finish_compress(&encoder->cinfo);
+    out_size = dest->pub.next_output_byte - dest->buffer;
 
     encoder->first_frame = FALSE;
-    return dest->pub.next_output_byte - dest->buffer;
+    encoder->rate_control.frames_encoded++;
+    encoder->rate_control.frames_encoded_consecutive++;
+    encoder->rate_control.quality_sample_size[encoder->quality_id] = out_size;
+
+    return out_size;
+}
+
+void mjpeg_encoder_notify_frame_drop(MJpegEncoder *encoder)
+{
+    encoder->rate_control.frames_dropped++;
+    encoder->rate_control.frames_encoded_consecutive = 0;
 }
+
+uint32_t mjpeg_encoder_get_fps(MJpegEncoder *encoder)
+{
+    return encoder->fps;
+}
+
diff --git a/server/mjpeg_encoder.h b/server/mjpeg_encoder.h
index 3a005b7..0946ce2 100644
--- a/server/mjpeg_encoder.h
+++ b/server/mjpeg_encoder.h
@@ -23,7 +23,11 @@ 
 
 typedef struct MJpegEncoder MJpegEncoder;
 
-MJpegEncoder *mjpeg_encoder_new();
+typedef struct MJpegEncoderCbs {
+    uint64_t (*get_bit_rate)(void *opaque);
+} MJpegEncoderCbs;
+
+MJpegEncoder *mjpeg_encoder_new(MJpegEncoderCbs *cbs, void *opaque);
 void mjpeg_encoder_destroy(MJpegEncoder *encoder);
 
 uint8_t mjpeg_encoder_get_bytes_per_pixel(MJpegEncoder *encoder);
@@ -34,5 +38,17 @@  int mjpeg_encoder_encode_scanline(MJpegEncoder *encoder, uint8_t *src_pixels,
                                   size_t image_width);
 size_t mjpeg_encoder_end_frame(MJpegEncoder *encoder);
 
+/*
+ * The recommanded frame rate (per second) for the
+ * current available bit rate.
+ */
+uint32_t mjpeg_encoder_get_fps(MJpegEncoder *encoder);
+
+/* 
+ *  Should be called every time a frame is dropped due
+ *  to networking reasons. The drops statitstics are used
+ *  in order to fine tune the mjpeg compression quality and fps.
+ */
+void mjpeg_encoder_notify_frame_drop(MJpegEncoder *encoder);
 
 #endif
diff --git a/server/red_worker.c b/server/red_worker.c
index ccc2bfc..6c86ba8 100644
--- a/server/red_worker.c
+++ b/server/red_worker.c
@@ -113,9 +113,6 @@ 
 #define RED_STREAM_FRAMES_RESET_CONDITION 100
 #define RED_STREAM_MIN_SIZE (96 * 96)
 
-#define FPS_TEST_INTERVAL 1
-#define MAX_FPS 30
-
 //best bit rate per pixel base on 13000000 bps for frame size 720x576 pixels and 25 fps
 #define BEST_BIT_RATE_PER_PIXEL 38
 #define WORST_BIT_RATE_PER_PIXEL 4
@@ -375,6 +372,9 @@  typedef struct ImageItem {
 
 typedef struct Drawable Drawable;
 
+typedef struct DisplayChannel DisplayChannel;
+typedef struct DisplayChannelClient DisplayChannelClient;
+
 enum {
     STREAM_FRAME_NONE,
     STREAM_FRAME_NATIVE,
@@ -402,10 +402,7 @@  typedef struct StreamAgent {
     Stream *stream;
     uint64_t last_send_time;
     MJpegEncoder *mjpeg_encoder;
-
-    int frames;
-    int drops;
-    int fps;
+    DisplayChannelClient *dcc;
 } StreamAgent;
 
 typedef struct StreamClipItem {
@@ -488,9 +485,6 @@  typedef struct FreeList {
     WaitForChannels wait;
 } FreeList;
 
-typedef struct DisplayChannel DisplayChannel;
-typedef struct DisplayChannelClient DisplayChannelClient;
-
 typedef struct  {
     DisplayChannelClient *dcc;
     RedCompressBuf *bufs_head;
@@ -938,6 +932,7 @@  typedef struct RedWorker {
     Ring streams;
     ItemTrace items_trace[NUM_TRACE_ITEMS];
     uint32_t next_item_trace;
+    uint64_t streams_size_total;
 
     QuicData quic_data;
     QuicContext *quic;
@@ -2519,6 +2514,7 @@  static void red_stop_stream(RedWorker *worker, Stream *stream)
 
     spice_assert(ring_item_is_linked(&stream->link));
     spice_assert(!stream->current);
+    spice_debug("id %ld", stream - worker->streams_buf);
     WORKER_FOREACH_DCC(worker, item, dcc) {
         StreamAgent *stream_agent;
 
@@ -2532,6 +2528,7 @@  static void red_stop_stream(RedWorker *worker, Stream *stream)
         stream->refs++;
         red_channel_client_pipe_add(&dcc->common.base, &stream_agent->destroy_item);
     }
+    worker->streams_size_total -= stream->width * stream->height;
     ring_remove(&stream->link);
     red_release_stream(worker, stream);
 }
@@ -2743,22 +2740,40 @@  static int get_minimal_bit_rate(RedWorker *worker, int width, int height)
     return ret;
 }
 
+static uint64_t red_stream_mjpeg_encoder_get_bit_rate(void *opaque)
+{
+    StreamAgent *agent = opaque;
+    uint64_t channel_bit_rate;
+
+    spice_assert(agent);
+    channel_bit_rate = red_channel_client_get_qos_bit_rate(&agent->dcc->common.base);
+
+    /* dividing the available bandwidth among the active streams, and saving 0.2
+     * of it for other messages */
+    return 0.8 * channel_bit_rate * (agent->stream->width * agent->stream->height) /
+           agent->dcc->common.worker->streams_size_total;
+}
+
 static void red_display_create_stream(DisplayChannelClient *dcc, Stream *stream)
 {
     StreamAgent *agent = &dcc->stream_agents[stream - dcc->common.worker->streams_buf];
+    MJpegEncoderCbs mjpeg_cbs;
+
+    spice_debug("id %ld %dx%d dest (%d,%d), (%d, %d)",
+                stream - dcc->common.worker->streams_buf,
+                stream->width, stream->height,
+                stream->dest_area.left, stream->dest_area.top,
+                stream->dest_area.right, stream->dest_area.bottom);
 
     stream->refs++;
     spice_assert(region_is_empty(&agent->vis_region));
     if (stream->current) {
-        agent->frames = 1;
         region_clone(&agent->vis_region, &stream->current->tree_item.base.rgn);
-    } else {
-        agent->frames = 0;
     }
-    agent->drops = 0;
-    agent->fps = MAX_FPS;
     reset_rate(dcc, agent);
-    agent->mjpeg_encoder = mjpeg_encoder_new();
+    mjpeg_cbs.get_bit_rate = red_stream_mjpeg_encoder_get_bit_rate;
+    agent->mjpeg_encoder = mjpeg_encoder_new(&mjpeg_cbs, agent);
+    agent->dcc = dcc;
     red_channel_client_pipe_add(&dcc->common.base, &agent->create_item);
 }
 
@@ -2795,6 +2810,7 @@  static void red_create_stream(RedWorker *worker, Drawable *drawable)
     SpiceBitmap *bitmap = &drawable->red_drawable->u.copy.src_bitmap->u.bitmap;
     stream->top_down = !!(bitmap->flags & SPICE_BITMAP_FLAGS_TOP_DOWN);
     drawable->stream = stream;
+    worker->streams_size_total += stream->width * stream->height;
 
     WORKER_FOREACH_DCC(worker, dcc_ring_item, dcc) {
         red_display_create_stream(dcc, stream);
@@ -2950,12 +2966,6 @@  static void reset_rate(DisplayChannelClient *dcc, StreamAgent *stream_agent)
     /* MJpeg has no rate limiting anyway, so do nothing */
 }
 
-static int display_channel_client_is_low_bandwidth(DisplayChannelClient *dcc)
-{
-    return main_channel_client_is_low_bandwidth(
-        red_client_get_main(red_channel_client_get_client(&dcc->common.base)));
-}
-
 static inline void pre_stream_item_swap(RedWorker *worker, Stream *stream)
 {
     DrawablePipeItem *dpi;
@@ -2973,34 +2983,11 @@  static inline void pre_stream_item_swap(RedWorker *worker, Stream *stream)
     index = stream - worker->streams_buf;
     DRAWABLE_FOREACH_DPI(stream->current, ring_item, dpi) {
         dcc = dpi->dcc;
-        if (!display_channel_client_is_low_bandwidth(dcc)) {
-            continue;
-        }
         agent = &dcc->stream_agents[index];
 
         if (pipe_item_is_linked(&dpi->dpi_pipe_item)) {
-            ++agent->drops;
+            mjpeg_encoder_notify_frame_drop(agent->mjpeg_encoder);
         }
-
-        if (agent->frames / agent->fps < FPS_TEST_INTERVAL) {
-            agent->frames++;
-            return;
-        }
-
-        double drop_factor = ((double)agent->frames - (double)agent->drops) /
-                             (double)agent->frames;
-
-        if (drop_factor == 1) {
-            if (agent->fps < MAX_FPS) {
-                agent->fps++;
-            }
-        } else if (drop_factor < 0.9) {
-            if (agent->fps > 1) {
-                agent->fps--;
-            }
-        }
-        agent->frames = 1;
-        agent->drops = 0;
     }
 }
 
@@ -8094,6 +8081,7 @@  static inline int red_marshall_stream_data(RedChannelClient *rcc,
     RedWorker *worker = dcc->common.worker;
     int n;
     int width, height;
+    uint32_t fps;
 
     if (!stream) {
         spice_assert(drawable->sized_stream);
@@ -8125,8 +8113,9 @@  static inline int red_marshall_stream_data(RedChannelClient *rcc,
     StreamAgent *agent = &dcc->stream_agents[stream - worker->streams_buf];
     uint64_t time_now = red_now();
     size_t outbuf_size;
-    if (time_now - agent->last_send_time < (1000 * 1000 * 1000) / agent->fps) {
-        agent->frames--;
+
+    fps = mjpeg_encoder_get_fps(agent->mjpeg_encoder);
+    if (time_now - agent->last_send_time < (1000 * 1000 * 1000) / fps) {
         return TRUE;
     }