[FFmpeg-devel] [PATCH] avcodec: add minimal teletext subtitle decoder

Aman Gupta ffmpeg at tmm1.net
Wed Apr 25 05:07:58 EEST 2018


From: Aman Gupta <aman at tmm1.net>

Based largely on VLC's modules/codec/telx.c.

Processes only teletext pages marked as subtitles, so depending
on the stream it might not produce any output.

Subtitles are rendered directly to ASS, with support for background
colors and a best-effort at screen positioning. The ASS packets
are emitted in real time (similar to ccaption_dec's real_time
option), with -1 durations. The decoder expects that the player
will remove all existing subtitles whenever a new packet arrives.

The teletext clear command is implemented using an empty subtitle,
which removes existing subtitles but does not render anything new.
---
 libavcodec/Makefile         |   1 +
 libavcodec/allcodecs.c      |   1 +
 libavcodec/teletextsubdec.c | 522 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 524 insertions(+)
 create mode 100644 libavcodec/teletextsubdec.c

diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 4b8ad121db..83f2eab513 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -593,6 +593,7 @@ OBJS-$(CONFIG_SVQ1_DECODER)            += svq1dec.o svq1.o svq13.o h263data.o
 OBJS-$(CONFIG_SVQ1_ENCODER)            += svq1enc.o svq1.o  h263data.o  \
                                           h263.o ituh263enc.o
 OBJS-$(CONFIG_SVQ3_DECODER)            += svq3.o svq13.o mpegutils.o h264data.o
+OBJS-$(CONFIG_TELETEXTSUB_DECODER)     += teletextsubdec.o ass.o
 OBJS-$(CONFIG_TEXT_DECODER)            += textdec.o ass.o
 OBJS-$(CONFIG_TEXT_ENCODER)            += srtenc.o ass_split.o
 OBJS-$(CONFIG_TAK_DECODER)             += takdec.o tak.o takdsp.o
diff --git a/libavcodec/allcodecs.c b/libavcodec/allcodecs.c
index 4d4ef530e4..f0f23eccef 100644
--- a/libavcodec/allcodecs.c
+++ b/libavcodec/allcodecs.c
@@ -634,6 +634,7 @@ extern AVCodec ff_subrip_encoder;
 extern AVCodec ff_subrip_decoder;
 extern AVCodec ff_subviewer_decoder;
 extern AVCodec ff_subviewer1_decoder;
+extern AVCodec ff_teletextsub_decoder;
 extern AVCodec ff_text_encoder;
 extern AVCodec ff_text_decoder;
 extern AVCodec ff_vplayer_decoder;
