| /* GStreamer |
| * Copyright (C) <2005> Jan Schmidt <jan@fluendo.com> |
| * Copyright (C) <2002> Wim Taymans <wim@fluendo.com> |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 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 |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public |
| * License along with this library; if not, write to the |
| * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif |
| |
| #include "gstdvdsubdec.h" |
| #include "gstdvdsubparse.h" |
| #include <string.h> |
| |
| #define gst_dvd_sub_dec_parent_class parent_class |
| G_DEFINE_TYPE (GstDvdSubDec, gst_dvd_sub_dec, GST_TYPE_ELEMENT); |
| |
| static gboolean gst_dvd_sub_dec_src_event (GstPad * srcpad, GstObject * parent, |
| GstEvent * event); |
| static GstFlowReturn gst_dvd_sub_dec_chain (GstPad * pad, GstObject * parent, |
| GstBuffer * buf); |
| |
| static gboolean gst_dvd_sub_dec_handle_dvd_event (GstDvdSubDec * dec, |
| GstEvent * event); |
| static void gst_dvd_sub_dec_finalize (GObject * gobject); |
| static void gst_setup_palette (GstDvdSubDec * dec); |
| static void gst_dvd_sub_dec_merge_title (GstDvdSubDec * dec, |
| GstVideoFrame * frame); |
| static GstClockTime gst_dvd_sub_dec_get_event_delay (GstDvdSubDec * dec); |
| static gboolean gst_dvd_sub_dec_sink_event (GstPad * pad, GstObject * parent, |
| GstEvent * event); |
| static gboolean gst_dvd_sub_dec_sink_setcaps (GstPad * pad, GstCaps * caps); |
| |
| static GstFlowReturn gst_send_subtitle_frame (GstDvdSubDec * dec, |
| GstClockTime end_ts); |
| |
| static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", |
| GST_PAD_SRC, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("video/x-raw, format = (string) { AYUV, ARGB }," |
| "width = (int) 720, height = (int) 576, framerate = (fraction) 0/1") |
| ); |
| |
| static GstStaticPadTemplate subtitle_template = GST_STATIC_PAD_TEMPLATE ("sink", |
| GST_PAD_SINK, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("subpicture/x-dvd") |
| ); |
| |
| GST_DEBUG_CATEGORY_STATIC (gst_dvd_sub_dec_debug); |
| #define GST_CAT_DEFAULT (gst_dvd_sub_dec_debug) |
| |
| enum |
| { |
| SPU_FORCE_DISPLAY = 0x00, |
| SPU_SHOW = 0x01, |
| SPU_HIDE = 0x02, |
| SPU_SET_PALETTE = 0x03, |
| SPU_SET_ALPHA = 0x04, |
| SPU_SET_SIZE = 0x05, |
| SPU_SET_OFFSETS = 0x06, |
| SPU_WIPE = 0x07, |
| SPU_END = 0xff |
| }; |
| |
| static const guint32 default_clut[16] = { |
| 0xb48080, 0x248080, 0x628080, 0xd78080, |
| 0x808080, 0x808080, 0x808080, 0x808080, |
| 0x808080, 0x808080, 0x808080, 0x808080, |
| 0x808080, 0x808080, 0x808080, 0x808080 |
| }; |
| |
| typedef struct RLE_state |
| { |
| gint id; |
| gint aligned; |
| gint offset[2]; |
| gint hl_left; |
| gint hl_right; |
| |
| guchar *target; |
| |
| guchar next; |
| } |
| RLE_state; |
| |
| static void |
| gst_dvd_sub_dec_class_init (GstDvdSubDecClass * klass) |
| { |
| GObjectClass *gobject_class; |
| GstElementClass *gstelement_class; |
| |
| gobject_class = (GObjectClass *) klass; |
| gstelement_class = (GstElementClass *) klass; |
| |
| gobject_class->finalize = gst_dvd_sub_dec_finalize; |
| |
| gst_element_class_add_static_pad_template (gstelement_class, &src_template); |
| gst_element_class_add_static_pad_template (gstelement_class, |
| &subtitle_template); |
| |
| gst_element_class_set_static_metadata (gstelement_class, |
| "DVD subtitle decoder", "Codec/Decoder/Video", |
| "Decodes DVD subtitles into AYUV video frames", |
| "Wim Taymans <wim.taymans@gmail.com>, " |
| "Jan Schmidt <thaytan@mad.scientist.com>"); |
| } |
| |
| static void |
| gst_dvd_sub_dec_init (GstDvdSubDec * dec) |
| { |
| GstPadTemplate *tmpl; |
| |
| dec->sinkpad = gst_pad_new_from_static_template (&subtitle_template, "sink"); |
| gst_pad_set_chain_function (dec->sinkpad, |
| GST_DEBUG_FUNCPTR (gst_dvd_sub_dec_chain)); |
| gst_pad_set_event_function (dec->sinkpad, |
| GST_DEBUG_FUNCPTR (gst_dvd_sub_dec_sink_event)); |
| gst_element_add_pad (GST_ELEMENT (dec), dec->sinkpad); |
| |
| tmpl = gst_static_pad_template_get (&src_template); |
| dec->srcpad = gst_pad_new_from_template (tmpl, "src"); |
| gst_pad_set_event_function (dec->srcpad, |
| GST_DEBUG_FUNCPTR (gst_dvd_sub_dec_src_event)); |
| gst_pad_use_fixed_caps (dec->srcpad); |
| gst_object_unref (tmpl); |
| gst_element_add_pad (GST_ELEMENT (dec), dec->srcpad); |
| |
| /* FIXME: aren't there more possible sizes? (tpm) */ |
| dec->in_width = 720; |
| dec->in_height = 576; |
| |
| dec->partialbuf = NULL; |
| dec->have_title = FALSE; |
| dec->parse_pos = NULL; |
| dec->forced_display = FALSE; |
| dec->visible = FALSE; |
| |
| memcpy (dec->current_clut, default_clut, sizeof (guint32) * 16); |
| |
| gst_setup_palette (dec); |
| |
| dec->next_ts = 0; |
| dec->next_event_ts = GST_CLOCK_TIME_NONE; |
| |
| dec->buf_dirty = TRUE; |
| dec->use_ARGB = FALSE; |
| } |
| |
| static void |
| gst_dvd_sub_dec_finalize (GObject * gobject) |
| { |
| GstDvdSubDec *dec = GST_DVD_SUB_DEC (gobject); |
| |
| if (dec->partialbuf) { |
| gst_buffer_unmap (dec->partialbuf, &dec->partialmap); |
| gst_buffer_unref (dec->partialbuf); |
| dec->partialbuf = NULL; |
| } |
| |
| G_OBJECT_CLASS (parent_class)->finalize (gobject); |
| } |
| |
| static gboolean |
| gst_dvd_sub_dec_src_event (GstPad * pad, GstObject * parent, GstEvent * event) |
| { |
| gboolean res = FALSE; |
| |
| switch (GST_EVENT_TYPE (event)) { |
| default: |
| res = gst_pad_event_default (pad, parent, event); |
| break; |
| } |
| |
| return res; |
| } |
| |
| static GstClockTime |
| gst_dvd_sub_dec_get_event_delay (GstDvdSubDec * dec) |
| { |
| guchar *buf; |
| guint16 ticks; |
| GstClockTime event_delay; |
| |
| /* If starting a new buffer, follow the first DCSQ ptr */ |
| if (dec->parse_pos == dec->partialmap.data) { |
| buf = dec->parse_pos + dec->data_size; |
| } else { |
| buf = dec->parse_pos; |
| } |
| |
| ticks = GST_READ_UINT16_BE (buf); |
| event_delay = gst_util_uint64_scale (ticks, 1024 * GST_SECOND, 90000); |
| |
| GST_DEBUG_OBJECT (dec, "returning delay %" GST_TIME_FORMAT " from offset %u", |
| GST_TIME_ARGS (event_delay), (guint) (buf - dec->parse_pos)); |
| |
| return event_delay; |
| } |
| |
| /* |
| * Parse the next event time in the current subpicture buffer, stopping |
| * when time advances to the next state. |
| */ |
| static void |
| gst_dvd_sub_dec_parse_subpic (GstDvdSubDec * dec) |
| { |
| #define PARSE_BYTES_NEEDED(x) if ((buf+(x)) >= end) \ |
| { GST_WARNING("Subtitle stream broken parsing %c", *buf); \ |
| broken = TRUE; break; } |
| |
| guchar *start = dec->partialmap.data; |
| guchar *buf; |
| guchar *end; |
| gboolean broken = FALSE; |
| gboolean last_seq = FALSE; |
| guchar *next_seq = NULL; |
| GstClockTime event_time; |
| |
| /* nothing to do if we finished this buffer already */ |
| if (dec->parse_pos == NULL) |
| return; |
| |
| g_return_if_fail (dec->packet_size >= 4); |
| |
| end = start + dec->packet_size; |
| if (dec->parse_pos == start) { |
| buf = dec->parse_pos + dec->data_size; |
| } else { |
| buf = dec->parse_pos; |
| } |
| |
| g_assert (buf >= start && buf < end); |
| |
| /* If the next control sequence is at the current offset, this is |
| * the last one */ |
| next_seq = start + GST_READ_UINT16_BE (buf + 2); |
| last_seq = (next_seq == buf); |
| buf += 4; |
| |
| while ((buf < end) && (!broken)) { |
| switch (*buf) { |
| case SPU_FORCE_DISPLAY: /* Forced display menu subtitle */ |
| dec->forced_display = TRUE; |
| dec->buf_dirty = TRUE; |
| GST_DEBUG_OBJECT (dec, "SPU FORCE_DISPLAY"); |
| buf++; |
| break; |
| case SPU_SHOW: /* Show the subtitle in this packet */ |
| dec->visible = TRUE; |
| dec->buf_dirty = TRUE; |
| GST_DEBUG_OBJECT (dec, "SPU SHOW at %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (dec->next_event_ts)); |
| buf++; |
| break; |
| case SPU_HIDE: |
| /* 02 ff (ff) is the end of the packet, hide the subpicture */ |
| dec->visible = FALSE; |
| dec->buf_dirty = TRUE; |
| |
| GST_DEBUG_OBJECT (dec, "SPU HIDE at %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (dec->next_event_ts)); |
| buf++; |
| break; |
| case SPU_SET_PALETTE: /* palette */ |
| PARSE_BYTES_NEEDED (3); |
| |
| GST_DEBUG_OBJECT (dec, "SPU SET_PALETTE"); |
| |
| dec->subtitle_index[3] = buf[1] >> 4; |
| dec->subtitle_index[2] = buf[1] & 0xf; |
| dec->subtitle_index[1] = buf[2] >> 4; |
| dec->subtitle_index[0] = buf[2] & 0xf; |
| gst_setup_palette (dec); |
| |
| dec->buf_dirty = TRUE; |
| buf += 3; |
| break; |
| case SPU_SET_ALPHA: /* transparency palette */ |
| PARSE_BYTES_NEEDED (3); |
| |
| GST_DEBUG_OBJECT (dec, "SPU SET_ALPHA"); |
| |
| dec->subtitle_alpha[3] = buf[1] >> 4; |
| dec->subtitle_alpha[2] = buf[1] & 0xf; |
| dec->subtitle_alpha[1] = buf[2] >> 4; |
| dec->subtitle_alpha[0] = buf[2] & 0xf; |
| gst_setup_palette (dec); |
| |
| dec->buf_dirty = TRUE; |
| buf += 3; |
| break; |
| case SPU_SET_SIZE: /* image coordinates */ |
| PARSE_BYTES_NEEDED (7); |
| |
| dec->top = ((buf[4] & 0x3f) << 4) | ((buf[5] & 0xe0) >> 4); |
| dec->left = ((buf[1] & 0x3f) << 4) | ((buf[2] & 0xf0) >> 4); |
| dec->right = ((buf[2] & 0x03) << 8) | buf[3]; |
| dec->bottom = ((buf[5] & 0x03) << 8) | buf[6]; |
| |
| GST_DEBUG_OBJECT (dec, "SPU SET_SIZE left %d, top %d, right %d, " |
| "bottom %d", dec->left, dec->top, dec->right, dec->bottom); |
| |
| dec->buf_dirty = TRUE; |
| buf += 7; |
| break; |
| case SPU_SET_OFFSETS: /* image 1 / image 2 offsets */ |
| PARSE_BYTES_NEEDED (5); |
| |
| dec->offset[0] = (((guint) buf[1]) << 8) | buf[2]; |
| dec->offset[1] = (((guint) buf[3]) << 8) | buf[4]; |
| GST_DEBUG_OBJECT (dec, "Offset1 %d, Offset2 %d", |
| dec->offset[0], dec->offset[1]); |
| |
| dec->buf_dirty = TRUE; |
| buf += 5; |
| break; |
| case SPU_WIPE: |
| { |
| guint length; |
| |
| PARSE_BYTES_NEEDED (3); |
| |
| GST_WARNING_OBJECT (dec, "SPU_WIPE not yet implemented"); |
| |
| length = (buf[1] << 8) | (buf[2]); |
| buf += 1 + length; |
| |
| dec->buf_dirty = TRUE; |
| break; |
| } |
| case SPU_END: |
| buf = (last_seq) ? end : next_seq; |
| |
| /* Start a new control sequence */ |
| if (buf + 4 < end) { |
| guint16 ticks = GST_READ_UINT16_BE (buf); |
| |
| event_time = gst_util_uint64_scale (ticks, 1024 * GST_SECOND, 90000); |
| |
| GST_DEBUG_OBJECT (dec, |
| "Next DCSQ at offset %u, delay %g secs (%d ticks)", |
| (guint) (buf - start), |
| gst_util_guint64_to_gdouble (event_time / GST_SECOND), ticks); |
| |
| dec->parse_pos = buf; |
| if (event_time > 0) { |
| dec->next_event_ts += event_time; |
| |
| GST_LOG_OBJECT (dec, "Exiting parse loop with time %g", |
| gst_guint64_to_gdouble (dec->next_event_ts) / |
| gst_guint64_to_gdouble (GST_SECOND)); |
| return; |
| } |
| break; |
| } else { |
| dec->parse_pos = NULL; |
| dec->next_event_ts = GST_CLOCK_TIME_NONE; |
| GST_LOG_OBJECT (dec, "Finished all cmds. Exiting parse loop"); |
| return; |
| } |
| default: |
| GST_ERROR |
| ("Invalid sequence in subtitle packet header (%.2x). Skipping", |
| *buf); |
| broken = TRUE; |
| dec->parse_pos = NULL; |
| break; |
| } |
| } |
| } |
| |
| static inline int |
| gst_get_nibble (guchar * buffer, RLE_state * state) |
| { |
| if (state->aligned) { |
| state->next = buffer[state->offset[state->id]++]; |
| state->aligned = 0; |
| return state->next >> 4; |
| } else { |
| state->aligned = 1; |
| return state->next & 0xf; |
| } |
| } |
| |
| /* Premultiply the current lookup table into the "target" cache */ |
| static void |
| gst_setup_palette (GstDvdSubDec * dec) |
| { |
| gint i; |
| guint32 col; |
| Color_val *target_yuv = dec->palette_cache_yuv; |
| Color_val *target2_yuv = dec->hl_palette_cache_yuv; |
| Color_val *target_rgb = dec->palette_cache_rgb; |
| Color_val *target2_rgb = dec->hl_palette_cache_rgb; |
| |
| for (i = 0; i < 4; i++, target2_yuv++, target_yuv++) { |
| col = dec->current_clut[dec->subtitle_index[i]]; |
| target_yuv->Y_R = (col >> 16) & 0xff; |
| target_yuv->V_B = (col >> 8) & 0xff; |
| target_yuv->U_G = col & 0xff; |
| target_yuv->A = dec->subtitle_alpha[i] * 0xff / 0xf; |
| |
| col = dec->current_clut[dec->menu_index[i]]; |
| target2_yuv->Y_R = (col >> 16) & 0xff; |
| target2_yuv->V_B = (col >> 8) & 0xff; |
| target2_yuv->U_G = col & 0xff; |
| target2_yuv->A = dec->menu_alpha[i] * 0xff / 0xf; |
| |
| /* If ARGB flag set, then convert YUV palette to RGB */ |
| /* Using integer aritmetic */ |
| if (dec->use_ARGB) { |
| guchar C = target_yuv->Y_R - 16; |
| guchar D = target_yuv->U_G - 128; |
| guchar E = target_yuv->V_B - 128; |
| |
| target_rgb->Y_R = CLAMP (((298 * C + 409 * E + 128) >> 8), 0, 255); |
| target_rgb->U_G = |
| CLAMP (((298 * C - 100 * D - 128 * E + 128) >> 8), 0, 255); |
| target_rgb->V_B = CLAMP (((298 * C + 516 * D + 128) >> 8), 0, 255); |
| target_rgb->A = target_yuv->A; |
| |
| C = target2_yuv->Y_R - 16; |
| D = target2_yuv->U_G - 128; |
| E = target2_yuv->V_B - 128; |
| |
| target2_rgb->Y_R = CLAMP (((298 * C + 409 * E + 128) >> 8), 0, 255); |
| target2_rgb->U_G = |
| CLAMP (((298 * C - 100 * D - 128 * E + 128) >> 8), 0, 255); |
| target2_rgb->V_B = CLAMP (((298 * C + 516 * D + 128) >> 8), 0, 255); |
| target2_rgb->A = target2_yuv->A; |
| } |
| target_rgb++; |
| target2_rgb++; |
| } |
| } |
| |
| static inline guint |
| gst_get_rle_code (guchar * buffer, RLE_state * state) |
| { |
| gint code; |
| |
| code = gst_get_nibble (buffer, state); |
| if (code < 0x4) { /* 4 .. f */ |
| code = (code << 4) | gst_get_nibble (buffer, state); |
| if (code < 0x10) { /* 1x .. 3x */ |
| code = (code << 4) | gst_get_nibble (buffer, state); |
| if (code < 0x40) { /* 04x .. 0fx */ |
| code = (code << 4) | gst_get_nibble (buffer, state); |
| } |
| } |
| } |
| return code; |
| } |
| |
| #define DRAW_RUN(target,len,c) \ |
| G_STMT_START { \ |
| gint i = 0; \ |
| if ((c)->A) { \ |
| for (i = 0; i < (len); i++) { \ |
| *(target)++ = (c)->A; \ |
| *(target)++ = (c)->Y_R; \ |
| *(target)++ = (c)->U_G; \ |
| *(target)++ = (c)->V_B; \ |
| } \ |
| } else { \ |
| (target) += 4 * (len); \ |
| } \ |
| } G_STMT_END |
| |
| /* |
| * This function steps over each run-length segment, drawing |
| * into the YUVA/ARGB buffers as it goes. UV are composited and then output |
| * at half width/height |
| */ |
| static void |
| gst_draw_rle_line (GstDvdSubDec * dec, guchar * buffer, RLE_state * state) |
| { |
| gint length, colourid; |
| guint code; |
| gint x, right; |
| guchar *target; |
| |
| target = state->target; |
| |
| x = dec->left; |
| right = dec->right + 1; |
| |
| while (x < right) { |
| gboolean in_hl; |
| const Color_val *colour_entry; |
| |
| code = gst_get_rle_code (buffer, state); |
| length = code >> 2; |
| colourid = code & 3; |
| if (dec->use_ARGB) |
| colour_entry = dec->palette_cache_rgb + colourid; |
| else |
| colour_entry = dec->palette_cache_yuv + colourid; |
| |
| /* Length = 0 implies fill to the end of the line */ |
| /* Restrict the colour run to the end of the line */ |
| if (length == 0 || x + length > right) |
| length = right - x; |
| |
| /* Check if this run of colour touches the highlight region */ |
| in_hl = ((x <= state->hl_right) && (x + length) >= state->hl_left); |
| if (in_hl) { |
| gint run; |
| |
| /* Draw to the left of the highlight */ |
| if (x <= state->hl_left) { |
| run = MIN (length, state->hl_left - x + 1); |
| |
| DRAW_RUN (target, run, colour_entry); |
| length -= run; |
| x += run; |
| } |
| |
| /* Draw across the highlight region */ |
| if (x <= state->hl_right) { |
| const Color_val *hl_colour; |
| if (dec->use_ARGB) |
| hl_colour = dec->hl_palette_cache_rgb + colourid; |
| else |
| hl_colour = dec->hl_palette_cache_yuv + colourid; |
| |
| run = MIN (length, state->hl_right - x + 1); |
| |
| DRAW_RUN (target, run, hl_colour); |
| length -= run; |
| x += run; |
| } |
| } |
| |
| /* Draw the rest of the run */ |
| if (length > 0) { |
| DRAW_RUN (target, length, colour_entry); |
| x += length; |
| } |
| } |
| } |
| |
| /* |
| * Decode the RLE subtitle image and blend with the current |
| * frame buffer. |
| */ |
| static void |
| gst_dvd_sub_dec_merge_title (GstDvdSubDec * dec, GstVideoFrame * frame) |
| { |
| gint y; |
| gint Y_stride; |
| guchar *buffer = dec->partialmap.data; |
| gint hl_top, hl_bottom; |
| gint last_y; |
| RLE_state state; |
| guint8 *Y_data; |
| |
| GST_DEBUG_OBJECT (dec, "Merging subtitle on frame"); |
| |
| Y_data = GST_VIDEO_FRAME_PLANE_DATA (frame, 0); |
| Y_stride = GST_VIDEO_FRAME_PLANE_STRIDE (frame, 0); |
| |
| state.id = 0; |
| state.aligned = 1; |
| state.next = 0; |
| state.offset[0] = dec->offset[0]; |
| state.offset[1] = dec->offset[1]; |
| |
| /* center the image when display rectangle exceeds the video width */ |
| if (dec->in_width <= dec->right) { |
| gint left, disp_width; |
| |
| disp_width = dec->right - dec->left + 1; |
| left = (dec->in_width - disp_width) / 2; |
| dec->left = left; |
| dec->right = left + disp_width - 1; |
| |
| /* if it clips to the right, shift it left, but only till zero */ |
| if (dec->right >= dec->in_width) { |
| gint shift = dec->right - dec->in_width - 1; |
| if (shift > dec->left) |
| shift = dec->left; |
| dec->left -= shift; |
| dec->right -= shift; |
| } |
| |
| GST_DEBUG_OBJECT (dec, "clipping width to %d,%d", |
| dec->left, dec->in_width - 1); |
| } |
| |
| /* for the height, bring it up till it fits as well as it can. We |
| * assume the picture is in the lower part. We should better check where it |
| * is and do something more clever. */ |
| if (dec->in_height <= dec->bottom) { |
| |
| /* shift it up, but only till zero */ |
| gint shift = dec->bottom - dec->in_height - 1; |
| if (shift > dec->top) |
| shift = dec->top; |
| dec->top -= shift; |
| dec->bottom -= shift; |
| |
| /* start on even line */ |
| if (dec->top & 1) { |
| dec->top--; |
| dec->bottom--; |
| } |
| |
| GST_DEBUG_OBJECT (dec, "clipping height to %d,%d", |
| dec->top, dec->in_height - 1); |
| } |
| |
| if (dec->current_button) { |
| hl_top = dec->hl_top; |
| hl_bottom = dec->hl_bottom; |
| } else { |
| hl_top = -1; |
| hl_bottom = -1; |
| } |
| last_y = MIN (dec->bottom, dec->in_height); |
| |
| y = dec->top; |
| state.target = Y_data + 4 * dec->left + (y * Y_stride); |
| |
| /* Now draw scanlines until we hit last_y or end of RLE data */ |
| for (; ((state.offset[1] < dec->data_size + 2) && (y <= last_y)); y++) { |
| /* Set up to draw the highlight if we're in the right scanlines */ |
| if (y > hl_bottom || y < hl_top) { |
| state.hl_left = -1; |
| state.hl_right = -1; |
| } else { |
| state.hl_left = dec->hl_left; |
| state.hl_right = dec->hl_right; |
| } |
| gst_draw_rle_line (dec, buffer, &state); |
| |
| state.target += Y_stride; |
| |
| /* Realign the RLE state for the next line */ |
| if (!state.aligned) |
| gst_get_nibble (buffer, &state); |
| state.id = !state.id; |
| } |
| } |
| |
| static void |
| gst_send_empty_fill (GstDvdSubDec * dec, GstClockTime ts) |
| { |
| if (dec->next_ts < ts) { |
| GST_LOG_OBJECT (dec, "Sending GAP event update to advance time to %" |
| GST_TIME_FORMAT, GST_TIME_ARGS (ts)); |
| |
| gst_pad_push_event (dec->srcpad, |
| gst_event_new_gap (dec->next_ts, ts - dec->next_ts)); |
| } |
| dec->next_ts = ts; |
| } |
| |
| static GstFlowReturn |
| gst_send_subtitle_frame (GstDvdSubDec * dec, GstClockTime end_ts) |
| { |
| GstFlowReturn flow; |
| GstBuffer *out_buf; |
| GstVideoFrame frame; |
| guint8 *data; |
| gint x, y; |
| static GstAllocationParams params = { 0, 3, 0, 0, }; |
| |
| g_assert (dec->have_title); |
| g_assert (dec->next_ts <= end_ts); |
| |
| /* Check if we need to redraw the output buffer */ |
| if (!dec->buf_dirty) { |
| flow = GST_FLOW_OK; |
| goto out; |
| } |
| |
| out_buf = |
| gst_buffer_new_allocate (NULL, GST_VIDEO_INFO_SIZE (&dec->info), ¶ms); |
| gst_video_frame_map (&frame, &dec->info, out_buf, GST_MAP_READWRITE); |
| |
| data = GST_VIDEO_FRAME_PLANE_DATA (&frame, 0); |
| |
| /* Clear the buffer */ |
| /* FIXME - move this into the buffer rendering code */ |
| for (y = 0; y < dec->in_height; y++) { |
| guchar *line = data + 4 * dec->in_width * y; |
| |
| for (x = 0; x < dec->in_width; x++) { |
| line[0] = 0; /* A */ |
| if (!dec->use_ARGB) { |
| line[1] = 16; /* Y */ |
| line[2] = 128; /* U */ |
| line[3] = 128; /* V */ |
| } else { |
| line[1] = 0; /* R */ |
| line[2] = 0; /* G */ |
| line[3] = 0; /* B */ |
| } |
| |
| line += 4; |
| } |
| } |
| |
| /* FIXME: do we really want to honour the forced_display flag |
| * for subtitles streans? */ |
| if (dec->visible || dec->forced_display) { |
| gst_dvd_sub_dec_merge_title (dec, &frame); |
| } |
| |
| gst_video_frame_unmap (&frame); |
| |
| dec->buf_dirty = FALSE; |
| |
| GST_BUFFER_TIMESTAMP (out_buf) = dec->next_ts; |
| if (GST_CLOCK_TIME_IS_VALID (dec->next_event_ts)) { |
| GST_BUFFER_DURATION (out_buf) = GST_CLOCK_DIFF (dec->next_ts, |
| dec->next_event_ts); |
| } else { |
| GST_BUFFER_DURATION (out_buf) = GST_CLOCK_TIME_NONE; |
| } |
| |
| GST_DEBUG_OBJECT (dec, "Sending subtitle buffer with ts %" |
| GST_TIME_FORMAT ", dur %" G_GINT64_FORMAT, |
| GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (out_buf)), |
| GST_BUFFER_DURATION (out_buf)); |
| |
| flow = gst_pad_push (dec->srcpad, out_buf); |
| |
| out: |
| dec->next_ts = end_ts; |
| return flow; |
| } |
| |
| /* Walk time forward, processing any subtitle events as needed. */ |
| static GstFlowReturn |
| gst_dvd_sub_dec_advance_time (GstDvdSubDec * dec, GstClockTime new_ts) |
| { |
| GstFlowReturn ret = GST_FLOW_OK; |
| |
| GST_LOG_OBJECT (dec, "Advancing time to %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (new_ts)); |
| |
| if (!dec->have_title) { |
| gst_send_empty_fill (dec, new_ts); |
| return ret; |
| } |
| |
| while (dec->next_ts < new_ts) { |
| GstClockTime next_ts = new_ts; |
| |
| if (GST_CLOCK_TIME_IS_VALID (dec->next_event_ts) && |
| dec->next_event_ts < next_ts) { |
| /* We might need to process the subtitle cmd queue */ |
| next_ts = dec->next_event_ts; |
| } |
| |
| /* |
| * Now, either output a filler or a frame spanning |
| * dec->next_ts to next_ts |
| */ |
| if (dec->visible || dec->forced_display) { |
| ret = gst_send_subtitle_frame (dec, next_ts); |
| } else { |
| gst_send_empty_fill (dec, next_ts); |
| } |
| |
| /* |
| * and then process some subtitle cmds if we need |
| */ |
| if (next_ts == dec->next_event_ts) |
| gst_dvd_sub_dec_parse_subpic (dec); |
| } |
| |
| return ret; |
| } |
| |
| static GstFlowReturn |
| gst_dvd_sub_dec_chain (GstPad * pad, GstObject * parent, GstBuffer * buf) |
| { |
| GstFlowReturn ret = GST_FLOW_OK; |
| GstDvdSubDec *dec; |
| guint8 *data; |
| glong size = 0; |
| |
| dec = GST_DVD_SUB_DEC (parent); |
| |
| GST_DEBUG_OBJECT (dec, "Have buffer of size %" G_GSIZE_FORMAT ", ts %" |
| GST_TIME_FORMAT ", dur %" G_GINT64_FORMAT, gst_buffer_get_size (buf), |
| GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buf)), GST_BUFFER_DURATION (buf)); |
| |
| if (GST_BUFFER_TIMESTAMP_IS_VALID (buf)) { |
| if (!GST_CLOCK_TIME_IS_VALID (dec->next_ts)) { |
| dec->next_ts = GST_BUFFER_TIMESTAMP (buf); |
| } |
| |
| /* Move time forward to the start of the new buffer */ |
| ret = gst_dvd_sub_dec_advance_time (dec, GST_BUFFER_TIMESTAMP (buf)); |
| } |
| |
| if (dec->have_title) { |
| gst_buffer_unmap (dec->partialbuf, &dec->partialmap); |
| gst_buffer_unref (dec->partialbuf); |
| dec->partialbuf = NULL; |
| dec->have_title = FALSE; |
| } |
| |
| GST_DEBUG_OBJECT (dec, "Got subtitle buffer, pts %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buf))); |
| |
| /* deal with partial frame from previous buffer */ |
| if (dec->partialbuf) { |
| gst_buffer_unmap (dec->partialbuf, &dec->partialmap); |
| dec->partialbuf = gst_buffer_append (dec->partialbuf, buf); |
| } else { |
| dec->partialbuf = buf; |
| } |
| |
| gst_buffer_map (dec->partialbuf, &dec->partialmap, GST_MAP_READ); |
| |
| data = dec->partialmap.data; |
| size = dec->partialmap.size; |
| |
| if (size > 4) { |
| dec->packet_size = GST_READ_UINT16_BE (data); |
| |
| if (dec->packet_size == size) { |
| GST_LOG_OBJECT (dec, "Subtitle packet size %d, current size %ld", |
| dec->packet_size, size); |
| |
| dec->data_size = GST_READ_UINT16_BE (data + 2); |
| |
| /* Reset parameters for a new subtitle buffer */ |
| dec->parse_pos = data; |
| dec->forced_display = FALSE; |
| dec->visible = FALSE; |
| |
| dec->have_title = TRUE; |
| dec->next_event_ts = GST_BUFFER_TIMESTAMP (dec->partialbuf); |
| |
| if (!GST_CLOCK_TIME_IS_VALID (dec->next_event_ts)) |
| dec->next_event_ts = dec->next_ts; |
| |
| dec->next_event_ts += gst_dvd_sub_dec_get_event_delay (dec); |
| } |
| } |
| |
| return ret; |
| } |
| |
| static gboolean |
| gst_dvd_sub_dec_sink_setcaps (GstPad * pad, GstCaps * caps) |
| { |
| GstDvdSubDec *dec = GST_DVD_SUB_DEC (gst_pad_get_parent (pad)); |
| gboolean ret = FALSE; |
| GstCaps *out_caps = NULL, *peer_caps = NULL; |
| |
| GST_DEBUG_OBJECT (dec, "setcaps called with %" GST_PTR_FORMAT, caps); |
| |
| out_caps = gst_caps_new_simple ("video/x-raw", |
| "format", G_TYPE_STRING, "AYUV", |
| "width", G_TYPE_INT, dec->in_width, |
| "height", G_TYPE_INT, dec->in_height, |
| "framerate", GST_TYPE_FRACTION, 0, 1, NULL); |
| |
| peer_caps = gst_pad_get_allowed_caps (dec->srcpad); |
| if (G_LIKELY (peer_caps)) { |
| guint i = 0, n = 0; |
| |
| n = gst_caps_get_size (peer_caps); |
| GST_DEBUG_OBJECT (dec, "peer allowed caps (%u structure(s)) are %" |
| GST_PTR_FORMAT, n, peer_caps); |
| |
| for (i = 0; i < n; i++) { |
| GstStructure *s = gst_caps_get_structure (peer_caps, i); |
| /* Check if the peer pad support ARGB format, if yes change caps */ |
| if (gst_structure_has_name (s, "video/x-raw")) { |
| GstCaps *downstream_caps; |
| gst_caps_unref (out_caps); |
| GST_DEBUG_OBJECT (dec, "trying with ARGB"); |
| |
| out_caps = gst_caps_new_simple ("video/x-raw", |
| "format", G_TYPE_STRING, "ARGB", |
| "width", G_TYPE_INT, dec->in_width, |
| "height", G_TYPE_INT, dec->in_height, |
| "framerate", GST_TYPE_FRACTION, 0, 1, NULL); |
| |
| downstream_caps = gst_pad_peer_query_caps (dec->srcpad, NULL); |
| if (gst_caps_can_intersect (downstream_caps, out_caps)) { |
| gst_caps_unref (downstream_caps); |
| GST_DEBUG_OBJECT (dec, "peer accepted ARGB"); |
| /* If ARGB format then set the flag */ |
| dec->use_ARGB = TRUE; |
| break; |
| } |
| gst_caps_unref (downstream_caps); |
| } |
| } |
| gst_caps_unref (peer_caps); |
| } |
| GST_DEBUG_OBJECT (dec, "setting caps downstream to %" GST_PTR_FORMAT, |
| out_caps); |
| if (gst_pad_set_caps (dec->srcpad, out_caps)) { |
| gst_video_info_from_caps (&dec->info, out_caps); |
| } else { |
| GST_WARNING_OBJECT (dec, "failed setting downstream caps"); |
| gst_caps_unref (out_caps); |
| goto beach; |
| } |
| |
| gst_caps_unref (out_caps); |
| ret = TRUE; |
| |
| beach: |
| gst_object_unref (dec); |
| return ret; |
| } |
| |
| static gboolean |
| gst_dvd_sub_dec_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) |
| { |
| GstDvdSubDec *dec = GST_DVD_SUB_DEC (parent); |
| gboolean ret = FALSE; |
| |
| GST_LOG_OBJECT (dec, "%s event", GST_EVENT_TYPE_NAME (event)); |
| |
| switch (GST_EVENT_TYPE (event)) { |
| case GST_EVENT_CAPS: |
| { |
| GstCaps *caps; |
| |
| gst_event_parse_caps (event, &caps); |
| ret = gst_dvd_sub_dec_sink_setcaps (pad, caps); |
| gst_event_unref (event); |
| break; |
| } |
| case GST_EVENT_CUSTOM_DOWNSTREAM:{ |
| GstClockTime ts = GST_EVENT_TIMESTAMP (event); |
| |
| if (gst_event_has_name (event, "application/x-gst-dvd")) { |
| if (GST_CLOCK_TIME_IS_VALID (ts)) |
| gst_dvd_sub_dec_advance_time (dec, ts); |
| |
| if (gst_dvd_sub_dec_handle_dvd_event (dec, event)) { |
| /* gst_dvd_sub_dec_advance_time (dec, dec->next_ts + GST_SECOND / 30.0); */ |
| gst_event_unref (event); |
| ret = TRUE; |
| break; |
| } |
| } |
| |
| ret = gst_pad_event_default (pad, parent, event); |
| break; |
| } |
| case GST_EVENT_GAP: |
| { |
| GstClockTime start, duration; |
| |
| gst_event_parse_gap (event, &start, &duration); |
| if (GST_CLOCK_TIME_IS_VALID (start)) { |
| if (GST_CLOCK_TIME_IS_VALID (duration)) |
| start += duration; |
| /* we do not expect another buffer until after gap, |
| * so that is our position now */ |
| GST_DEBUG_OBJECT (dec, "Got GAP event, advancing time from %" |
| GST_TIME_FORMAT " to %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (dec->next_ts), GST_TIME_ARGS (start)); |
| |
| gst_dvd_sub_dec_advance_time (dec, start); |
| } else { |
| GST_WARNING_OBJECT (dec, "Got GAP event with invalid position"); |
| } |
| |
| gst_event_unref (event); |
| ret = TRUE; |
| break; |
| } |
| case GST_EVENT_SEGMENT: |
| { |
| GstSegment seg; |
| |
| gst_event_copy_segment (event, &seg); |
| |
| { |
| #if 0 |
| /* Turn off forced highlight display */ |
| dec->forced_display = 0; |
| dec->current_button = 0; |
| #endif |
| if (dec->partialbuf) { |
| gst_buffer_unmap (dec->partialbuf, &dec->partialmap); |
| gst_buffer_unref (dec->partialbuf); |
| dec->partialbuf = NULL; |
| dec->have_title = FALSE; |
| } |
| |
| if (GST_CLOCK_TIME_IS_VALID (seg.time)) |
| dec->next_ts = seg.time; |
| else |
| dec->next_ts = GST_CLOCK_TIME_NONE; |
| |
| GST_DEBUG_OBJECT (dec, "Got newsegment, new time = %" |
| GST_TIME_FORMAT, GST_TIME_ARGS (dec->next_ts)); |
| |
| ret = gst_pad_event_default (pad, parent, event); |
| } |
| break; |
| } |
| case GST_EVENT_FLUSH_STOP:{ |
| /* Turn off forced highlight display */ |
| dec->forced_display = 0; |
| dec->current_button = 0; |
| |
| if (dec->partialbuf) { |
| gst_buffer_unmap (dec->partialbuf, &dec->partialmap); |
| gst_buffer_unref (dec->partialbuf); |
| dec->partialbuf = NULL; |
| dec->have_title = FALSE; |
| } |
| |
| ret = gst_pad_event_default (pad, parent, event); |
| break; |
| } |
| default:{ |
| ret = gst_pad_event_default (pad, parent, event); |
| break; |
| } |
| } |
| return ret; |
| } |
| |
| static gboolean |
| gst_dvd_sub_dec_handle_dvd_event (GstDvdSubDec * dec, GstEvent * event) |
| { |
| GstStructure *structure; |
| const gchar *event_name; |
| |
| structure = (GstStructure *) gst_event_get_structure (event); |
| |
| if (structure == NULL) |
| goto not_handled; |
| |
| event_name = gst_structure_get_string (structure, "event"); |
| |
| GST_LOG_OBJECT (dec, |
| "DVD event %s with timestamp %" G_GINT64_FORMAT " on sub pad", |
| GST_STR_NULL (event_name), GST_EVENT_TIMESTAMP (event)); |
| |
| if (event_name == NULL) |
| goto not_handled; |
| |
| if (strcmp (event_name, "dvd-spu-highlight") == 0) { |
| gint button; |
| gint palette, sx, sy, ex, ey; |
| gint i; |
| |
| /* Details for the highlight region to display */ |
| if (!gst_structure_get_int (structure, "button", &button) || |
| !gst_structure_get_int (structure, "palette", &palette) || |
| !gst_structure_get_int (structure, "sx", &sx) || |
| !gst_structure_get_int (structure, "sy", &sy) || |
| !gst_structure_get_int (structure, "ex", &ex) || |
| !gst_structure_get_int (structure, "ey", &ey)) { |
| GST_ERROR_OBJECT (dec, "Invalid dvd-spu-highlight event received"); |
| return TRUE; |
| } |
| dec->current_button = button; |
| dec->hl_left = sx; |
| dec->hl_top = sy; |
| dec->hl_right = ex; |
| dec->hl_bottom = ey; |
| for (i = 0; i < 4; i++) { |
| dec->menu_alpha[i] = ((guint32) (palette) >> (i * 4)) & 0x0f; |
| dec->menu_index[i] = ((guint32) (palette) >> (16 + (i * 4))) & 0x0f; |
| } |
| |
| GST_DEBUG_OBJECT (dec, "New button activated highlight=(%d,%d) to (%d,%d) " |
| "palette 0x%x", sx, sy, ex, ey, palette); |
| gst_setup_palette (dec); |
| |
| dec->buf_dirty = TRUE; |
| } else if (strcmp (event_name, "dvd-spu-clut-change") == 0) { |
| /* Take a copy of the colour table */ |
| gchar name[16]; |
| int i; |
| gint value; |
| |
| GST_LOG_OBJECT (dec, "New colour table received"); |
| for (i = 0; i < 16; i++) { |
| g_snprintf (name, sizeof (name), "clut%02d", i); |
| if (!gst_structure_get_int (structure, name, &value)) { |
| GST_ERROR_OBJECT (dec, "dvd-spu-clut-change event did not " |
| "contain %s field", name); |
| break; |
| } |
| dec->current_clut[i] = (guint32) (value); |
| } |
| |
| gst_setup_palette (dec); |
| |
| dec->buf_dirty = TRUE; |
| } else if (strcmp (event_name, "dvd-spu-stream-change") == 0 |
| || strcmp (event_name, "dvd-spu-reset-highlight") == 0) { |
| /* Turn off forced highlight display */ |
| dec->current_button = 0; |
| |
| GST_LOG_OBJECT (dec, "Clearing button state"); |
| dec->buf_dirty = TRUE; |
| } else if (strcmp (event_name, "dvd-spu-still-frame") == 0) { |
| /* Handle a still frame */ |
| GST_LOG_OBJECT (dec, "Received still frame notification"); |
| } else { |
| goto not_handled; |
| } |
| |
| return TRUE; |
| |
| not_handled: |
| { |
| /* Ignore all other unknown events */ |
| GST_LOG_OBJECT (dec, "Ignoring other custom event %" GST_PTR_FORMAT, |
| structure); |
| return FALSE; |
| } |
| } |
| |
| static gboolean |
| plugin_init (GstPlugin * plugin) |
| { |
| if (!gst_element_register (plugin, "dvdsubdec", GST_RANK_NONE, |
| GST_TYPE_DVD_SUB_DEC) || |
| !gst_element_register (plugin, "dvdsubparse", GST_RANK_NONE, |
| GST_TYPE_DVD_SUB_PARSE)) { |
| return FALSE; |
| } |
| |
| GST_DEBUG_CATEGORY_INIT (gst_dvd_sub_dec_debug, "dvdsubdec", 0, |
| "DVD subtitle decoder"); |
| |
| return TRUE; |
| } |
| |
| GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, |
| GST_VERSION_MINOR, |
| dvdsub, |
| "DVD subtitle parser and decoder", plugin_init, |
| VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN); |