--- /dev/null
+/*****************************************************************************
+ * scte27.c : SCTE-27 subtitles decoder
+ *****************************************************************************
+ * Copyright (C) Laurent Aimar
+ * $Id$
+ *
+ * Authors: Laurent Aimar <fenrir _AT_ videolan _DOT_ org>
+ *
+ * This program 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 program 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 program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_codec.h>
+#include <vlc_bits.h>
+
+#include <assert.h>
+
+/*****************************************************************************
+ * Module descriptor.
+ *****************************************************************************/
+static int Open (vlc_object_t *);
+static void Close(vlc_object_t *);
+
+vlc_module_begin ()
+ set_description(N_("SCTE-27 decoder"))
+ set_shortname(N_("SCTE-27"))
+ set_capability( "decoder", 51)
+ set_category(CAT_INPUT)
+ set_subcategory(SUBCAT_INPUT_SCODEC)
+ set_callbacks(Open, Close)
+vlc_module_end ()
+
+/****************************************************************************
+ * Local prototypes
+ ****************************************************************************/
+struct decoder_sys_t {
+ int segment_id;
+ int segment_size;
+ uint8_t *segment_buffer;
+ mtime_t segment_date;
+};
+
+typedef struct {
+ uint8_t y, u, v;
+ uint8_t alpha;
+} scte27_color_t;
+
+static const scte27_color_t scte27_color_transparent = {
+ .y = 0x00,
+ .u = 0x80,
+ .v = 0x80,
+ .alpha = 0x00,
+};
+
+static scte27_color_t bs_read_color(bs_t *bs)
+{
+ scte27_color_t color;
+
+ /* XXX it's unclear if a value of 0 in Y/U/V means a transparent pixel */
+ color.y = bs_read(bs, 5) << 3;
+ color.alpha = bs_read1(bs) ? 0xff : 0x80;
+ color.u = bs_read(bs, 5) << 3;
+ color.v = bs_read(bs, 5) << 3;
+
+ return color;
+}
+
+static inline void SetPixel(picture_t *picture, int x, int y, int value)
+{
+ picture->p->p_pixels[y * picture->p->i_pitch + x] = value;
+}
+
+static subpicture_region_t *DecodeSimpleBitmap(decoder_t *dec,
+ const uint8_t *data, int size)
+{
+ VLC_UNUSED(dec);
+ /* Parse the bitmap and its properties */
+ bs_t bs;
+ bs_init(&bs, data, size);
+
+ bs_skip(&bs, 5);
+ int is_framed = bs_read(&bs, 1);
+ int outline_style = bs_read(&bs, 2);
+ scte27_color_t character_color = bs_read_color(&bs);
+ int top_h = bs_read(&bs, 12);
+ int top_v = bs_read(&bs, 12);
+ int bottom_h = bs_read(&bs, 12);
+ int bottom_v = bs_read(&bs, 12);
+ if (top_h >= bottom_h || top_v >= bottom_v)
+ return NULL;
+ int frame_top_h = top_h;
+ int frame_top_v = top_v;
+ int frame_bottom_h = bottom_h;
+ int frame_bottom_v = bottom_v;
+ scte27_color_t frame_color = scte27_color_transparent;
+ if (is_framed) {
+ frame_top_h = bs_read(&bs, 12);
+ frame_top_v = bs_read(&bs, 12);
+ frame_bottom_h = bs_read(&bs, 12);
+ frame_bottom_v = bs_read(&bs, 12);
+ frame_color = bs_read_color(&bs);
+ if (frame_top_h > top_h ||
+ frame_top_v > top_v ||
+ frame_bottom_h < bottom_h ||
+ frame_bottom_v < bottom_v)
+ return NULL;
+ }
+ int outline_thickness = 0;
+ scte27_color_t outline_color = scte27_color_transparent;
+ int shadow_right = 0;
+ int shadow_bottom = 0;
+ scte27_color_t shadow_color = scte27_color_transparent;
+ if (outline_style == 1) {
+ bs_skip(&bs, 4);
+ outline_thickness = bs_read(&bs, 4);
+ outline_color = bs_read_color(&bs);
+ } else if (outline_style == 2) {
+ shadow_right = bs_read(&bs, 4);
+ shadow_bottom = bs_read(&bs, 4);
+ shadow_color = bs_read_color(&bs);
+ } else if (outline_style == 3) {
+ bs_skip(&bs, 24);
+ }
+ bs_skip(&bs, 16); // bitmap_compressed_length
+ int bitmap_h = bottom_h - top_h;
+ int bitmap_v = bottom_v - top_v;
+ int bitmap_size = bitmap_h * bitmap_v;
+ bool *bitmap = malloc(bitmap_size * sizeof(*bitmap));
+ if (!bitmap)
+ return NULL;
+ for (int position = 0; position < bitmap_size;) {
+ if (bs_eof(&bs)) {
+ for (; position < bitmap_size; position++)
+ bitmap[position] = false;
+ break;
+ }
+
+ int run_on_length = 0;
+ int run_off_length = 0;
+ if (!bs_read1(&bs)) {
+ if (!bs_read1(&bs)) {
+ if (!bs_read1(&bs)) {
+ if (bs_read(&bs, 2) == 1) {
+ int next = __MIN((position / bitmap_h + 1) * bitmap_h,
+ bitmap_size);
+ for (; position < next; position++)
+ bitmap[position] = false;
+ }
+ } else {
+ run_on_length = 4;
+ }
+ } else {
+ run_off_length = 6;
+ }
+ } else {
+ run_on_length = 3;
+ run_off_length = 5;
+ }
+
+ if (run_on_length > 0) {
+ int run = bs_read(&bs, run_on_length);
+ if (!run)
+ run = 1 << run_on_length;
+ for (; position < bitmap_size && run > 0; position++, run--)
+ bitmap[position] = true;
+ }
+ if (run_off_length > 0) {
+ int run = bs_read(&bs, run_off_length);
+ if (!run)
+ run = 1 << run_off_length;
+ for (; position < bitmap_size && run > 0; position++, run--)
+ bitmap[position] = false;
+ }
+ }
+
+ /* Render the bitmap into a subpicture_region_t */
+
+ /* Reserve the place for the style
+ * FIXME It's unclear if it is needed or if the bitmap should already include
+ * the needed margin (I think the samples I have do both). */
+ int margin_h = 0;
+ int margin_v = 0;
+ if (outline_style == 1) {
+ margin_h =
+ margin_v = outline_thickness;
+ } else if (outline_style == 2) {
+ margin_h = shadow_right;
+ margin_v = shadow_bottom;
+ }
+ frame_top_h -= margin_h;
+ frame_top_v -= margin_v;
+ frame_bottom_h += margin_h;
+ frame_bottom_v += margin_v;
+
+ const int frame_h = frame_bottom_h - frame_top_h;
+ const int frame_v = frame_bottom_v - frame_top_v;
+ const int bitmap_oh = top_h - frame_top_h;
+ const int bitmap_ov = top_v - frame_top_v;
+
+ enum {
+ COLOR_FRAME,
+ COLOR_CHARACTER,
+ COLOR_OUTLINE,
+ COLOR_SHADOW,
+ };
+ video_palette_t palette = {
+ .i_entries = 4,
+ .palette = {
+ [COLOR_FRAME] = {
+ frame_color.y,
+ frame_color.u,
+ frame_color.v,
+ frame_color.alpha
+ },
+ [COLOR_CHARACTER] = {
+ character_color.y,
+ character_color.u,
+ character_color.v,
+ character_color.alpha
+ },
+ [COLOR_OUTLINE] = {
+ outline_color.y,
+ outline_color.u,
+ outline_color.v,
+ outline_color.alpha
+ },
+ [COLOR_SHADOW] = {
+ shadow_color.y,
+ shadow_color.u,
+ shadow_color.v,
+ shadow_color.alpha
+ },
+ },
+ };
+ video_format_t fmt = {
+ .i_chroma = VLC_CODEC_YUVP,
+ .i_width = frame_h,
+ .i_height = frame_v,
+ .i_sar_num = 0, /* Use video AR */
+ .i_sar_den = 1,
+ .p_palette = &palette,
+ };
+ subpicture_region_t *r = subpicture_region_New(&fmt);
+ if (!r) {
+ free(bitmap);
+ return NULL;
+ }
+ r->i_x = frame_top_h;
+ r->i_y = frame_top_v;
+
+ /* Fill up with frame (background) color */
+ for (int y = 0; y < frame_v; y++)
+ memset(&r->p_picture->p->p_pixels[y * r->p_picture->p->i_pitch],
+ COLOR_FRAME,
+ frame_h);
+
+ /* Draw the outline/shadow if requested */
+ if (outline_style == 1) {
+ /* Draw an outline
+ * XXX simple but slow and of low quality (no anti-aliasing) */
+ bool circle[16][16];
+ for (int dy = 0; dy <= 15; dy++) {
+ for (int dx = 0; dx <= 15; dx++)
+ circle[dy][dx] = (dx > 0 || dy > 0) &&
+ dx * dx + dy * dy <= outline_thickness * outline_thickness;
+ }
+ for (int by = 0; by < bitmap_v; by++) {
+ for (int bx = 0; bx < bitmap_h; bx++) {
+ if (!bitmap[by * bitmap_h + bx])
+ continue;
+ for (int dy = 0; dy <= outline_thickness; dy++) {
+ for (int dx = 0; dx <= outline_thickness; dx++) {
+ if (circle[dy][dx]) {
+ SetPixel(r->p_picture,
+ bx + bitmap_oh + dx, by + bitmap_ov + dy, COLOR_OUTLINE);
+ SetPixel(r->p_picture,
+ bx + bitmap_oh - dx, by + bitmap_ov + dy, COLOR_OUTLINE);
+ SetPixel(r->p_picture,
+ bx + bitmap_oh + dx, by + bitmap_ov - dy, COLOR_OUTLINE);
+ SetPixel(r->p_picture,
+ bx + bitmap_oh - dx, by + bitmap_ov - dy, COLOR_OUTLINE);
+ }
+ }
+ }
+ }
+ }
+ } else if (outline_style == 2) {
+ /* Draw a shadow by drawing the character shifted by shaddow right/bottom */
+ for (int by = 0; by < bitmap_v; by++) {
+ for (int bx = 0; bx < bitmap_h; bx++) {
+ if (bitmap[by * bitmap_h + bx])
+ SetPixel(r->p_picture,
+ bx + bitmap_oh + shadow_right,
+ by + bitmap_ov + shadow_bottom,
+ COLOR_SHADOW);
+ }
+ }
+ }
+
+ /* Draw the character */
+ for (int by = 0; by < bitmap_v; by++) {
+ for (int bx = 0; bx < bitmap_h; bx++) {
+ if (bitmap[by * bitmap_h + bx])
+ SetPixel(r->p_picture,
+ bx + bitmap_oh, by + bitmap_ov, COLOR_CHARACTER);
+ }
+ }
+ free(bitmap);
+ return r;
+}
+
+static subpicture_t *DecodeSubtitleMessage(decoder_t *dec,
+ const uint8_t *data, int size,
+ mtime_t date)
+{
+ if (size < 12)
+ goto error;
+
+ /* Parse the header */
+ bool pre_clear_display = data[3] & 0x80;
+ int display_standard = data[3] & 0x1f;
+ int subtitle_type = data[8] >> 4;
+ int display_duration = ((data[8] & 0x07) << 8) | data[9];
+ int block_length = GetWBE(&data[10]);
+
+ size -= 12;
+ data += 12;
+
+ if (block_length > size)
+ goto error;
+
+ if (subtitle_type == 1) {
+ subpicture_region_t *region = DecodeSimpleBitmap(dec, data, block_length);
+ if (!region)
+ goto error;
+ subpicture_t *sub = decoder_NewSubpicture(dec, NULL);
+ if (!sub) {
+ subpicture_region_Delete(region);
+ return NULL;
+ }
+ int frame_duration;
+ switch (display_standard) {
+ case 0:
+ sub->i_original_picture_width = 720;
+ sub->i_original_picture_height = 480;
+ frame_duration = 33367;
+ break;
+ case 1:
+ sub->i_original_picture_width = 720;
+ sub->i_original_picture_height = 576;
+ frame_duration = 40000;
+ break;
+ case 2:
+ sub->i_original_picture_width = 1280;
+ sub->i_original_picture_height = 720;
+ frame_duration = 16683;
+ break;
+ case 3:
+ sub->i_original_picture_width = 1920;
+ sub->i_original_picture_height = 1080;
+ frame_duration = 16683;
+ break;
+ default:
+ msg_Warn(dec, "Unknown display standard");
+ sub->i_original_picture_width = 0;
+ sub->i_original_picture_height = 0;
+ frame_duration = 40000;
+ break;
+ }
+ sub->b_absolute = true;
+ if (!pre_clear_display)
+ msg_Warn(dec, "SCTE-27 subtitles without pre_clear_display flag are not well supported");
+ sub->b_ephemer = true;
+ sub->i_start = date;
+ sub->i_stop = date + display_duration * frame_duration;
+ sub->p_region = region;
+
+ return sub;
+ } else {
+ /* Reserved */
+ return NULL;
+ }
+
+error:
+ msg_Err(dec, "corrupted subtitle_message");
+ return NULL;
+}
+
+static subpicture_t *Decode(decoder_t *dec, block_t **block)
+{
+ decoder_sys_t *sys = dec->p_sys;
+
+ if (block == NULL || *block == NULL)
+ return NULL;
+ block_t *b = *block; *block = NULL;
+
+ subpicture_t *sub_first = NULL;
+ subpicture_t **sub_last = &sub_first;
+
+ if (b->i_flags & (BLOCK_FLAG_DISCONTINUITY|BLOCK_FLAG_CORRUPTED))
+ goto exit;
+
+ while (b->i_buffer > 3) {
+ const int table_id = b->p_buffer[0];
+ if (table_id != 0xc6) {
+ //if (table_id != 0xff)
+ // msg_Err(dec, "Invalid SCTE-27 table id (0x%x)", table_id);
+ break;
+ }
+ const int section_length = ((b->p_buffer[1] & 0xf) << 8) | b->p_buffer[2];
+ if (section_length <= 1 + 4 || b->i_buffer < 3 + (unsigned)section_length) {
+ msg_Err(dec, "Invalid SCTE-27 section length");
+ break;
+ }
+ const int protocol_version = b->p_buffer[3] & 0x3f;
+ if (protocol_version != 0) {
+ msg_Err(dec, "Unsupported SCTE-27 protocol version (%d)", protocol_version);
+ break;
+ }
+ const bool segmentation_overlay = b->p_buffer[3] & 0x40;
+
+ subpicture_t *sub = NULL;
+ if (segmentation_overlay) {
+ if (section_length < 1 + 5 + 4)
+ break;
+ int id = GetWBE(&b->p_buffer[4]);
+ int last = (b->p_buffer[6] << 4) | (b->p_buffer[7] >> 4);
+ int index = ((b->p_buffer[7] & 0x0f) << 8) | b->p_buffer[8];
+ if (index > last)
+ break;
+ if (index == 0) {
+ sys->segment_id = id;
+ sys->segment_size = 0;
+ sys->segment_date = b->i_pts > VLC_TS_INVALID ? b->i_pts : b->i_dts;
+ } else {
+ if (sys->segment_id != id || sys->segment_size <= 0) {
+ sys->segment_id = -1;
+ break;
+ }
+ }
+
+ int segment_size = section_length - 1 - 5 - 4;
+
+ sys->segment_buffer = xrealloc(sys->segment_buffer,
+ sys->segment_size + segment_size);
+ memcpy(&sys->segment_buffer[sys->segment_size],
+ &b->p_buffer[9], segment_size);
+ sys->segment_size += segment_size;
+
+ if (index == last) {
+ sub = DecodeSubtitleMessage(dec,
+ sys->segment_buffer,
+ sys->segment_size,
+ sys->segment_date);
+ sys->segment_size = 0;
+ }
+ } else {
+ sub = DecodeSubtitleMessage(dec,
+ &b->p_buffer[4],
+ section_length - 1 - 4,
+ b->i_pts > VLC_TS_INVALID ? b->i_pts : b->i_dts);
+ }
+ *sub_last = sub;
+ if (*sub_last)
+ sub_last = &(*sub_last)->p_next;
+
+ b->i_buffer -= 3 + section_length;
+ b->p_buffer += 3 + section_length;
+ break;
+ }
+
+exit:
+ block_Release(b);
+ return sub_first;
+}
+
+static int Open(vlc_object_t *object)
+{
+ decoder_t *dec = (decoder_t *)object;
+
+ if (dec->fmt_in.i_codec != VLC_CODEC_SCTE_27)
+ return VLC_EGENERIC;
+
+ decoder_sys_t *sys = dec->p_sys = malloc(sizeof(*sys));
+ if (!sys)
+ return VLC_ENOMEM;
+ sys->segment_id = -1;
+ sys->segment_size = 0;
+ sys->segment_buffer = NULL;
+
+ dec->pf_decode_sub = Decode;
+ es_format_Init(&dec->fmt_out, SPU_ES, VLC_CODEC_SPU);
+ dec->fmt_out.video.i_chroma = VLC_CODEC_YUVP;
+
+ return VLC_SUCCESS;
+}
+
+static void Close(vlc_object_t *object)
+{
+ decoder_t *dec = (decoder_t *)object;
+ decoder_sys_t *sys = dec->p_sys;
+
+ free(sys->segment_buffer);
+ free(sys);
+}
+