diff --git a/libavcodec/teletextsubdec.c b/libavcodec/teletextsubdec.c
new file mode 100644
index 0000000000..f5f0f935ef
--- /dev/null
+++ b/libavcodec/teletextsubdec.c
@@ -0,0 +1,522 @@
+/*
+ * Minimal Teletext subtitle decoding for ffmpeg
+ * Copyright (c) 2018 Aman Gupta
+ * Copyright (c) 2013 Marton Balint
+ * Copyright (c) 2005-2010, 2012 Wolfram Gloger
+ * Copyright (c) 2007 Vincent Penne, VLC authors and VideoLAN (modules/codec/telx.c)
+ * Copyright (c) 2001-2005 dvb.matt, ProjectX java dvb decoder
+ *
+ * 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 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdbool.h>
+#include "avcodec.h"
+#include "libavcodec/ass.h"
+#include "libavcodec/dvbtxt.h"
+#include "libavutil/opt.h"
+#include "libavutil/bprint.h"
+#include "libavutil/internal.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/log.h"
+#include "libavutil/reverse.h"
+
+#define NUM_ROWS 24
+#define ROW_SIZE 40
+#define NUM_MAGAZINES 9
+
+typedef struct TeletextContext
+{
+    AVClass *class;
+    int64_t pts;
+    AVBPrint buffer;
+    int readorder;
+
+    uint8_t rows[NUM_ROWS][ROW_SIZE];
+    bool active[NUM_MAGAZINES];
+    int charset;
+} TeletextContext;
+
+/*
+ * My doc only mentions 13 national characters, but experiments show there
+ * are more, in france for example I already found two more (0x9 and 0xb).
+ *
+ * Conversion is in this order :
+ *
+ * 0x23 0x24 0x40 0x5b 0x5c 0x5d 0x5e 0x5f 0x60 0x7b 0x7c 0x7d 0x7e
+ * (these are the standard ones)
+ * 0x08 0x09 0x0a 0x0b 0x0c 0x0d (apparently a control character) 0x0e 0x0f
+ */
+static const uint16_t national_charsets[][20] = {
+    { 0x00a3, 0x0024, 0x0040, 0x00ab, 0x00bd, 0x00bb, 0x005e, 0x0023,
+      0x002d, 0x00bc, 0x00a6, 0x00be, 0x00f7 }, /* english ,000 */
+
+    { 0x00e9, 0x00ef, 0x00e0, 0x00eb, 0x00ea, 0x00f9, 0x00ee, 0x0023,
+      0x00e8, 0x00e2, 0x00f4, 0x00fb, 0x00e7, 0, 0x00eb, 0, 0x00ef }, /* french  ,001 */
+
+    { 0x0023, 0x00a4, 0x00c9, 0x00c4, 0x00d6, 0x00c5, 0x00dc, 0x005f,
+      0x00e9, 0x00e4, 0x00f6, 0x00e5, 0x00fc }, /* swedish,finnish,hungarian ,010 */
+
+    { 0x0023, 0x016f, 0x010d, 0x0165, 0x017e, 0x00fd, 0x00ed, 0x0159,
+      0x00e9, 0x00e1, 0x011b, 0x00fa, 0x0161 }, /* czech,slovak  ,011 */
+
+    { 0x0023, 0x0024, 0x00a7, 0x00c4, 0x00d6, 0x00dc, 0x005e, 0x005f,
+      0x00b0, 0x00e4, 0x00f6, 0x00fc, 0x00df }, /* german ,100 */
+
+    { 0x00e7, 0x0024, 0x00a1, 0x00e1, 0x00e9, 0x00ed, 0x00f3, 0x00fa,
+      0x00bf, 0x00fc, 0x00f1, 0x00e8, 0x00e0 }, /* portuguese,spanish ,101 */
+
+    { 0x00a3, 0x0024, 0x00e9, 0x00b0, 0x00e7, 0x00bb, 0x005e, 0x0023,
+      0x00f9, 0x00e0, 0x00f2, 0x00e8, 0x00ec }, /* italian  ,110 */
+
+    { 0x0023, 0x00a4, 0x0162, 0x00c2, 0x015e, 0x0102, 0x00ce, 0x0131,
+      0x0163, 0x00e2, 0x015f, 0x0103, 0x00ee }, /* rumanian ,111 */
+
+    /* I have these tables too, but I don't know how they can be triggered */
+    { 0x0023, 0x0024, 0x0160, 0x0117, 0x0119, 0x017d, 0x010d, 0x016b,
+      0x0161, 0x0105, 0x0173, 0x017e, 0x012f }, /* lettish,lithuanian ,1000 */
+
+    { 0x0023, 0x0144, 0x0105, 0x005a, 0x015a, 0x0141, 0x0107, 0x00f3,
+      0x0119, 0x017c, 0x015b, 0x0142, 0x017a }, /* polish,  1001 */
+
+    { 0x0023, 0x00cb, 0x010c, 0x0106, 0x017d, 0x0110, 0x0160, 0x00eb,
+      0x010d, 0x0107, 0x017e, 0x0111, 0x0161 }, /* serbian,croatian,slovenian, 1010 */
+
+    { 0x0023, 0x00f5, 0x0160, 0x00c4, 0x00d6, 0x017e, 0x00dc, 0x00d5,
+      0x0161, 0x00e4, 0x00f6, 0x017e, 0x00fc }, /* estonian  ,1011 */
+
+    { 0x0054, 0x011f, 0x0130, 0x015e, 0x00d6, 0x00c7, 0x00dc, 0x011e,
+      0x0131, 0x015f, 0x00f6, 0x00e7, 0x00fc }, /* turkish  ,1100 */
+};
+
+static const char *color_mappings[8] = {
+    "{\\c&H000000&}", // black
+    "{\\c&H0000FF&}", // red
+    "{\\c&H00FF00&}", // green
+    "{\\c&H00FFFF&}", // yellow
+    "{\\c&HFF0000&}", // blue
+    "{\\c&HFF00FF&}", // magenta
+    "{\\c&HFFFF00&}", // cyan
+    "{\\c&HFFFFFF&}"  // white
+};
+
+static int hamming(int a)
+{
+    switch (a) {
+    case 0xA8: return 0;
+    case 0x0B: return 1;
+    case 0x26: return 2;
+    case 0x85: return 3;
+    case 0x92: return 4;
+    case 0x31: return 5;
+    case 0x1C: return 6;
+    case 0xBF: return 7;
+    case 0x40: return 8;
+    case 0xE3: return 9;
+    case 0xCE: return 10;
+    case 0x6D: return 11;
+    case 0x7A: return 12;
+    case 0xD9: return 13;
+    case 0xF4: return 14;
+    case 0x57: return 15;
+    default:   return -1; // decoding error , not yet corrected
+    }
+}
+
+// ucs-2 --> utf-8
+// this is not a general function, but it's enough for what we do here
+// the result buffer need to be at least 4 bytes long
+static void to_utf8(char *res, uint16_t ch)
+{
+    if (ch >= 0x80) {
+        if (ch >= 0x800) {
+            res[0] = (ch >> 12) | 0xE0;
+            res[1] = ((ch >> 6) & 0x3F) | 0x80;
+            res[2] = (ch & 0x3F) | 0x80;
+            res[3] = 0;
+        } else {
+            res[0] = (ch >> 6) | 0xC0;
+            res[1] = (ch & 0x3F) | 0x80;
+            res[2] = 0;
+        }
+    } else {
+        res[0] = ch;
+        res[1] = 0;
+    }
+}
+
+static void decode_string(AVCodecContext *avctx, AVBPrint *buf,
+                          const uint8_t *packet, int *leading, int *olen)
+{
+    TeletextContext *ctx = avctx->priv_data;
+    int i, len = 0;
+    int end_box = 0;
+    bool char_seen = false;
+    char utf8[7];
+    const uint16_t *charset = national_charsets[ctx->charset];
+    av_bprint_clear(buf);
+    *leading = 0;
+
+    for (i = 0; i < ROW_SIZE; i++) {
+        int in = ff_reverse[packet[i]] & 0x7f;
+        uint16_t out = 32;
+
+        switch (in) {
+        /* special national characters */
+        case 0x23: out = charset[0]; break;
+        case 0x24: out = charset[1]; break;
+        case 0x40: out = charset[2]; break;
+        case 0x5b: out = charset[3]; break;
+        case 0x5c: out = charset[4]; break;
+        case 0x5d: out = charset[5]; break;
+        case 0x5e: out = charset[6]; break;
+        case 0x5f: out = charset[7]; break;
+        case 0x60: out = charset[8]; break;
+        case 0x7b: out = charset[9]; break;
+        case 0x7c: out = charset[10]; break;
+        case 0x7d: out = charset[11]; break;
+        case 0x7e: out = charset[12]; break;
+
+        /* control codes */
+        case 0xd:
+            i++;
+            in = ff_reverse[packet[i]] & 0x7f;
+            if (in == 0xb)
+                in = 7;
+            if (in >= 0 && in < 8) {
+                av_bprintf(buf, "%s", color_mappings[in]);
+            }
+            continue;
+
+        case 0xa:
+            end_box++;
+            if (end_box == 1)
+                continue;
+            break;
+
+        case 0xb:
+            continue;
+
+        /* color codes */
+        case 0x0:
+        case 0x1:
+        case 0x2:
+        case 0x3:
+        case 0x4:
+        case 0x5:
+        case 0x6:
+        case 0x7:
+            av_bprintf(buf, "%s", color_mappings[in]);
+            break;
+
+        default:
+            /* non documented national range 0x08 - 0x0f */
+            if (in >= 0x08 && in <= 0x0f) {
+                out = charset[13 + in - 8];
+                break;
+            }
+
+            /* normal ascii */
+            if (in >= 32 && in < 0x7f)
+                out = in;
+        }
+
+        if (end_box == 2)
+            break;
+
+        /* handle undefined national characters */
+        if (out == 0)
+            out = 32;
+
+        if (out == 32 && !char_seen)
+            (*leading)++;
+        else if (out != 32)
+            char_seen = true;
+
+        /* convert to utf-8 */
+        to_utf8(utf8, out);
+        av_bprintf(buf, "%s", utf8);
+
+        if (char_seen || out != 32)
+            len += strlen(utf8);
+    }
+
+    /* remove trailing spaces */
+    for (i = buf->len-1; i >= 0 && buf->str[i] == 32; i--)
+        ;
+    buf->str[i+1] = 0;
+    buf->len = i;
+
+    *olen = len;
+}
+
+static int teletext_init_decoder(AVCodecContext *avctx)
+{
+    TeletextContext *ctx = avctx->priv_data;
+    av_bprint_init(&ctx->buffer, 0, AV_BPRINT_SIZE_UNLIMITED);
+    ctx->pts = AV_NOPTS_VALUE;
+    return ff_ass_subtitle_header(avctx,
+        "Monospace",
+        ASS_DEFAULT_FONT_SIZE,
+        ASS_DEFAULT_COLOR,
+        ASS_DEFAULT_BACK_COLOR,
+        ASS_DEFAULT_BOLD,
+        ASS_DEFAULT_ITALIC,
+        ASS_DEFAULT_UNDERLINE,
+        3,
+        ASS_DEFAULT_ALIGNMENT);
+}
+
+static int teletext_close_decoder(AVCodecContext *avctx)
+{
+    TeletextContext *ctx = avctx->priv_data;
+    ctx->pts = AV_NOPTS_VALUE;
+    av_bprint_finalize(&ctx->buffer, NULL);
+    return 0;
+}
+
+static void teletext_flush(AVCodecContext *avctx)
+{
+    TeletextContext *ctx = avctx->priv_data;
+    int i;
+    ctx->pts = AV_NOPTS_VALUE;
+    if (!(avctx->flags2 & AV_CODEC_FLAG2_RO_FLUSH_NOOP))
+        ctx->readorder = 0;
+    for (i = 0; i < NUM_ROWS; i++)
+        ctx->rows[i][0] = 0;
+    av_bprint_clear(&ctx->buffer);
+}
+
+static int capture_screen(AVCodecContext *avctx)
+{
+    TeletextContext *ctx = avctx->priv_data;
+    int i, j;
+    int tab = 0;
+    int num_rows = 0;
+    bool align_center = false, align_right = false;
+    bool maybe_center = false;
+    char prev_line[128] = {0};
+    AVBPrint line;
+    av_bprint_init(&line, 0, AV_BPRINT_SIZE_UNLIMITED);
+    av_bprint_clear(&ctx->buffer);
+
+    for (i = 0; i < NUM_ROWS; i++) {
+        int leading, len;
+        int spaces = 0;
+        const uint8_t *row = ctx->rows[i];
+        if (row[0] == 0)
+            continue;
+        num_rows++;
+
+        av_bprint_clear(&line);
+        decode_string(avctx, &line, row, &leading, &len);
+
+        // av_log(avctx, AV_LOG_DEBUG, "line[%d]: '%s' [leading=%d, len=%d]\n",
+        //        i, line.str, leading, len);
+
+        for (j = 0; j < line.len; j++) {
+            if (line.str[j] == ' ')
+                spaces++;
+            else
+                break;
+        }
+
+        if (!tab || spaces < tab)
+            tab = spaces;
+
+        if (leading > 0 && leading + len > 35)
+            align_right = true;
+        else if (leading > 0 && leading*2 + len <= 36)
+            align_center = true;
+        else if (leading == 0 && len == 34)
+            maybe_center = true;
+    }
+
+    if (num_rows == 1 && maybe_center)
+        align_center = true;
+
+    for (i = 0; i < NUM_ROWS; i++) {
+        int leading, len;
+        int x, y, alignment = 7;
+        const uint8_t *row = ctx->rows[i];
+        bool char_seen = false;
+        if (row[0] == 0)
+             continue;
+
+        av_bprint_clear(&line);
+        decode_string(avctx, &line, row, &leading, &len);
+
+        len = FFMIN(sizeof(prev_line), line.len+1);
+        if (strncmp(prev_line, line.str, len) == 0)
+            continue;
+        strncpy(prev_line, line.str, len);
+
+        /* skip leading space */
+        j = 0;
+        while (line.str[j] == ' ' && j < tab)
+            j++;
+
+        x = ASS_DEFAULT_PLAYRESX * (0.1 + (0.80 / 34) * j);
+        y = ASS_DEFAULT_PLAYRESY * (0.1 + (0.80 / 25) * i);
+
+        if (align_center) {
+            x = ASS_DEFAULT_PLAYRESX * 0.5;
+            alignment = 8;
+        } else if (align_right) {
+            x = ASS_DEFAULT_PLAYRESX * 0.9;
+            alignment = 9;
+        }
+
+        av_bprintf(&ctx->buffer, "{\\an%d}{\\pos(%d,%d)}", alignment, x, y);
+        for (; j <= line.len; j++) {
+            if (line.str[j] == 32 && !char_seen && !align_center && !align_right)
+                av_bprintf(&ctx->buffer, "\\h");
+            else {
+                av_bprintf(&ctx->buffer, "%c", line.str[j]);
+                char_seen = true;
+            }
+        }
+        av_bprintf(&ctx->buffer, "\\N");
+    }
+
+    av_bprint_finalize(&line, NULL);
+    if (!av_bprint_is_complete(&ctx->buffer))
+        return AVERROR(ENOMEM);
+    return 0;
+}
+
+static int teletext_decode_frame(AVCodecContext *avctx, void *data, int *got_sub, AVPacket *pkt)
+{
+    TeletextContext *ctx = avctx->priv_data;
+    AVSubtitle *sub = data;
+    int ret = 0;
+    int offset = 0;
+    bool updated = false, erased = false;
+
+    if (avctx->pkt_timebase.num && pkt->pts != AV_NOPTS_VALUE)
+        ctx->pts = av_rescale_q(pkt->pts, avctx->pkt_timebase, AV_TIME_BASE_Q);
+
+    if (pkt->size) {
+        const int full_pes_size = pkt->size + 45; /* PES header is 45 bytes */
+
+        // We allow unreasonably big packets, even if the standard only allows a max size of 1472
+        if (full_pes_size < 184 || full_pes_size > 65504 || full_pes_size % 184 != 0)
+            return AVERROR_INVALIDDATA;
+
+        if (!ff_data_identifier_is_teletext(*pkt->data))
+            return pkt->size;
+
+        for (offset = 1; offset + 46 <= pkt->size; offset += 46) {
+            int mpag, row, magazine;
+            uint8_t *packet = pkt->data + offset;
+            if (packet[0] == 0xFF)
+                continue;
+
+            mpag = (hamming(packet[4]) << 4) | hamming(packet[5]);
+            if (mpag < 0)
+                continue;
+
+            row = 0xFF & ff_reverse[mpag];
+            magazine = 7 & row;
+            if (magazine == 0)
+                magazine = 8;
+            row >>= 3;
+
+            if (row == 0) { // row 0: flags and header line
+                int flag = 0;
+                int i, page, f_charset;
+                bool f_erase, f_subtitle, f_inhibit, f_suppress, f_update, f_news;
+
+                for (i = 0; i < 6; i++)
+                    flag |= (0xF & (ff_reverse[hamming(packet[8+i])] >> 4)) << (4*i);
+
+                page = (0xF0 &  ff_reverse[hamming(packet[7])]      ) |
+                       (0x0F & (ff_reverse[hamming(packet[6])] >> 4));
+
+                f_erase    = 1 & (flag>>7);
+                f_news     = 1 & (flag>>15);
+                f_subtitle = 1 & (flag>>15);
+                f_suppress = 1 & (flag>>16);
+                f_update   = 1 & (flag>>17);
+                f_inhibit  = 1 & (flag>>19);
+                f_charset  = 7 & (flag>>21);
+
+                ctx->active[magazine] = f_subtitle;
+                if (!ctx->active[magazine])
+                    continue;
+
+                ctx->charset = f_charset;
+
+                // av_log(avctx, AV_LOG_DEBUG, "magazine=%d page=%d erase=%d subtitle=%d news=%d charset=%d inhibit=%d suppress=%d update=%d\n",
+                //       magazine, page, f_erase, f_subtitle, f_news, f_charset, f_inhibit, f_suppress, f_update);
+
+                if (f_erase) {
+                    for (i = 0; i < NUM_ROWS; i++)
+                        ctx->rows[i][0] = 0;
+                    erased = true;
+                    continue;
+                }
+
+            } else if (row < 24) { // row 1-23: normal lines
+                if (!ctx->active[magazine])
+                    continue;
+                if (memcmp(ctx->rows[row], packet+6, ROW_SIZE) != 0) {
+                    updated |= true;
+                    memcpy(ctx->rows[row], packet+6, ROW_SIZE);
+                }
+
+                // int j;
+                // av_log(avctx, AV_LOG_DEBUG, "row[%d]: ", row);
+                // for (j = 0; j < ROW_SIZE; j++) {
+                //     int in = ff_reverse[ctx->rows[row][j]] & 0x7f;
+                //     av_log(avctx, AV_LOG_DEBUG, "0x%x ", in);
+                // }
+                // av_log(avctx, AV_LOG_DEBUG, "\n");
+            }
+        }
+    }
+
+    if (updated || (erased && ctx->buffer.len)) {
+        ret = capture_screen(avctx);
+        if (ret < 0)
+            return ret;
+        ret = ff_ass_add_rect(sub, ctx->buffer.str, ctx->readorder++, 0, NULL, NULL);
+        if (ret < 0)
+            return ret;
+        sub->end_display_time = -1;
+    }
+
+    *got_sub = sub->num_rects > 0;
+    return offset;
+}
+
+static const AVClass teletext_class = {
+    .class_name = "teletextsub",
+    .item_name  = av_default_item_name,
+    .version    = LIBAVUTIL_VERSION_INT,
+};
+
+AVCodec ff_teletextsub_decoder = {
+    .name      = "teletext_subtitle",
+    .long_name = NULL_IF_CONFIG_SMALL("Minimal DVB teletext subtitle decoder"),
+    .type      = AVMEDIA_TYPE_SUBTITLE,
+    .id        = AV_CODEC_ID_DVB_TELETEXT,
+    .priv_data_size = sizeof(TeletextContext),
+    .init      = teletext_init_decoder,
+    .close     = teletext_close_decoder,
+    .decode    = teletext_decode_frame,
+    .capabilities = AV_CODEC_CAP_DELAY,
+    .flush     = teletext_flush,
+    .priv_class= &teletext_class,
+};
-- 
2.14.2



More information about the ffmpeg-devel mailing list