/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ /* Copyright (C) 2010 Red Hat, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see . */ #include "config.h" #include "spice-client.h" #include "spice-common.h" #include "spice-channel-priv.h" #include "channel-display-priv.h" /* MJpeg decoder implementation */ typedef struct MJpegDecoder { VideoDecoder base; /* ---------- The builtin mjpeg decoder ---------- */ struct jpeg_source_mgr mjpeg_src; struct jpeg_decompress_struct mjpeg_cinfo; struct jpeg_error_mgr mjpeg_jerr; /* ---------- Frame queue ---------- */ GQueue *msgq; SpiceMsgIn *cur_frame_msg; guint timer_id; /* ---------- Output frame data ---------- */ uint8_t *out_frame; uint32_t out_size; } MJpegDecoder; /* ---------- The JPEG library callbacks ---------- */ static void mjpeg_src_init(struct jpeg_decompress_struct *cinfo) { MJpegDecoder *decoder = SPICE_CONTAINEROF(cinfo->src, MJpegDecoder, mjpeg_src); uint8_t *data; cinfo->src->bytes_in_buffer = spice_msg_in_frame_data(decoder->cur_frame_msg, &data); cinfo->src->next_input_byte = data; } static boolean mjpeg_src_fill(struct jpeg_decompress_struct *cinfo) { g_critical("need more input data"); return 0; } static void mjpeg_src_skip(struct jpeg_decompress_struct *cinfo, long num_bytes) { cinfo->src->next_input_byte += num_bytes; } static void mjpeg_src_term(struct jpeg_decompress_struct *cinfo) { /* nothing */ } /* ---------- Decoder proper ---------- */ static void mjpeg_decoder_schedule(MJpegDecoder *decoder); /* main context */ static gboolean mjpeg_decoder_decode_frame(gpointer video_decoder) { MJpegDecoder *decoder = (MJpegDecoder*)video_decoder; gboolean back_compat = decoder->base.stream->channel->priv->peer_hdr.major_version == 1; JDIMENSION width, height; uint8_t *dest; uint8_t *lines[4]; jpeg_read_header(&decoder->mjpeg_cinfo, 1); width = decoder->mjpeg_cinfo.image_width; height = decoder->mjpeg_cinfo.image_height; if (decoder->out_size < width * height * 4) { g_free(decoder->out_frame); decoder->out_size = width * height * 4; decoder->out_frame = g_malloc(decoder->out_size); } dest = decoder->out_frame; #ifdef JCS_EXTENSIONS // requires jpeg-turbo if (back_compat) decoder->mjpeg_cinfo.out_color_space = JCS_EXT_RGBX; else decoder->mjpeg_cinfo.out_color_space = JCS_EXT_BGRX; #else #warning "You should consider building with libjpeg-turbo" decoder->mjpeg_cinfo.out_color_space = JCS_RGB; #endif #ifndef SPICE_QUALITY decoder->mjpeg_cinfo.dct_method = JDCT_IFAST; decoder->mjpeg_cinfo.do_fancy_upsampling = FALSE; decoder->mjpeg_cinfo.do_block_smoothing = FALSE; decoder->mjpeg_cinfo.dither_mode = JDITHER_ORDERED; #endif // TODO: in theory should check cinfo.output_height match with our height jpeg_start_decompress(&decoder->mjpeg_cinfo); /* rec_outbuf_height is the recommended size of the output buffer we * pass to libjpeg for optimum performance */ if (decoder->mjpeg_cinfo.rec_outbuf_height > G_N_ELEMENTS(lines)) { jpeg_abort_decompress(&decoder->mjpeg_cinfo); g_return_val_if_reached(G_SOURCE_REMOVE); } while (decoder->mjpeg_cinfo.output_scanline < decoder->mjpeg_cinfo.output_height) { /* only used when JCS_EXTENSIONS is undefined */ G_GNUC_UNUSED unsigned int lines_read; for (unsigned int j = 0; j < decoder->mjpeg_cinfo.rec_outbuf_height; j++) { lines[j] = dest; #ifdef JCS_EXTENSIONS dest += 4 * width; #else dest += 3 * width; #endif } lines_read = jpeg_read_scanlines(&decoder->mjpeg_cinfo, lines, decoder->mjpeg_cinfo.rec_outbuf_height); #ifndef JCS_EXTENSIONS { uint8_t *s = lines[0]; uint32_t *d = (uint32_t *)s; if (back_compat) { for (unsigned int j = lines_read * width; j > 0; ) { j -= 1; // reverse order, bad for cache? d[j] = s[j * 3 + 0] | s[j * 3 + 1] << 8 | s[j * 3 + 2] << 16; } } else { for (unsigned int j = lines_read * width; j > 0; ) { j -= 1; // reverse order, bad for cache? d[j] = s[j * 3 + 0] << 16 | s[j * 3 + 1] << 8 | s[j * 3 + 2]; } } } #endif dest = &(decoder->out_frame[decoder->mjpeg_cinfo.output_scanline * width * 4]); } jpeg_finish_decompress(&decoder->mjpeg_cinfo); /* Display the frame and dispose of it */ stream_display_frame(decoder->base.stream, decoder->cur_frame_msg, width, height, decoder->out_frame); spice_msg_in_unref(decoder->cur_frame_msg); decoder->cur_frame_msg = NULL; decoder->timer_id = 0; /* Schedule the next frame */ mjpeg_decoder_schedule(decoder); return G_SOURCE_REMOVE; } /* ---------- VideoDecoder's queue scheduling ---------- */ static void mjpeg_decoder_schedule(MJpegDecoder *decoder) { SPICE_DEBUG("%s", __FUNCTION__); if (decoder->timer_id) { return; } guint32 time = stream_get_time(decoder->base.stream); SpiceMsgIn *frame_msg = decoder->cur_frame_msg; decoder->cur_frame_msg = NULL; do { if (frame_msg) { SpiceStreamDataHeader *op = spice_msg_in_parsed(frame_msg); if (time <= op->multi_media_time) { guint32 d = op->multi_media_time - time; decoder->cur_frame_msg = frame_msg; decoder->timer_id = g_timeout_add(d, mjpeg_decoder_decode_frame, decoder); break; } SPICE_DEBUG("%s: rendering too late by %u ms (ts: %u, mmtime: %u), dropping ", __FUNCTION__, time - op->multi_media_time, op->multi_media_time, time); stream_dropped_frame_on_playback(decoder->base.stream); spice_msg_in_unref(frame_msg); } frame_msg = g_queue_pop_head(decoder->msgq); } while (frame_msg); } /* mjpeg_decoder_drop_queue() helper */ static void _msg_in_unref_func(gpointer data, gpointer user_data) { spice_msg_in_unref(data); } static void mjpeg_decoder_drop_queue(MJpegDecoder *decoder) { if (decoder->timer_id != 0) { g_source_remove(decoder->timer_id); decoder->timer_id = 0; } if (decoder->cur_frame_msg) { spice_msg_in_unref(decoder->cur_frame_msg); decoder->cur_frame_msg = NULL; } g_queue_foreach(decoder->msgq, _msg_in_unref_func, NULL); g_queue_clear(decoder->msgq); } /* ---------- VideoDecoder's public API ---------- */ static void mjpeg_decoder_queue_frame(VideoDecoder *video_decoder, SpiceMsgIn *frame_msg, int32_t latency) { MJpegDecoder *decoder = (MJpegDecoder*)video_decoder; SpiceMsgIn *last_msg; SPICE_DEBUG("%s", __FUNCTION__); last_msg = g_queue_peek_tail(decoder->msgq); if (last_msg) { SpiceStreamDataHeader *last_op, *frame_op; last_op = spice_msg_in_parsed(last_msg); frame_op = spice_msg_in_parsed(frame_msg); if (frame_op->multi_media_time < last_op->multi_media_time) { /* This should really not happen */ SPICE_DEBUG("new-frame-time < last-frame-time (%u < %u):" " resetting stream, id %u", frame_op->multi_media_time, last_op->multi_media_time, frame_op->id); mjpeg_decoder_drop_queue(decoder); } } /* Dropped MJPEG frames don't impact the ones that come after. * So drop late frames as early as possible to save on processing time. */ if (latency < 0) { return; } spice_msg_in_ref(frame_msg); g_queue_push_tail(decoder->msgq, frame_msg); mjpeg_decoder_schedule(decoder); } static void mjpeg_decoder_reschedule(VideoDecoder *video_decoder) { MJpegDecoder *decoder = (MJpegDecoder*)video_decoder; SPICE_DEBUG("%s", __FUNCTION__); if (decoder->timer_id != 0) { g_source_remove(decoder->timer_id); decoder->timer_id = 0; } mjpeg_decoder_schedule(decoder); } static void mjpeg_decoder_destroy(VideoDecoder* video_decoder) { MJpegDecoder *decoder = (MJpegDecoder*)video_decoder; mjpeg_decoder_drop_queue(decoder); jpeg_destroy_decompress(&decoder->mjpeg_cinfo); g_free(decoder->out_frame); free(decoder); } G_GNUC_INTERNAL VideoDecoder* create_mjpeg_decoder(int codec_type, display_stream *stream) { g_return_val_if_fail(codec_type == SPICE_VIDEO_CODEC_TYPE_MJPEG, NULL); MJpegDecoder *decoder = spice_new0(MJpegDecoder, 1); decoder->base.destroy = mjpeg_decoder_destroy; decoder->base.reschedule = mjpeg_decoder_reschedule; decoder->base.queue_frame = mjpeg_decoder_queue_frame; decoder->base.codec_type = codec_type; decoder->base.stream = stream; decoder->msgq = g_queue_new(); decoder->mjpeg_cinfo.err = jpeg_std_error(&decoder->mjpeg_jerr); jpeg_create_decompress(&decoder->mjpeg_cinfo); decoder->mjpeg_src.init_source = mjpeg_src_init; decoder->mjpeg_src.fill_input_buffer = mjpeg_src_fill; decoder->mjpeg_src.skip_input_data = mjpeg_src_skip; decoder->mjpeg_src.resync_to_restart = jpeg_resync_to_restart; decoder->mjpeg_src.term_source = mjpeg_src_term; decoder->mjpeg_cinfo.src = &decoder->mjpeg_src; /* All the other fields are initialized to zero by spice_new0(). */ return (VideoDecoder*)decoder; }