diff options
-rw-r--r-- | server/mjpeg_encoder.c | 384 | ||||
-rw-r--r-- | server/mjpeg_encoder.h | 24 | ||||
-rw-r--r-- | server/red_worker.c | 3 |
3 files changed, 401 insertions, 10 deletions
diff --git a/server/mjpeg_encoder.c b/server/mjpeg_encoder.c index 28c7e69f..fd0bae35 100644 --- a/server/mjpeg_encoder.c +++ b/server/mjpeg_encoder.c @@ -37,6 +37,17 @@ static const int mjpeg_quality_samples[MJPEG_QUALITY_SAMPLE_NUM] = {20, 30, 40, #define MJPEG_AVERAGE_SIZE_WINDOW 3 +#define MJPEG_BIT_RATE_EVAL_MIN_NUM_FRAMES 3 +#define MJPEG_LOW_FPS_RATE_TH 3 + +/* + * acting on positive client reports only if enough frame mm time + * has passed since the last bit rate change and the report. + * time + */ +#define MJPEG_CLIENT_POSITIVE_REPORT_TIMEOUT 2000 +#define MJPEG_CLIENT_POSITIVE_REPORT_STRICT_TIMEOUT 3000 + enum { MJPEG_QUALITY_EVAL_TYPE_SET, MJPEG_QUALITY_EVAL_TYPE_UPGRADE, @@ -64,6 +75,22 @@ typedef struct MJpegEncoderQualityEval { int max_sampled_fps_quality_id; } MJpegEncoderQualityEval; +typedef struct MJpegEncoderClientState { + int max_video_latency; + uint32_t max_audio_latency; +} MJpegEncoderClientState; + +typedef struct MJpegEncoderBitRateInfo { + uint64_t change_start_time; + uint64_t last_frame_time; + uint32_t change_start_mm_time; + int was_upgraded; + + /* gathering data about the frames that + * were encoded since the last bit rate change*/ + uint32_t num_enc_frames; + uint64_t sum_enc_size; +} MJpegEncoderBitRateInfo; /* * Adjusting the stream jpeg quality and frame rate (fps): * When during_quality_eval=TRUE, we compress different frames with different @@ -77,6 +104,8 @@ typedef struct MJpegEncoderQualityEval { typedef struct MJpegEncoderRateControl { int during_quality_eval; MJpegEncoderQualityEval quality_eval_data; + MJpegEncoderBitRateInfo bit_rate_info; + MJpegEncoderClientState client_state; uint64_t byte_rate; int quality_id; @@ -580,8 +609,10 @@ static void mjpeg_encoder_adjust_params_to_bit_rate(MJpegEncoder *encoder) int mjpeg_encoder_start_frame(MJpegEncoder *encoder, SpiceBitmapFmt format, int width, int height, - uint8_t **dest, size_t *dest_len) + uint8_t **dest, size_t *dest_len, + uint32_t frame_mm_time) { + MJpegEncoderBitRateInfo *bit_rate_info; uint32_t quality; mjpeg_encoder_adjust_params_to_bit_rate(encoder); @@ -632,6 +663,23 @@ int mjpeg_encoder_start_frame(MJpegEncoder *encoder, SpiceBitmapFmt format, spice_jpeg_mem_dest(&encoder->cinfo, dest, dest_len); + if (!encoder->rate_control.during_quality_eval || + encoder->rate_control.quality_eval_data.reason == MJPEG_QUALITY_EVAL_REASON_SIZE_CHANGE) { + struct timespec time; + uint64_t now; + + clock_gettime(CLOCK_MONOTONIC, &time); + now = ((uint64_t) time.tv_sec) * 1000000000 + time.tv_nsec; + + bit_rate_info = &encoder->rate_control.bit_rate_info; + + if (!bit_rate_info->change_start_time) { + bit_rate_info->change_start_time = now; + bit_rate_info->change_start_mm_time = frame_mm_time; + } + bit_rate_info->last_frame_time = now; + } + encoder->cinfo.image_width = width; encoder->cinfo.image_height = height; jpeg_set_defaults(&encoder->cinfo); @@ -680,13 +728,19 @@ size_t mjpeg_encoder_end_frame(MJpegEncoder *encoder) encoder->first_frame = FALSE; rate_control->last_enc_size = dest->pub.next_output_byte - dest->buffer; - if (!rate_control->during_quality_eval) { - if (rate_control->num_recent_enc_frames >= MJPEG_AVERAGE_SIZE_WINDOW) { - rate_control->num_recent_enc_frames = 0; - rate_control->sum_recent_enc_size = 0; + if (!rate_control->during_quality_eval || + rate_control->quality_eval_data.reason == MJPEG_QUALITY_EVAL_REASON_SIZE_CHANGE) { + + if (!rate_control->during_quality_eval) { + if (rate_control->num_recent_enc_frames >= MJPEG_AVERAGE_SIZE_WINDOW) { + rate_control->num_recent_enc_frames = 0; + rate_control->sum_recent_enc_size = 0; + } + rate_control->sum_recent_enc_size += rate_control->last_enc_size; + rate_control->num_recent_enc_frames++; } - rate_control->sum_recent_enc_size += rate_control->last_enc_size; - rate_control->num_recent_enc_frames++; + rate_control->bit_rate_info.sum_enc_size += encoder->rate_control.last_enc_size; + rate_control->bit_rate_info.num_enc_frames++; } return encoder->rate_control.last_enc_size; } @@ -698,3 +752,319 @@ uint32_t mjpeg_encoder_get_fps(MJpegEncoder *encoder) } return encoder->rate_control.fps; } + +static void mjpeg_encoder_quality_eval_stop(MJpegEncoder *encoder) +{ + MJpegEncoderRateControl *rate_control = &encoder->rate_control; + uint32_t quality_id; + uint32_t fps; + + if (!rate_control->during_quality_eval) { + return; + } + switch (rate_control->quality_eval_data.type) { + case MJPEG_QUALITY_EVAL_TYPE_UPGRADE: + quality_id = rate_control->quality_eval_data.min_quality_id; + fps = rate_control->quality_eval_data.min_quality_fps; + break; + case MJPEG_QUALITY_EVAL_TYPE_DOWNGRADE: + quality_id = rate_control->quality_eval_data.max_quality_id; + fps = rate_control->quality_eval_data.max_quality_fps; + break; + case MJPEG_QUALITY_EVAL_TYPE_SET: + quality_id = MJPEG_QUALITY_SAMPLE_NUM / 2; + fps = MJPEG_MAX_FPS / 2; + break; + default: + spice_warning("unexected"); + return; + } + mjpeg_encoder_reset_quality(encoder, quality_id, fps, 0); + spice_debug("during quality evaluation: canceling." + "reset quality to %d fps %d", + mjpeg_quality_samples[rate_control->quality_id], rate_control->fps); +} + +static void mjpeg_encoder_decrease_bit_rate(MJpegEncoder *encoder) +{ + MJpegEncoderRateControl *rate_control = &encoder->rate_control; + MJpegEncoderBitRateInfo *bit_rate_info = &rate_control->bit_rate_info; + uint64_t measured_byte_rate; + uint32_t measured_fps; + uint64_t decrease_size; + + mjpeg_encoder_quality_eval_stop(encoder); + + rate_control->client_state.max_video_latency = 0; + rate_control->client_state.max_audio_latency = 0; + + if (bit_rate_info->num_enc_frames > MJPEG_BIT_RATE_EVAL_MIN_NUM_FRAMES || + bit_rate_info->num_enc_frames > rate_control->fps) { + double duration_sec; + + duration_sec = (bit_rate_info->last_frame_time - bit_rate_info->change_start_time); + duration_sec /= (1000.0 * 1000.0 * 1000.0); + measured_byte_rate = bit_rate_info->sum_enc_size / duration_sec; + measured_fps = bit_rate_info->num_enc_frames / duration_sec; + decrease_size = bit_rate_info->sum_enc_size / bit_rate_info->num_enc_frames; + spice_debug("bit rate esitimation %.2f (Mbps) fps %u", + measured_byte_rate*8/1024.0/1024, + measured_fps); + } else { + measured_byte_rate = rate_control->byte_rate; + measured_fps = rate_control->fps; + decrease_size = measured_byte_rate/measured_fps; + spice_debug("bit rate not re-estimated %.2f (Mbps) fps %u", + measured_byte_rate*8/1024.0/1024, + measured_fps); + } + + measured_byte_rate = MIN(rate_control->byte_rate, measured_byte_rate); + + if (decrease_size >= measured_byte_rate) { + decrease_size = measured_byte_rate / 2; + } + + rate_control->byte_rate = measured_byte_rate - decrease_size; + bit_rate_info->change_start_time = 0; + bit_rate_info->change_start_mm_time = 0; + bit_rate_info->last_frame_time = 0; + bit_rate_info->num_enc_frames = 0; + bit_rate_info->sum_enc_size = 0; + bit_rate_info->was_upgraded = FALSE; + + spice_debug("decrease bit rate %.2f (Mbps)", rate_control->byte_rate * 8 / 1024.0/1024.0); + mjpeg_encoder_quality_eval_set_downgrade(encoder, + MJPEG_QUALITY_EVAL_REASON_RATE_CHANGE, + rate_control->quality_id, + rate_control->fps); +} + +static void mjpeg_encoder_handle_negative_client_stream_report(MJpegEncoder *encoder, + uint32_t report_end_frame_mm_time) +{ + MJpegEncoderRateControl *rate_control = &encoder->rate_control; + + spice_debug(NULL); + + if ((rate_control->bit_rate_info.change_start_mm_time > report_end_frame_mm_time || + !rate_control->bit_rate_info.change_start_mm_time) && + !rate_control->bit_rate_info.was_upgraded) { + spice_debug("ignoring, a downgrade has already occurred later to the report time"); + return; + } + + mjpeg_encoder_decrease_bit_rate(encoder); +} + +static void mjpeg_encoder_increase_bit_rate(MJpegEncoder *encoder) +{ + MJpegEncoderRateControl *rate_control = &encoder->rate_control; + MJpegEncoderBitRateInfo *bit_rate_info = &rate_control->bit_rate_info; + uint64_t measured_byte_rate; + uint32_t measured_fps; + uint64_t increase_size; + + + if (bit_rate_info->num_enc_frames > MJPEG_BIT_RATE_EVAL_MIN_NUM_FRAMES || + bit_rate_info->num_enc_frames > rate_control->fps) { + uint64_t avg_frame_size; + double duration_sec; + + duration_sec = (bit_rate_info->last_frame_time - bit_rate_info->change_start_time); + duration_sec /= (1000.0 * 1000.0 * 1000.0); + measured_byte_rate = bit_rate_info->sum_enc_size / duration_sec; + measured_fps = bit_rate_info->num_enc_frames / duration_sec; + avg_frame_size = bit_rate_info->sum_enc_size / bit_rate_info->num_enc_frames; + spice_debug("bit rate esitimation %.2f (Mbps) defined %.2f" + " fps %u avg-frame-size=%.2f (KB)", + measured_byte_rate*8/1024.0/1024, + rate_control->byte_rate*8/1024.0/1024, + measured_fps, + avg_frame_size/1024.0); + increase_size = avg_frame_size; + } else { + spice_debug("not enough samples for measuring the bit rate. no change"); + return; + } + + + mjpeg_encoder_quality_eval_stop(encoder); + + if (measured_byte_rate + increase_size < rate_control->byte_rate) { + spice_debug("measured byte rate is small: not upgrading, just re-evaluating"); + } else { + rate_control->byte_rate = MIN(measured_byte_rate, rate_control->byte_rate) + increase_size; + } + + bit_rate_info->change_start_time = 0; + bit_rate_info->change_start_mm_time = 0; + bit_rate_info->last_frame_time = 0; + bit_rate_info->num_enc_frames = 0; + bit_rate_info->sum_enc_size = 0; + bit_rate_info->was_upgraded = TRUE; + + spice_debug("increase bit rate %.2f (Mbps)", rate_control->byte_rate * 8 / 1024.0/1024.0); + mjpeg_encoder_quality_eval_set_upgrade(encoder, + MJPEG_QUALITY_EVAL_REASON_RATE_CHANGE, + rate_control->quality_id, + rate_control->fps); +} +static void mjpeg_encoder_handle_positive_client_stream_report(MJpegEncoder *encoder, + uint32_t report_start_frame_mm_time) +{ + MJpegEncoderRateControl *rate_control = &encoder->rate_control; + MJpegEncoderBitRateInfo *bit_rate_info = &rate_control->bit_rate_info; + int stable_client_mm_time; + int timeout; + + if (rate_control->during_quality_eval && + rate_control->quality_eval_data.reason == MJPEG_QUALITY_EVAL_REASON_RATE_CHANGE) { + spice_debug("during quality evaluation (rate change). ignoring report"); + return; + } + + if ((rate_control->fps > MJPEG_IMPROVE_QUALITY_FPS_STRICT_TH || + rate_control->fps >= encoder->cbs.get_source_fps(encoder->cbs_opaque)) && + rate_control->quality_id > MJPEG_QUALITY_SAMPLE_NUM / 2) { + timeout = MJPEG_CLIENT_POSITIVE_REPORT_STRICT_TIMEOUT; + } else { + timeout = MJPEG_CLIENT_POSITIVE_REPORT_TIMEOUT; + } + + stable_client_mm_time = (int)report_start_frame_mm_time - bit_rate_info->change_start_mm_time; + + if (!bit_rate_info->change_start_mm_time || stable_client_mm_time < timeout) { + /* assessing the stability of the current setting and only then + * respond to the report */ + spice_debug("no drops, but not enough time has passed for assessing" + "the playback stability since the last bit rate change"); + return; + } + mjpeg_encoder_increase_bit_rate(encoder); +} + +/* + * the video playback jitter buffer should be at least (send_time*2 + net_latency) for + * preventing underflow + */ +static uint32_t get_min_required_playback_delay(uint64_t frame_enc_size, + uint64_t byte_rate, + uint32_t latency) +{ + uint32_t one_frame_time; + + if (!frame_enc_size || !byte_rate) { + return latency; + } + one_frame_time = (frame_enc_size*1000)/byte_rate; + + return one_frame_time*2 + latency; +} + +#define MJPEG_PLAYBACK_LATENCY_DECREASE_FACTOR 0.5 +#define MJPEG_VIDEO_VS_AUDIO_LATENCY_FACTOR 1.25 +#define MJPEG_VIDEO_DELAY_TH -15 + +void mjpeg_encoder_client_stream_report(MJpegEncoder *encoder, + uint32_t num_frames, + uint32_t num_drops, + uint32_t start_frame_mm_time, + uint32_t end_frame_mm_time, + int32_t end_frame_delay, + uint32_t audio_delay) +{ + MJpegEncoderRateControl *rate_control = &encoder->rate_control; + MJpegEncoderClientState *client_state = &rate_control->client_state; + uint64_t avg_enc_size = 0; + uint32_t min_playback_delay; + + spice_debug("client report: #frames %u, #drops %d, duration %u video-delay %d audio-delay %u", + num_frames, num_drops, + end_frame_mm_time - start_frame_mm_time, + end_frame_delay, audio_delay); + + if (!encoder->rate_control_is_active) { + spice_debug("rate control was not activated: ignoring"); + return; + } + if (rate_control->during_quality_eval) { + if (rate_control->quality_eval_data.type == MJPEG_QUALITY_EVAL_TYPE_DOWNGRADE && + rate_control->quality_eval_data.reason == MJPEG_QUALITY_EVAL_REASON_RATE_CHANGE) { + spice_debug("during rate downgrade evaluation"); + return; + } + } + + if (rate_control->num_recent_enc_frames) { + avg_enc_size = rate_control->sum_recent_enc_size / + rate_control->num_recent_enc_frames; + } + spice_debug("recent size avg %.2f (KB)", avg_enc_size / 1024.0); + min_playback_delay = get_min_required_playback_delay(avg_enc_size, rate_control->byte_rate, + mjpeg_encoder_get_latency(encoder)); + spice_debug("min-delay %u client-delay %d", min_playback_delay, end_frame_delay); + + /* + * If the audio latency has decreased (since the start of the current + * sequence of positive reports), and the video latency is bigger, slow down + * the video rate + */ + if (end_frame_delay > 0 && + audio_delay < MJPEG_PLAYBACK_LATENCY_DECREASE_FACTOR*client_state->max_audio_latency && + end_frame_delay > MJPEG_VIDEO_VS_AUDIO_LATENCY_FACTOR*audio_delay) { + spice_debug("video_latency >> audio_latency && audio_latency << max (%u)", + client_state->max_audio_latency); + mjpeg_encoder_handle_negative_client_stream_report(encoder, + end_frame_mm_time); + return; + } + + if (end_frame_delay < MJPEG_VIDEO_DELAY_TH) { + mjpeg_encoder_handle_negative_client_stream_report(encoder, + end_frame_mm_time); + } else { + int is_video_delay_small = FALSE; + double major_delay_decrease_thresh; + double medium_delay_decrease_thresh; + + client_state->max_video_latency = MAX(end_frame_delay, client_state->max_video_latency); + client_state->max_audio_latency = MAX(audio_delay, client_state->max_audio_latency); + + if (min_playback_delay > end_frame_delay) { + uint32_t src_fps = encoder->cbs.get_source_fps(encoder->cbs_opaque); + /* + * if the stream is at its highest rate, we can't estimate the "real" + * network bit rate and the min_playback_delay + */ + if (rate_control->quality_id != MJPEG_QUALITY_SAMPLE_NUM - 1 || + rate_control->fps < MIN(src_fps, MJPEG_MAX_FPS)) { + is_video_delay_small = TRUE; + } + } + + medium_delay_decrease_thresh = client_state->max_video_latency; + medium_delay_decrease_thresh *= MJPEG_PLAYBACK_LATENCY_DECREASE_FACTOR; + + major_delay_decrease_thresh = medium_delay_decrease_thresh; + major_delay_decrease_thresh *= MJPEG_PLAYBACK_LATENCY_DECREASE_FACTOR; + /* + * since the bit rate and the required latency are only evaluation based on the + * reports we got till now, we assume that the latency is too low only if it + * was higher during the time that passed since the last report that resulted + * in a bit rate decrement. If we find that the latency has decreased, it might + * suggest that the stream bit rate is too high. + */ + if ((end_frame_delay < medium_delay_decrease_thresh && + is_video_delay_small) || end_frame_delay < major_delay_decrease_thresh) { + spice_debug("downgrade due to short video delay (last=%u, past-max=%u", + end_frame_delay, client_state->max_video_latency); + mjpeg_encoder_handle_negative_client_stream_report(encoder, + end_frame_mm_time); + } else if (!num_drops) { + mjpeg_encoder_handle_positive_client_stream_report(encoder, + start_frame_mm_time); + + } + } +} diff --git a/server/mjpeg_encoder.h b/server/mjpeg_encoder.h index 902dcbe7..cc49edfb 100644 --- a/server/mjpeg_encoder.h +++ b/server/mjpeg_encoder.h @@ -48,7 +48,8 @@ uint8_t mjpeg_encoder_get_bytes_per_pixel(MJpegEncoder *encoder); */ int mjpeg_encoder_start_frame(MJpegEncoder *encoder, SpiceBitmapFmt format, int width, int height, - uint8_t **dest, size_t *dest_len); + uint8_t **dest, size_t *dest_len, + uint32_t frame_mm_time); int mjpeg_encoder_encode_scanline(MJpegEncoder *encoder, uint8_t *src_pixels, size_t image_width); size_t mjpeg_encoder_end_frame(MJpegEncoder *encoder); @@ -63,5 +64,24 @@ size_t mjpeg_encoder_end_frame(MJpegEncoder *encoder); */ uint32_t mjpeg_encoder_get_fps(MJpegEncoder *encoder); - +/* + * Data that should be periodically obtained from the client. The report contains: + * num_frames : the number of frames that reached the client during the time + * the report is referring to. + * num_drops : the part of the above frames that was dropped by the client due to + * late arrival time. + * start_frame_mm_time: the mm_time of the first frame included in the report + * end_frame_mm_time : the mm_time of the last_frame included in the report + * end_frame_delay : (end_frame_mm_time - client_mm_time) + * audio delay : the latency of the audio playback. + * If there is no audio playback, set it to MAX_UINT. + * + */ +void mjpeg_encoder_client_stream_report(MJpegEncoder *encoder, + uint32_t num_frames, + uint32_t num_drops, + uint32_t start_frame_mm_time, + uint32_t end_frame_mm_time, + int32_t end_frame_delay, + uint32_t audio_delay); #endif diff --git a/server/red_worker.c b/server/red_worker.c index 23d08a82..b453023a 100644 --- a/server/red_worker.c +++ b/server/red_worker.c @@ -8392,7 +8392,8 @@ static inline int red_marshall_stream_data(RedChannelClient *rcc, if (!mjpeg_encoder_start_frame(agent->mjpeg_encoder, image->u.bitmap.format, width, height, &dcc->send_data.stream_outbuf, - &outbuf_size)) { + &outbuf_size, + drawable->red_drawable->mm_time)) { return FALSE; } if (!encode_frame(dcc, &drawable->red_drawable->u.copy.src_area, |