diff mbox

[FFmpeg-devel,2/2] avcodec/libzvbi-teletextdec: formatted ass output

Message ID 20180506210514.7390-2-cus@passwd.hu
State Accepted
Headers show

Commit Message

Marton Balint May 6, 2018, 9:05 p.m. UTC
Inspired by the VideoLAN text decoder and its port to FFmpeg made by Aman
Gupta.

Signed-off-by: Marton Balint <cus@passwd.hu>
---
 doc/decoders.texi                |  18 ++-
 libavcodec/libzvbi-teletextdec.c | 265 +++++++++++++++++++++++++++++++++++++--
 2 files changed, 270 insertions(+), 13 deletions(-)

Comments

Aman Karmani May 7, 2018, 7:50 p.m. UTC | #1
On Sun, May 6, 2018 at 2:05 PM, Marton Balint <cus@passwd.hu> wrote:

> Inspired by the VideoLAN text decoder and its port to FFmpeg made by Aman
> Gupta.
>

Thanks for incorporating my changes.

I ran some tests, and colors work as expected. Positioning also works well,
and is also pretty close to my version.


I found that some live streams are not chopping empty lines correctly. I
see extra newlines at the end:

  Dialogue:
0,0:13:21.66,9:59:59.99,Default,,0,0,0,,{\an1}Simson,\hPeter\hHartcher,\hRobyn\hParker
\Nand\hBenjamin \N \N \N

Here's a sample which shows extra newlines:
https://s3.amazonaws.com/tmm1/teletext/capture_live1.mpg


It also looks like the "erase page" command is not being processed, which
causes stale captions to stay on the screen in some cases.
This is especially confusing when part of an old caption remains on one
line, but then newer captions appear on another line.

Here's a sample that shows lines not being cleared correctly:
https://s3.amazonaws.com/tmm1/teletext/simple.mpg


I also have several other samples which use various features, available at
the same URL:

capture_formatting1.mpg
capture_formatting2.mpg
capture_formatting3.mpg
capture_live1.mpg
capture_live2.mpg
capture_notCaptionedMessage.mpg
capture_threeLines1.mpg
capture_threeLines2.mpg
capture_threeLines3.mpg
capture_threeLines4.mpg
capture_yOffset1.mpg
three.ts
four.ts

Thanks,
Aman



>
> Signed-off-by: Marton Balint <cus@passwd.hu>
> ---
>  doc/decoders.texi                |  18 ++-
>  libavcodec/libzvbi-teletextdec.c | 265 ++++++++++++++++++++++++++++++
> +++++++--
>  2 files changed, 270 insertions(+), 13 deletions(-)
>
> diff --git a/doc/decoders.texi b/doc/decoders.texi
> index 8f07bc1afb..cc9b2ef123 100644
> --- a/doc/decoders.texi
> +++ b/doc/decoders.texi
> @@ -255,12 +255,18 @@ Default value is *.
>  @item txt_chop_top
>  Discards the top teletext line. Default value is 1.
>  @item txt_format
> -Specifies the format of the decoded subtitles. The teletext decoder is
> capable
> -of decoding the teletext pages to bitmaps or to simple text, you should
> use
> -"bitmap" for teletext pages, because certain graphics and colors cannot be
> -expressed in simple text. You might use "text" for teletext based
> subtitles if
> -your application can handle simple text based subtitles. Default value is
> -bitmap.
> +Specifies the format of the decoded subtitles.
> +@table @option
> +@item bitmap
> +The default format, you should use this for teletext pages, because
> certain
> +graphics and colors cannot be expressed in simple text or even ASS.
> +@item text
> +Simple text based output without formatting.
> +@item ass
> +Formatted ASS output, subtitle pages and teletext pages are returned in
> +different styles, subtitle pages are stripped down to text, but an effort
> is
> +made to keep the text alignment and the formatting.
> +@end table
>  @item txt_left
>  X offset of generated bitmaps, default is 0.
>  @item txt_top
> diff --git a/libavcodec/libzvbi-teletextdec.c b/libavcodec/libzvbi-
> teletextdec.c
> index 56a7182882..7541de0d53 100644
> --- a/libavcodec/libzvbi-teletextdec.c
> +++ b/libavcodec/libzvbi-teletextdec.c
> @@ -26,6 +26,7 @@
>  #include "libavutil/internal.h"
>  #include "libavutil/intreadwrite.h"
>  #include "libavutil/log.h"
> +#include "libavutil/common.h"
>
>  #include <libzvbi.h>
>
> @@ -56,7 +57,7 @@ typedef struct TeletextContext
>      char           *pgno;
>      int             x_offset;
>      int             y_offset;
> -    int             format_id; /* 0 = bitmap, 1 = text/ass */
> +    int             format_id; /* 0 = bitmap, 1 = text/ass, 2 = ass */
>      int             chop_top;
>      int             sub_duration; /* in msec */
>      int             transparent_bg;
> @@ -74,8 +75,55 @@ typedef struct TeletextContext
>
>      int             readorder;
>      uint8_t         subtitle_map[2048];
> +    int             last_ass_alignment;
>  } TeletextContext;
>
> +static int my_ass_subtitle_header(AVCodecContext *avctx)
> +{
> +    int ret = ff_ass_subtitle_header_default(avctx);
> +    char *new_header;
> +    uint8_t *event_pos;
> +
> +    if (ret < 0)
> +        return ret;
> +
> +    event_pos = strstr(avctx->subtitle_header, "\r\n[Events]\r\n");
> +    if (!event_pos)
> +        return AVERROR_BUG;
> +
> +    new_header = av_asprintf("%.*s%s%s",
> +        (int)(event_pos - avctx->subtitle_header), avctx->subtitle_header,
> +        "Style: "
> +        "Teletext,"            /* Name */
> +        "Monospace,11,"        /* Font{name,size} */
> +        "&Hffffff,&Hffffff,&H0,&H0," /* {Primary,Secondary,Outline,Back}Colour
> */
> +        "0,0,0,0,"             /* Bold, Italic, Underline, StrikeOut */
> +        "160,100,"             /* Scale{X,Y} */
> +        "0,0,"                 /* Spacing, Angle */
> +        "3,0.1,0,"             /* BorderStyle, Outline, Shadow */
> +        "5,1,1,1,"             /* Alignment, Margin[LRV] */
> +        "0\r\n"                /* Encoding */
> +        "Style: "
> +        "Subtitle,"            /* Name */
> +        "Monospace,16,"        /* Font{name,size} */
> +        "&Hffffff,&Hffffff,&H0,&H0," /* {Primary,Secondary,Outline,Back}Colour
> */
> +        "0,0,0,0,"             /* Bold, Italic, Underline, StrikeOut */
> +        "100,100,"             /* Scale{X,Y} */
> +        "0,0,"                 /* Spacing, Angle */
> +        "1,1,1,"               /* BorderStyle, Outline, Shadow */
> +        "8,48,48,20,"          /* Alignment, Margin[LRV] */
> +        "0\r\n"                /* Encoding */
> +        , event_pos);
> +
> +    if (!new_header)
> +        return AVERROR(ENOMEM);
> +
> +    av_free(avctx->subtitle_header);
> +    avctx->subtitle_header = new_header;
> +    avctx->subtitle_header_size = strlen(new_header);
> +    return 0;
> +}
> +
>  static int chop_spaces_utf8(const unsigned char* t, int len)
>  {
>      t += len;
> @@ -179,6 +227,186 @@ static int gen_sub_text(TeletextContext *ctx,
> AVSubtitleRect *sub_rect, vbi_page
>      return 0;
>  }
>
> +static void bprint_color(const char *type, AVBPrint *buf, vbi_page *page,
> unsigned ci)
> +{
> +    int r = VBI_R(page->color_map[ci]);
> +    int g = VBI_G(page->color_map[ci]);
> +    int b = VBI_B(page->color_map[ci]);
> +    av_bprintf(buf, "{\\%s&H%02X%02X%02X&}", type, b, g, r);
> +}
> +
> +#define IS_TXT_SPACE(ch) ((ch).unicode < 0x0020 || (ch).unicode >= 0xe000
> || (ch).unicode == 0x00a0 ||\
> +                          (ch).size > VBI_DOUBLE_SIZE || (ch).opacity ==
> VBI_TRANSPARENT_SPACE)
> +
> +static void get_trim_info(vbi_page *page, vbi_char *row, int *leading,
> int *trailing, int *olen)
> +{
> +    int i, len = 0;
> +    int char_seen = 0;
> +
> +    *leading = 0;
> +
> +    for (i = 0; i < page->columns; i++) {
> +        uint16_t out = IS_TXT_SPACE(row[i]) ? 32 : row[i].unicode;
> +
> +        if (out == 32 && !char_seen)
> +            (*leading)++;
> +        else if (out != 32)
> +            char_seen = 1, len = i - (*leading) + 1;
> +    }
> +
> +    *olen = len;
> +    *trailing = len > 0 ? page->columns - *leading - len : page->columns;
> +}
> +
> +static void decode_string(vbi_page *page, vbi_char *row, AVBPrint *buf,
> +                          int start, int end, vbi_color *cur_color,
> vbi_color *cur_back_color)
> +{
> +    int i;
> +
> +    for (i = start; i < end; i++) {
> +        uint16_t out = IS_TXT_SPACE(row[i]) ? 32 : row[i].unicode;
> +
> +        if (*cur_color != row[i].foreground) {
> +            bprint_color("c", buf, page, row[i].foreground);
> +            *cur_color = row[i].foreground;
> +        }
> +        if (*cur_back_color != row[i].background) {
> +            bprint_color("3c", buf, page, row[i].background);
> +            *cur_back_color = row[i].background;
> +        }
> +
> +        if (out == 32) {
> +            av_bprintf(buf, "\\h");
> +        } else if (out == '\\' || out == '{' || out == '}') {
> +            av_bprintf(buf, "\\%c", (char)out);
> +        } else {
> +            char tmp;
> +            /* convert to utf-8 */
> +            PUT_UTF8(out, tmp, av_bprint_chars(buf, tmp, 1););
> +        }
> +    }
> +}
> +
> +/* Draw a page as ass formatted text */
> +static int gen_sub_ass(TeletextContext *ctx, AVSubtitleRect *sub_rect,
> vbi_page *page, int chop_top)
> +{
> +    int i;
> +    int leading, trailing, len;
> +    int last_trailing = -1, last_leading = -1;
> +    int min_trailing = page->columns, min_leading = page->columns;
> +    int alignment = 2;
> +    int vertical_align = -1;
> +    int can_align_left = 1, can_align_right = 1, can_align_center = 1;
> +    int is_subtitle_page = ctx->subtitle_map[page->pgno & 0x7ff];
> +    int empty_lines = 0;
> +    vbi_color cur_color = VBI_WHITE;
> +    vbi_color cur_back_color = VBI_BLACK;
> +    AVBPrint buf;
> +
> +    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
> +
> +    for (i = chop_top; i < page->rows; i++) {
> +        vbi_char *row = page->text + i * page->columns;
> +
> +        get_trim_info(page, row, &leading, &trailing, &len);
> +
> +        if (len) {
> +            if (last_leading != -1 && last_leading != leading || leading
> > 5)
> +                can_align_left = 0;
> +            if (last_trailing != -1 && last_trailing != trailing ||
> trailing > 2)
> +                can_align_right = 0;
> +            if (last_trailing != -1 && (FFABS((trailing - leading) -
> (last_trailing - last_leading)) > 1) || trailing - leading > 4)
> +                can_align_center = 0;
> +            last_leading = leading;
> +            last_trailing = trailing;
> +            min_leading = FFMIN(leading, min_leading);
> +            min_trailing = FFMIN(trailing, min_trailing);
> +        }
> +    }
> +
> +    if ((can_align_right == can_align_left) && !can_align_center) {
> +        alignment = ctx->last_ass_alignment;
> +    } else if (!can_align_right && can_align_left && !can_align_center) {
> +        ctx->last_ass_alignment = alignment = 1;
> +    } else if (!can_align_right && !can_align_left && can_align_center) {
> +        ctx->last_ass_alignment = alignment = 2;
> +    } else if (can_align_right && !can_align_left && !can_align_center) {
> +        ctx->last_ass_alignment = alignment = 3;
> +    } else {
> +        if (ctx->last_ass_alignment == 1 && can_align_left ||
> +            ctx->last_ass_alignment == 2 && can_align_center ||
> +            ctx->last_ass_alignment == 3 && can_align_right)
> +            alignment = ctx->last_ass_alignment;
> +    }
> +
> +    for (i = chop_top; i < page->rows; i++) {
> +        int j;
> +        vbi_char *row = page->text + i * page->columns;
> +        int is_transparent_line;
> +
> +        for (j = 0; j < page->columns; j++)
> +            if (row[j].opacity != VBI_TRANSPARENT_SPACE)
> +                break;
> +        is_transparent_line = (j == page->columns);
> +
> +        len = is_transparent_line ? 0 : page->columns;
> +        leading = trailing = is_transparent_line ? page->columns : 0;
> +
> +        if (is_subtitle_page) {
> +            if (!is_transparent_line)
> +                get_trim_info(page, row, &leading, &trailing, &len);
> +
> +            if (vertical_align == -1 && len) {
> +                vertical_align = (2 - (av_clip(i + 1, 0, 23) / 8));
> +                av_bprintf(&buf, "{\\an%d}", alignment + vertical_align *
> 3);
> +                if (vertical_align != 2)
> +                    empty_lines = 0;
> +            }
> +
> +            if (len && empty_lines > 1)
> +                for (empty_lines /= 2; empty_lines > 0; empty_lines--)
> +                    av_bprintf(&buf, " \\N");
> +
> +            if (alignment == 1)
> +                leading = min_leading;
> +            if (alignment == 3)
> +                trailing = min_trailing;
> +        }
> +
> +        if (len || !is_subtitle_page) {
> +            decode_string(page, row, &buf, leading, page->columns -
> trailing, &cur_color, &cur_back_color);
> +            av_bprintf(&buf, " \\N");
> +            empty_lines = 0;
> +        } else {
> +            empty_lines++;
> +        }
> +    }
> +
> +    if (vertical_align == 0)
> +        for (empty_lines = (empty_lines - 1) / 2; empty_lines > 0;
> empty_lines--)
> +            av_bprintf(&buf, " \\N");
> +
> +    if (!av_bprint_is_complete(&buf)) {
> +        av_bprint_finalize(&buf, NULL);
> +        return AVERROR(ENOMEM);
> +    }
> +
> +    if (buf.len) {
> +        sub_rect->type = SUBTITLE_ASS;
> +        sub_rect->ass = ff_ass_get_dialog(ctx->readorder++, 0,
> is_subtitle_page ? "Subtitle" : "Teletext", NULL, buf.str);
> +
> +        if (!sub_rect->ass) {
> +            av_bprint_finalize(&buf, NULL);
> +            return AVERROR(ENOMEM);
> +        }
> +        av_log(ctx, AV_LOG_DEBUG, "subtext:%s:txetbus\n", sub_rect->ass);
> +    } else {
> +        sub_rect->type = SUBTITLE_NONE;
> +    }
> +    av_bprint_finalize(&buf, NULL);
> +    return 0;
> +}
> +
>  static void fix_transparency(TeletextContext *ctx, AVSubtitleRect
> *sub_rect, vbi_page *page,
>                               int chop_top, int resx, int resy)
>  {
> @@ -316,9 +544,20 @@ static void handler(vbi_event *ev, void *user_data)
>              cur_page->pgno = ev->ev.ttx_page.pgno;
>              cur_page->subno = ev->ev.ttx_page.subno;
>              if (cur_page->sub_rect) {
> -                res = (ctx->format_id == 0) ?
> -                    gen_sub_bitmap(ctx, cur_page->sub_rect, &page,
> chop_top) :
> -                    gen_sub_text  (ctx, cur_page->sub_rect, &page,
> chop_top);
> +                switch (ctx->format_id) {
> +                    case 0:
> +                        res = gen_sub_bitmap(ctx, cur_page->sub_rect,
> &page, chop_top);
> +                        break;
> +                    case 1:
> +                        res = gen_sub_text(ctx, cur_page->sub_rect,
> &page, chop_top);
> +                        break;
> +                    case 2:
> +                        res = gen_sub_ass(ctx, cur_page->sub_rect, &page,
> chop_top);
> +                        break;
> +                    default:
> +                        res = AVERROR_BUG;
> +                        break;
> +                }
>                  if (res < 0) {
>                      av_freep(&cur_page->sub_rect);
>                      ctx->handler_ret = res;
> @@ -435,7 +674,7 @@ static int teletext_decode_frame(AVCodecContext
> *avctx, void *data, int *data_si
>      // is there a subtitle to pass?
>      if (ctx->nb_pages) {
>          int i;
> -        sub->format = ctx->format_id;
> +        sub->format = !!ctx->format_id;
>          sub->start_display_time = 0;
>          sub->end_display_time = ctx->sub_duration;
>          sub->num_rects = 0;
> @@ -494,12 +733,22 @@ static int teletext_init_decoder(AVCodecContext
> *avctx)
>
>      ctx->vbi = NULL;
>      ctx->pts = AV_NOPTS_VALUE;
> +    ctx->last_ass_alignment = 2;
>
>      if (ctx->opacity == -1)
>          ctx->opacity = ctx->transparent_bg ? 0 : 255;
>
>      av_log(avctx, AV_LOG_VERBOSE, "page filter: %s\n", ctx->pgno);
> -    return (ctx->format_id == 1) ? ff_ass_subtitle_header_default(avctx)
> : 0;
> +
> +    switch (ctx->format_id) {
> +        case 0:
> +            return 0;
> +        case 1:
> +            return ff_ass_subtitle_header_default(avctx);
> +        case 2:
> +            return my_ass_subtitle_header(avctx);
> +    }
> +    return AVERROR_BUG;
>  }
>
>  static int teletext_close_decoder(AVCodecContext *avctx)
> @@ -514,6 +763,7 @@ static int teletext_close_decoder(AVCodecContext
> *avctx)
>      vbi_decoder_delete(ctx->vbi);
>      ctx->vbi = NULL;
>      ctx->pts = AV_NOPTS_VALUE;
> +    ctx->last_ass_alignment = 2;
>      memset(ctx->subtitle_map, 0, sizeof(ctx->subtitle_map));
>      if (!(avctx->flags2 & AV_CODEC_FLAG2_RO_FLUSH_NOOP))
>          ctx->readorder = 0;
> @@ -530,9 +780,10 @@ static void teletext_flush(AVCodecContext *avctx)
>  static const AVOption options[] = {
>      {"txt_page",        "list of teletext page numbers to decode, * is
> all", OFFSET(pgno),           AV_OPT_TYPE_STRING, {.str = "*"},      0, 0,
>       SD},
>      {"txt_chop_top",    "discards the top teletext line",
>     OFFSET(chop_top),       AV_OPT_TYPE_INT,    {.i64 = 1},        0, 1,
>     SD},
> -    {"txt_format",      "format of the subtitles (bitmap or text)",
>     OFFSET(format_id),      AV_OPT_TYPE_INT,    {.i64 = 0},        0, 1,
>     SD,  "txt_format"},
> +    {"txt_format",      "format of the subtitles (bitmap or text or
> ass)",   OFFSET(format_id),      AV_OPT_TYPE_INT,    {.i64 = 0},        0,
> 2,        SD,  "txt_format"},
>      {"bitmap",          NULL,
>     0,                      AV_OPT_TYPE_CONST,  {.i64 = 0},        0, 0,
>     SD,  "txt_format"},
>      {"text",            NULL,
>     0,                      AV_OPT_TYPE_CONST,  {.i64 = 1},        0, 0,
>     SD,  "txt_format"},
> +    {"ass",             NULL,
>     0,                      AV_OPT_TYPE_CONST,  {.i64 = 2},        0, 0,
>     SD,  "txt_format"},
>      {"txt_left",        "x offset of generated bitmaps",
>    OFFSET(x_offset),       AV_OPT_TYPE_INT,    {.i64 = 0},        0,
> 65535,    SD},
>      {"txt_top",         "y offset of generated bitmaps",
>    OFFSET(y_offset),       AV_OPT_TYPE_INT,    {.i64 = 0},        0,
> 65535,    SD},
>      {"txt_chop_spaces", "chops leading and trailing spaces from text",
>    OFFSET(chop_spaces),    AV_OPT_TYPE_INT,    {.i64 = 1},        0, 1,
>     SD},
> --
> 2.13.6
>
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> http://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
Aman Karmani May 7, 2018, 7:59 p.m. UTC | #2
On Mon, May 7, 2018 at 12:50 PM, Aman Gupta <ffmpeg@tmm1.net> wrote:

>
>
> On Sun, May 6, 2018 at 2:05 PM, Marton Balint <cus@passwd.hu> wrote:
>
>> Inspired by the VideoLAN text decoder and its port to FFmpeg made by Aman
>> Gupta.
>>
>
> Thanks for incorporating my changes.
>
> I ran some tests, and colors work as expected. Positioning also works
> well, and is also pretty close to my version.
>
>
> I found that some live streams are not chopping empty lines correctly. I
> see extra newlines at the end:
>
>   Dialogue: 0,0:13:21.66,9:59:59.99,Default,,0,0,0,,{\an1}Simson,\
> hPeter\hHartcher,\hRobyn\hParker \Nand\hBenjamin \N \N \N
>
> Here's a sample which shows extra newlines: https://s3.
> amazonaws.com/tmm1/teletext/capture_live1.mpg
>
>
> It also looks like the "erase page" command is not being processed, which
> causes stale captions to stay on the screen in some cases.
> This is especially confusing when part of an old caption remains on one
> line, but then newer captions appear on another line.
>
> Here's a sample that shows lines not being cleared correctly: https://s3.
> amazonaws.com/tmm1/teletext/simple.mpg
>

With this sample, for instance, my decoder produces:

Dialogue:
0,0:00:01.90,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}and
this, as you rightly say,\N
Dialogue:
0,0:00:01.94,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}and
this, as you rightly say,\N{\an8}{\pos(192,231)}        {\c&HFFFFFF&}is
American.\N
Dialogue: 0,0:00:04.86,9:59:59.99,Default,,0,0,0,,
Dialogue:
0,0:00:04.94,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,231)}{\c&HFFFFFF&}It's
rather nicely made.\N
Dialogue: 0,0:00:06.62,9:59:59.99,Default,,0,0,0,,
Dialogue:
0,0:00:06.70,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}It's
got this fabulous\N
Dialogue:
0,0:00:06.74,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}It's
got this fabulous\N{\an8}{\pos(192,231)}   {\c&HFFFFFF&}cast finial here\N
Dialogue: 0,0:00:10.34,9:59:59.99,Default,,0,0,0,,
Dialogue:
0,0:00:10.42,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}of
a very muscular figure\N
Dialogue:
0,0:00:10.46,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}of
a very muscular figure\N{\an8}{\pos(192,231)} {\c&HFFFFFF&}pulling this
medallion.\N
Dialogue: 0,0:00:15.50,9:59:59.99,Default,,0,0,0,,
Dialogue:
0,0:00:15.58,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}So
it's got strength to it. It's a\N
Dialogue: 0,0:00:15.62,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}
{\c&HFFFFFF&}So it's got strength to it. It's
a\N{\an8}{\pos(192,231)}{\c&HFFFFFF&}really characterful piece of silver.\N
Dialogue: 0,0:00:19.14,9:59:59.99,Default,,0,0,0,,

And the decoder in this patch produces:

Dialogue:
0,0:00:02.02,9:59:59.99,Default,,0,0,0,,{\an2}and\hthis,\has\hyou\hrightly\hsay,
\Nis\hAmerican. \N
Dialogue:
0,0:00:05.02,9:59:59.99,Default,,0,0,0,,{\an2}and\hthis,\has\hyou\hrightly\hsay,
\NIt's\hrather\hnicely\hmade. \N
Dialogue:
0,0:00:06.82,9:59:59.99,Default,,0,0,0,,{\an2}It's\hgot\hthis\hfabulous
\Ncast\hfinial\hhere \N
Dialogue:
0,0:00:10.54,9:59:59.99,Default,,0,0,0,,{\an2}of\ha\hvery\hmuscular\hfigure
\Npulling\hthis\hmedallion. \N
Dialogue:
0,0:00:15.70,9:59:59.99,Default,,0,0,0,,{\an2}So\hit's\hgot\hstrength\hto\hit.\hIt's\ha
\Nreally\hcharacterful\hpiece\hof\hsilver. \N
Dialogue:
0,0:00:19.30,9:59:59.99,Default,,0,0,0,,{\an2}So\hit's\hgot\hstrength\hto\hit.\hIt's\ha
\NBut\hmost\himportant\hof\hall, \N

At the ~5s mark, the text on the screen should say "It's rather nicely
made", but this decoder still displays the line "and this as you rightly
say" from the previous sentence.

Aman


>
>
> I also have several other samples which use various features, available at
> the same URL:
>
> capture_formatting1.mpg
> capture_formatting2.mpg
> capture_formatting3.mpg
> capture_live1.mpg
> capture_live2.mpg
> capture_notCaptionedMessage.mpg
> capture_threeLines1.mpg
> capture_threeLines2.mpg
> capture_threeLines3.mpg
> capture_threeLines4.mpg
> capture_yOffset1.mpg
> three.ts
> four.ts
>
> Thanks,
> Aman
>
>
>
>>
>> Signed-off-by: Marton Balint <cus@passwd.hu>
>> ---
>>  doc/decoders.texi                |  18 ++-
>>  libavcodec/libzvbi-teletextdec.c | 265 ++++++++++++++++++++++++++++++
>> +++++++--
>>  2 files changed, 270 insertions(+), 13 deletions(-)
>>
>> diff --git a/doc/decoders.texi b/doc/decoders.texi
>> index 8f07bc1afb..cc9b2ef123 100644
>> --- a/doc/decoders.texi
>> +++ b/doc/decoders.texi
>> @@ -255,12 +255,18 @@ Default value is *.
>>  @item txt_chop_top
>>  Discards the top teletext line. Default value is 1.
>>  @item txt_format
>> -Specifies the format of the decoded subtitles. The teletext decoder is
>> capable
>> -of decoding the teletext pages to bitmaps or to simple text, you should
>> use
>> -"bitmap" for teletext pages, because certain graphics and colors cannot
>> be
>> -expressed in simple text. You might use "text" for teletext based
>> subtitles if
>> -your application can handle simple text based subtitles. Default value is
>> -bitmap.
>> +Specifies the format of the decoded subtitles.
>> +@table @option
>> +@item bitmap
>> +The default format, you should use this for teletext pages, because
>> certain
>> +graphics and colors cannot be expressed in simple text or even ASS.
>> +@item text
>> +Simple text based output without formatting.
>> +@item ass
>> +Formatted ASS output, subtitle pages and teletext pages are returned in
>> +different styles, subtitle pages are stripped down to text, but an
>> effort is
>> +made to keep the text alignment and the formatting.
>> +@end table
>>  @item txt_left
>>  X offset of generated bitmaps, default is 0.
>>  @item txt_top
>> diff --git a/libavcodec/libzvbi-teletextdec.c
>> b/libavcodec/libzvbi-teletextdec.c
>> index 56a7182882..7541de0d53 100644
>> --- a/libavcodec/libzvbi-teletextdec.c
>> +++ b/libavcodec/libzvbi-teletextdec.c
>> @@ -26,6 +26,7 @@
>>  #include "libavutil/internal.h"
>>  #include "libavutil/intreadwrite.h"
>>  #include "libavutil/log.h"
>> +#include "libavutil/common.h"
>>
>>  #include <libzvbi.h>
>>
>> @@ -56,7 +57,7 @@ typedef struct TeletextContext
>>      char           *pgno;
>>      int             x_offset;
>>      int             y_offset;
>> -    int             format_id; /* 0 = bitmap, 1 = text/ass */
>> +    int             format_id; /* 0 = bitmap, 1 = text/ass, 2 = ass */
>>      int             chop_top;
>>      int             sub_duration; /* in msec */
>>      int             transparent_bg;
>> @@ -74,8 +75,55 @@ typedef struct TeletextContext
>>
>>      int             readorder;
>>      uint8_t         subtitle_map[2048];
>> +    int             last_ass_alignment;
>>  } TeletextContext;
>>
>> +static int my_ass_subtitle_header(AVCodecContext *avctx)
>> +{
>> +    int ret = ff_ass_subtitle_header_default(avctx);
>> +    char *new_header;
>> +    uint8_t *event_pos;
>> +
>> +    if (ret < 0)
>> +        return ret;
>> +
>> +    event_pos = strstr(avctx->subtitle_header, "\r\n[Events]\r\n");
>> +    if (!event_pos)
>> +        return AVERROR_BUG;
>> +
>> +    new_header = av_asprintf("%.*s%s%s",
>> +        (int)(event_pos - avctx->subtitle_header),
>> avctx->subtitle_header,
>> +        "Style: "
>> +        "Teletext,"            /* Name */
>> +        "Monospace,11,"        /* Font{name,size} */
>> +        "&Hffffff,&Hffffff,&H0,&H0," /* {Primary,Secondary,Outline,Back}Colour
>> */
>> +        "0,0,0,0,"             /* Bold, Italic, Underline, StrikeOut */
>> +        "160,100,"             /* Scale{X,Y} */
>> +        "0,0,"                 /* Spacing, Angle */
>> +        "3,0.1,0,"             /* BorderStyle, Outline, Shadow */
>> +        "5,1,1,1,"             /* Alignment, Margin[LRV] */
>> +        "0\r\n"                /* Encoding */
>> +        "Style: "
>> +        "Subtitle,"            /* Name */
>> +        "Monospace,16,"        /* Font{name,size} */
>> +        "&Hffffff,&Hffffff,&H0,&H0," /* {Primary,Secondary,Outline,Back}Colour
>> */
>> +        "0,0,0,0,"             /* Bold, Italic, Underline, StrikeOut */
>> +        "100,100,"             /* Scale{X,Y} */
>> +        "0,0,"                 /* Spacing, Angle */
>> +        "1,1,1,"               /* BorderStyle, Outline, Shadow */
>> +        "8,48,48,20,"          /* Alignment, Margin[LRV] */
>> +        "0\r\n"                /* Encoding */
>> +        , event_pos);
>> +
>> +    if (!new_header)
>> +        return AVERROR(ENOMEM);
>> +
>> +    av_free(avctx->subtitle_header);
>> +    avctx->subtitle_header = new_header;
>> +    avctx->subtitle_header_size = strlen(new_header);
>> +    return 0;
>> +}
>> +
>>  static int chop_spaces_utf8(const unsigned char* t, int len)
>>  {
>>      t += len;
>> @@ -179,6 +227,186 @@ static int gen_sub_text(TeletextContext *ctx,
>> AVSubtitleRect *sub_rect, vbi_page
>>      return 0;
>>  }
>>
>> +static void bprint_color(const char *type, AVBPrint *buf, vbi_page
>> *page, unsigned ci)
>> +{
>> +    int r = VBI_R(page->color_map[ci]);
>> +    int g = VBI_G(page->color_map[ci]);
>> +    int b = VBI_B(page->color_map[ci]);
>> +    av_bprintf(buf, "{\\%s&H%02X%02X%02X&}", type, b, g, r);
>> +}
>> +
>> +#define IS_TXT_SPACE(ch) ((ch).unicode < 0x0020 || (ch).unicode >=
>> 0xe000 || (ch).unicode == 0x00a0 ||\
>> +                          (ch).size > VBI_DOUBLE_SIZE || (ch).opacity ==
>> VBI_TRANSPARENT_SPACE)
>> +
>> +static void get_trim_info(vbi_page *page, vbi_char *row, int *leading,
>> int *trailing, int *olen)
>> +{
>> +    int i, len = 0;
>> +    int char_seen = 0;
>> +
>> +    *leading = 0;
>> +
>> +    for (i = 0; i < page->columns; i++) {
>> +        uint16_t out = IS_TXT_SPACE(row[i]) ? 32 : row[i].unicode;
>> +
>> +        if (out == 32 && !char_seen)
>> +            (*leading)++;
>> +        else if (out != 32)
>> +            char_seen = 1, len = i - (*leading) + 1;
>> +    }
>> +
>> +    *olen = len;
>> +    *trailing = len > 0 ? page->columns - *leading - len : page->columns;
>> +}
>> +
>> +static void decode_string(vbi_page *page, vbi_char *row, AVBPrint *buf,
>> +                          int start, int end, vbi_color *cur_color,
>> vbi_color *cur_back_color)
>> +{
>> +    int i;
>> +
>> +    for (i = start; i < end; i++) {
>> +        uint16_t out = IS_TXT_SPACE(row[i]) ? 32 : row[i].unicode;
>> +
>> +        if (*cur_color != row[i].foreground) {
>> +            bprint_color("c", buf, page, row[i].foreground);
>> +            *cur_color = row[i].foreground;
>> +        }
>> +        if (*cur_back_color != row[i].background) {
>> +            bprint_color("3c", buf, page, row[i].background);
>> +            *cur_back_color = row[i].background;
>> +        }
>> +
>> +        if (out == 32) {
>> +            av_bprintf(buf, "\\h");
>> +        } else if (out == '\\' || out == '{' || out == '}') {
>> +            av_bprintf(buf, "\\%c", (char)out);
>> +        } else {
>> +            char tmp;
>> +            /* convert to utf-8 */
>> +            PUT_UTF8(out, tmp, av_bprint_chars(buf, tmp, 1););
>> +        }
>> +    }
>> +}
>> +
>> +/* Draw a page as ass formatted text */
>> +static int gen_sub_ass(TeletextContext *ctx, AVSubtitleRect *sub_rect,
>> vbi_page *page, int chop_top)
>> +{
>> +    int i;
>> +    int leading, trailing, len;
>> +    int last_trailing = -1, last_leading = -1;
>> +    int min_trailing = page->columns, min_leading = page->columns;
>> +    int alignment = 2;
>> +    int vertical_align = -1;
>> +    int can_align_left = 1, can_align_right = 1, can_align_center = 1;
>> +    int is_subtitle_page = ctx->subtitle_map[page->pgno & 0x7ff];
>> +    int empty_lines = 0;
>> +    vbi_color cur_color = VBI_WHITE;
>> +    vbi_color cur_back_color = VBI_BLACK;
>> +    AVBPrint buf;
>> +
>> +    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
>> +
>> +    for (i = chop_top; i < page->rows; i++) {
>> +        vbi_char *row = page->text + i * page->columns;
>> +
>> +        get_trim_info(page, row, &leading, &trailing, &len);
>> +
>> +        if (len) {
>> +            if (last_leading != -1 && last_leading != leading || leading
>> > 5)
>> +                can_align_left = 0;
>> +            if (last_trailing != -1 && last_trailing != trailing ||
>> trailing > 2)
>> +                can_align_right = 0;
>> +            if (last_trailing != -1 && (FFABS((trailing - leading) -
>> (last_trailing - last_leading)) > 1) || trailing - leading > 4)
>> +                can_align_center = 0;
>> +            last_leading = leading;
>> +            last_trailing = trailing;
>> +            min_leading = FFMIN(leading, min_leading);
>> +            min_trailing = FFMIN(trailing, min_trailing);
>> +        }
>> +    }
>> +
>> +    if ((can_align_right == can_align_left) && !can_align_center) {
>> +        alignment = ctx->last_ass_alignment;
>> +    } else if (!can_align_right && can_align_left && !can_align_center) {
>> +        ctx->last_ass_alignment = alignment = 1;
>> +    } else if (!can_align_right && !can_align_left && can_align_center) {
>> +        ctx->last_ass_alignment = alignment = 2;
>> +    } else if (can_align_right && !can_align_left && !can_align_center) {
>> +        ctx->last_ass_alignment = alignment = 3;
>> +    } else {
>> +        if (ctx->last_ass_alignment == 1 && can_align_left ||
>> +            ctx->last_ass_alignment == 2 && can_align_center ||
>> +            ctx->last_ass_alignment == 3 && can_align_right)
>> +            alignment = ctx->last_ass_alignment;
>> +    }
>> +
>> +    for (i = chop_top; i < page->rows; i++) {
>> +        int j;
>> +        vbi_char *row = page->text + i * page->columns;
>> +        int is_transparent_line;
>> +
>> +        for (j = 0; j < page->columns; j++)
>> +            if (row[j].opacity != VBI_TRANSPARENT_SPACE)
>> +                break;
>> +        is_transparent_line = (j == page->columns);
>> +
>> +        len = is_transparent_line ? 0 : page->columns;
>> +        leading = trailing = is_transparent_line ? page->columns : 0;
>> +
>> +        if (is_subtitle_page) {
>> +            if (!is_transparent_line)
>> +                get_trim_info(page, row, &leading, &trailing, &len);
>> +
>> +            if (vertical_align == -1 && len) {
>> +                vertical_align = (2 - (av_clip(i + 1, 0, 23) / 8));
>> +                av_bprintf(&buf, "{\\an%d}", alignment + vertical_align
>> * 3);
>> +                if (vertical_align != 2)
>> +                    empty_lines = 0;
>> +            }
>> +
>> +            if (len && empty_lines > 1)
>> +                for (empty_lines /= 2; empty_lines > 0; empty_lines--)
>> +                    av_bprintf(&buf, " \\N");
>> +
>> +            if (alignment == 1)
>> +                leading = min_leading;
>> +            if (alignment == 3)
>> +                trailing = min_trailing;
>> +        }
>> +
>> +        if (len || !is_subtitle_page) {
>> +            decode_string(page, row, &buf, leading, page->columns -
>> trailing, &cur_color, &cur_back_color);
>> +            av_bprintf(&buf, " \\N");
>> +            empty_lines = 0;
>> +        } else {
>> +            empty_lines++;
>> +        }
>> +    }
>> +
>> +    if (vertical_align == 0)
>> +        for (empty_lines = (empty_lines - 1) / 2; empty_lines > 0;
>> empty_lines--)
>> +            av_bprintf(&buf, " \\N");
>> +
>> +    if (!av_bprint_is_complete(&buf)) {
>> +        av_bprint_finalize(&buf, NULL);
>> +        return AVERROR(ENOMEM);
>> +    }
>> +
>> +    if (buf.len) {
>> +        sub_rect->type = SUBTITLE_ASS;
>> +        sub_rect->ass = ff_ass_get_dialog(ctx->readorder++, 0,
>> is_subtitle_page ? "Subtitle" : "Teletext", NULL, buf.str);
>> +
>> +        if (!sub_rect->ass) {
>> +            av_bprint_finalize(&buf, NULL);
>> +            return AVERROR(ENOMEM);
>> +        }
>> +        av_log(ctx, AV_LOG_DEBUG, "subtext:%s:txetbus\n", sub_rect->ass);
>> +    } else {
>> +        sub_rect->type = SUBTITLE_NONE;
>> +    }
>> +    av_bprint_finalize(&buf, NULL);
>> +    return 0;
>> +}
>> +
>>  static void fix_transparency(TeletextContext *ctx, AVSubtitleRect
>> *sub_rect, vbi_page *page,
>>                               int chop_top, int resx, int resy)
>>  {
>> @@ -316,9 +544,20 @@ static void handler(vbi_event *ev, void *user_data)
>>              cur_page->pgno = ev->ev.ttx_page.pgno;
>>              cur_page->subno = ev->ev.ttx_page.subno;
>>              if (cur_page->sub_rect) {
>> -                res = (ctx->format_id == 0) ?
>> -                    gen_sub_bitmap(ctx, cur_page->sub_rect, &page,
>> chop_top) :
>> -                    gen_sub_text  (ctx, cur_page->sub_rect, &page,
>> chop_top);
>> +                switch (ctx->format_id) {
>> +                    case 0:
>> +                        res = gen_sub_bitmap(ctx, cur_page->sub_rect,
>> &page, chop_top);
>> +                        break;
>> +                    case 1:
>> +                        res = gen_sub_text(ctx, cur_page->sub_rect,
>> &page, chop_top);
>> +                        break;
>> +                    case 2:
>> +                        res = gen_sub_ass(ctx, cur_page->sub_rect,
>> &page, chop_top);
>> +                        break;
>> +                    default:
>> +                        res = AVERROR_BUG;
>> +                        break;
>> +                }
>>                  if (res < 0) {
>>                      av_freep(&cur_page->sub_rect);
>>                      ctx->handler_ret = res;
>> @@ -435,7 +674,7 @@ static int teletext_decode_frame(AVCodecContext
>> *avctx, void *data, int *data_si
>>      // is there a subtitle to pass?
>>      if (ctx->nb_pages) {
>>          int i;
>> -        sub->format = ctx->format_id;
>> +        sub->format = !!ctx->format_id;
>>          sub->start_display_time = 0;
>>          sub->end_display_time = ctx->sub_duration;
>>          sub->num_rects = 0;
>> @@ -494,12 +733,22 @@ static int teletext_init_decoder(AVCodecContext
>> *avctx)
>>
>>      ctx->vbi = NULL;
>>      ctx->pts = AV_NOPTS_VALUE;
>> +    ctx->last_ass_alignment = 2;
>>
>>      if (ctx->opacity == -1)
>>          ctx->opacity = ctx->transparent_bg ? 0 : 255;
>>
>>      av_log(avctx, AV_LOG_VERBOSE, "page filter: %s\n", ctx->pgno);
>> -    return (ctx->format_id == 1) ? ff_ass_subtitle_header_default(avctx)
>> : 0;
>> +
>> +    switch (ctx->format_id) {
>> +        case 0:
>> +            return 0;
>> +        case 1:
>> +            return ff_ass_subtitle_header_default(avctx);
>> +        case 2:
>> +            return my_ass_subtitle_header(avctx);
>> +    }
>> +    return AVERROR_BUG;
>>  }
>>
>>  static int teletext_close_decoder(AVCodecContext *avctx)
>> @@ -514,6 +763,7 @@ static int teletext_close_decoder(AVCodecContext
>> *avctx)
>>      vbi_decoder_delete(ctx->vbi);
>>      ctx->vbi = NULL;
>>      ctx->pts = AV_NOPTS_VALUE;
>> +    ctx->last_ass_alignment = 2;
>>      memset(ctx->subtitle_map, 0, sizeof(ctx->subtitle_map));
>>      if (!(avctx->flags2 & AV_CODEC_FLAG2_RO_FLUSH_NOOP))
>>          ctx->readorder = 0;
>> @@ -530,9 +780,10 @@ static void teletext_flush(AVCodecContext *avctx)
>>  static const AVOption options[] = {
>>      {"txt_page",        "list of teletext page numbers to decode, * is
>> all", OFFSET(pgno),           AV_OPT_TYPE_STRING, {.str = "*"},      0, 0,
>>       SD},
>>      {"txt_chop_top",    "discards the top teletext line",
>>     OFFSET(chop_top),       AV_OPT_TYPE_INT,    {.i64 = 1},        0, 1,
>>     SD},
>> -    {"txt_format",      "format of the subtitles (bitmap or text)",
>>     OFFSET(format_id),      AV_OPT_TYPE_INT,    {.i64 = 0},        0, 1,
>>     SD,  "txt_format"},
>> +    {"txt_format",      "format of the subtitles (bitmap or text or
>> ass)",   OFFSET(format_id),      AV_OPT_TYPE_INT,    {.i64 = 0},        0,
>> 2,        SD,  "txt_format"},
>>      {"bitmap",          NULL,
>>     0,                      AV_OPT_TYPE_CONST,  {.i64 = 0},        0, 0,
>>     SD,  "txt_format"},
>>      {"text",            NULL,
>>     0,                      AV_OPT_TYPE_CONST,  {.i64 = 1},        0, 0,
>>     SD,  "txt_format"},
>> +    {"ass",             NULL,
>>     0,                      AV_OPT_TYPE_CONST,  {.i64 = 2},        0, 0,
>>     SD,  "txt_format"},
>>      {"txt_left",        "x offset of generated bitmaps",
>>      OFFSET(x_offset),       AV_OPT_TYPE_INT,    {.i64 = 0},        0,
>> 65535,    SD},
>>      {"txt_top",         "y offset of generated bitmaps",
>>      OFFSET(y_offset),       AV_OPT_TYPE_INT,    {.i64 = 0},        0,
>> 65535,    SD},
>>      {"txt_chop_spaces", "chops leading and trailing spaces from text",
>>      OFFSET(chop_spaces),    AV_OPT_TYPE_INT,    {.i64 = 1},        0, 1,
>>       SD},
>> --
>> 2.13.6
>>
>> _______________________________________________
>> ffmpeg-devel mailing list
>> ffmpeg-devel@ffmpeg.org
>> http://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>>
>
>
Marton Balint May 7, 2018, 11:07 p.m. UTC | #3
On Mon, 7 May 2018, Aman Gupta wrote:

> 
> 
> On Mon, May 7, 2018 at 12:50 PM, Aman Gupta <ffmpeg@tmm1.net> wrote:
> 
>
>       On Sun, May 6, 2018 at 2:05 PM, Marton Balint <cus@passwd.hu> wrote:
>             Inspired by the VideoLAN text decoder and its port to FFmpeg made by Aman
>             Gupta.
> 
> 
> Thanks for incorporating my changes.
> 
> I ran some tests, and colors work as expected. Positioning also works well, and is also pretty close to my version.
> 
> 
> I found that some live streams are not chopping empty lines correctly. I see extra newlines at the end:
> 
>   Dialogue: 0,0:13:21.66,9:59:59.99,Default,,0,0,0,,{\an1}Simson,\hPeter\hHartcher,\hRobyn\hParker \Nand\hBenjamin \N \N \N
> 
> Here's a sample which shows extra newlines: https://s3.amazonaws.com/tmm1/teletext/capture_live1.mpg

This is kind of intentional, in this case the new lines are kept to 
position the subtitles a bit higher, and not to the very bottom of the 
screen, to keep the rough position of the teletext subtitles. At first I 
used \pos as well, but it only worked once per line for me, so I had to 
use some kind of newline-positioning anyway, and using native vertical 
alignment instead of \pos seemed nicer. (E.g. it allows the user to change 
left/right alignment text margins more easily in the headers or by 
an override).

> 
> It also looks like the "erase page" command is not being processed, which causes stale captions to stay on the screen in some cases.
> This is especially confusing when part of an old caption remains on one line, but then newer captions appear on another line.
> 
> Here's a sample that shows lines not being cleared correctly: https://s3.amazonaws.com/tmm1/teletext/simple.mpg
> 
> 
> With this sample, for instance, my decoder produces:
> 
> Dialogue: 0,0:00:01.90,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}and this, as you rightly say,\N
> Dialogue: 0,0:00:01.94,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}and this, as you rightly say,\N{\an8}{\pos(192,231)}        {\c&HFFFFFF&}is American.\N
> Dialogue: 0,0:00:04.86,9:59:59.99,Default,,0,0,0,,
> Dialogue: 0,0:00:04.94,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,231)}{\c&HFFFFFF&}It's rather nicely made.\N
> Dialogue: 0,0:00:06.62,9:59:59.99,Default,,0,0,0,,
> Dialogue: 0,0:00:06.70,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}It's got this fabulous\N
> Dialogue: 0,0:00:06.74,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}It's got this fabulous\N{\an8}{\pos(192,231)}   {\c&HFFFFFF&}cast finial here\N
> Dialogue: 0,0:00:10.34,9:59:59.99,Default,,0,0,0,,
> Dialogue: 0,0:00:10.42,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}of a very muscular figure\N
> Dialogue: 0,0:00:10.46,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}of a very muscular figure\N{\an8}{\pos(192,231)} {\c&HFFFFFF&}pulling this medallion.\N
> Dialogue: 0,0:00:15.50,9:59:59.99,Default,,0,0,0,,
> Dialogue: 0,0:00:15.58,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}So it's got strength to it. It's a\N
> Dialogue: 0,0:00:15.62,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)} {\c&HFFFFFF&}So it's got strength to it. It's a\N{\an8}{\pos(192,231)}{\c&HFFFFFF&}really characterful piece of silver.\N
> Dialogue: 0,0:00:19.14,9:59:59.99,Default,,0,0,0,,
> 
> And the decoder in this patch produces:
> 
> Dialogue: 0,0:00:02.02,9:59:59.99,Default,,0,0,0,,{\an2}and\hthis,\has\hyou\hrightly\hsay, \Nis\hAmerican. \N
> Dialogue: 0,0:00:05.02,9:59:59.99,Default,,0,0,0,,{\an2}and\hthis,\has\hyou\hrightly\hsay, \NIt's\hrather\hnicely\hmade. \N
> Dialogue: 0,0:00:06.82,9:59:59.99,Default,,0,0,0,,{\an2}It's\hgot\hthis\hfabulous \Ncast\hfinial\hhere \N
> Dialogue: 0,0:00:10.54,9:59:59.99,Default,,0,0,0,,{\an2}of\ha\hvery\hmuscular\hfigure \Npulling\hthis\hmedallion. \N
> Dialogue: 0,0:00:15.70,9:59:59.99,Default,,0,0,0,,{\an2}So\hit's\hgot\hstrength\hto\hit.\hIt's\ha \Nreally\hcharacterful\hpiece\hof\hsilver. \N
> Dialogue: 0,0:00:19.30,9:59:59.99,Default,,0,0,0,,{\an2}So\hit's\hgot\hstrength\hto\hit.\hIt's\ha \NBut\hmost\himportant\hof\hall, \N
> 
> At the ~5s mark, the text on the screen should say "It's rather nicely made", but this decoder still displays the line "and this as you rightly say" from the previous sentence.

The mix of different subtitles showing up seems like a libzvbi bug :( ... 
I will try to send a patch to sourceforge hoping it will get picked up and 
integrated to a future release...

Also there is a slight difference in how empty subtitles are forwarded. 
For empty subtitles I always used AVSubtitleRect type AVSUBTITLE_NONE, you 
used AVSUBTITLE_ASS. I prefer AVSUBTITLE_NONE, because with it
ffmpeg -fix_sub_duration creates a valid ass file without empty 
subtitles.

> I also have several other samples which use various features, available at the same URL:
> 
> capture_formatting1.mpg
> capture_formatting2.mpg
> capture_formatting3.mpg
> capture_live1.mpg
> capture_live2.mpg
> capture_notCaptionedMessage.mpg
> capture_threeLines1.mpg
> capture_threeLines2.mpg
> capture_threeLines3.mpg
> capture_threeLines4.mpg
> capture_yOffset1.mpg
> three.ts
> four.ts

I will have a look at those, thanks for your feedback.

Regards,
Marton
Aman Karmani May 7, 2018, 11:20 p.m. UTC | #4
On Mon, May 7, 2018 at 4:07 PM, Marton Balint <cus@passwd.hu> wrote:

>
>
> On Mon, 7 May 2018, Aman Gupta wrote:
>
>
>>
>> On Mon, May 7, 2018 at 12:50 PM, Aman Gupta <ffmpeg@tmm1.net> wrote:
>>
>>
>>       On Sun, May 6, 2018 at 2:05 PM, Marton Balint <cus@passwd.hu>
>> wrote:
>>             Inspired by the VideoLAN text decoder and its port to FFmpeg
>> made by Aman
>>             Gupta.
>>
>>
>> Thanks for incorporating my changes.
>>
>> I ran some tests, and colors work as expected. Positioning also works
>> well, and is also pretty close to my version.
>>
>>
>> I found that some live streams are not chopping empty lines correctly. I
>> see extra newlines at the end:
>>
>>   Dialogue: 0,0:13:21.66,9:59:59.99,Default,,0,0,0,,{\an1}Simson,\hPeter\hHartcher,\hRobyn\hParker
>> \Nand\hBenjamin \N \N \N
>>
>> Here's a sample which shows extra newlines: https://s3.amazonaws
>> .com/tmm1/teletext/capture_live1.mpg
>>
>
> This is kind of intentional, in this case the new lines are kept to
> position the subtitles a bit higher, and not to the very bottom of the
> screen, to keep the rough position of the teletext subtitles. At first I
> used \pos as well, but it only worked once per line for me, so I had to use
> some kind of newline-positioning anyway, and using native vertical
> alignment instead of \pos seemed nicer. (E.g. it allows the user to change
> left/right alignment text margins more easily in the headers or by an
> override).


Okay I see. I'm rendering my subtitles with a background so the large
half-empty bounding box looked strange to me.


>
>
>
>> It also looks like the "erase page" command is not being processed, which
>> causes stale captions to stay on the screen in some cases.
>> This is especially confusing when part of an old caption remains on one
>> line, but then newer captions appear on another line.
>>
>> Here's a sample that shows lines not being cleared correctly:
>> https://s3.amazonaws.com/tmm1/teletext/simple.mpg
>>
>>
>> With this sample, for instance, my decoder produces:
>>
>> Dialogue: 0,0:00:01.90,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}and
>> this, as you rightly say,\N
>> Dialogue: 0,0:00:01.94,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}and
>> this, as you rightly say,\N{\an8}{\pos(192,231)}        {\c&HFFFFFF&}is
>> American.\N
>> Dialogue: 0,0:00:04.86,9:59:59.99,Default,,0,0,0,,
>> Dialogue: 0,0:00:04.94,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,231)}{\c&HFFFFFF&}It's
>> rather nicely made.\N
>> Dialogue: 0,0:00:06.62,9:59:59.99,Default,,0,0,0,,
>> Dialogue: 0,0:00:06.70,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}It's
>> got this fabulous\N
>> Dialogue: 0,0:00:06.74,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}It's
>> got this fabulous\N{\an8}{\pos(192,231)}   {\c&HFFFFFF&}cast finial
>> here\N
>> Dialogue: 0,0:00:10.34,9:59:59.99,Default,,0,0,0,,
>> Dialogue: 0,0:00:10.42,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}of
>> a very muscular figure\N
>> Dialogue: 0,0:00:10.46,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}of
>> a very muscular figure\N{\an8}{\pos(192,231)} {\c&HFFFFFF&}pulling this
>> medallion.\N
>> Dialogue: 0,0:00:15.50,9:59:59.99,Default,,0,0,0,,
>> Dialogue: 0,0:00:15.58,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}So
>> it's got strength to it. It's a\N
>> Dialogue: 0,0:00:15.62,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}
>> {\c&HFFFFFF&}So it's got strength to it. It's a\N{\an8}{\pos(192,231)}{\c&HFFFFFF&}really
>> characterful piece of silver.\N
>> Dialogue: 0,0:00:19.14,9:59:59.99,Default,,0,0,0,,
>>
>> And the decoder in this patch produces:
>>
>> Dialogue: 0,0:00:02.02,9:59:59.99,Default,,0,0,0,,{\an2}and\hthis,\has\hyou\hrightly\hsay,
>> \Nis\hAmerican. \N
>> Dialogue: 0,0:00:05.02,9:59:59.99,Default,,0,0,0,,{\an2}and\hthis,\has\hyou\hrightly\hsay,
>> \NIt's\hrather\hnicely\hmade. \N
>> Dialogue: 0,0:00:06.82,9:59:59.99,Default,,0,0,0,,{\an2}It's\hgot\hthis\hfabulous
>> \Ncast\hfinial\hhere \N
>> Dialogue: 0,0:00:10.54,9:59:59.99,Default,,0,0,0,,{\an2}of\ha\hvery\hmuscular\hfigure
>> \Npulling\hthis\hmedallion. \N
>> Dialogue: 0,0:00:15.70,9:59:59.99,Default,,0,0,0,,{\an2}So\hit's\hgot\hstrength\hto\hit.\hIt's\ha
>> \Nreally\hcharacterful\hpiece\hof\hsilver. \N
>> Dialogue: 0,0:00:19.30,9:59:59.99,Default,,0,0,0,,{\an2}So\hit's\hgot\hstrength\hto\hit.\hIt's\ha
>> \NBut\hmost\himportant\hof\hall, \N
>>
>> At the ~5s mark, the text on the screen should say "It's rather nicely
>> made", but this decoder still displays the line "and this as you rightly
>> say" from the previous sentence.
>>
>
> The mix of different subtitles showing up seems like a libzvbi bug :( ...
> I will try to send a patch to sourceforge hoping it will get picked up and
> integrated to a future release...
>

I'm curious how much code there is in libzvbi that we're actually? i.e. How
practical would it be to import it into ffmpeg?


>
> Also there is a slight difference in how empty subtitles are forwarded.
> For empty subtitles I always used AVSubtitleRect type AVSUBTITLE_NONE, you
> used AVSUBTITLE_ASS. I prefer AVSUBTITLE_NONE, because with it
> ffmpeg -fix_sub_duration creates a valid ass file without empty subtitles.


Ah. Do those show up at all when using `-f ass`?

Aman


>
>
> I also have several other samples which use various features, available at
>> the same URL:
>>
>> capture_formatting1.mpg
>> capture_formatting2.mpg
>> capture_formatting3.mpg
>> capture_live1.mpg
>> capture_live2.mpg
>> capture_notCaptionedMessage.mpg
>> capture_threeLines1.mpg
>> capture_threeLines2.mpg
>> capture_threeLines3.mpg
>> capture_threeLines4.mpg
>> capture_yOffset1.mpg
>> three.ts
>> four.ts
>>
>
> I will have a look at those, thanks for your feedback.
>
> Regards,
> Marton
>
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> http://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
Aman Karmani May 18, 2018, 1:29 a.m. UTC | #5
On Mon, May 7, 2018 at 4:20 PM, Aman Gupta <ffmpeg@tmm1.net> wrote:

>
>
> On Mon, May 7, 2018 at 4:07 PM, Marton Balint <cus@passwd.hu> wrote:
>
>>
>>
>> On Mon, 7 May 2018, Aman Gupta wrote:
>>
>>
>>>
>>> On Mon, May 7, 2018 at 12:50 PM, Aman Gupta <ffmpeg@tmm1.net> wrote:
>>>
>>>
>>>       On Sun, May 6, 2018 at 2:05 PM, Marton Balint <cus@passwd.hu>
>>> wrote:
>>>             Inspired by the VideoLAN text decoder and its port to FFmpeg
>>> made by Aman
>>>             Gupta.
>>>
>>>
>>> Thanks for incorporating my changes.
>>>
>>> I ran some tests, and colors work as expected. Positioning also works
>>> well, and is also pretty close to my version.
>>>
>>>
>>> I found that some live streams are not chopping empty lines correctly. I
>>> see extra newlines at the end:
>>>
>>>   Dialogue: 0,0:13:21.66,9:59:59.99,Default,,0,0,0,,{\an1}Simson,\hPeter\hHartcher,\hRobyn\hParker
>>> \Nand\hBenjamin \N \N \N
>>>
>>> Here's a sample which shows extra newlines: https://s3.amazonaws
>>> .com/tmm1/teletext/capture_live1.mpg
>>>
>>
>> This is kind of intentional, in this case the new lines are kept to
>> position the subtitles a bit higher, and not to the very bottom of the
>> screen, to keep the rough position of the teletext subtitles. At first I
>> used \pos as well, but it only worked once per line for me, so I had to use
>> some kind of newline-positioning anyway, and using native vertical
>> alignment instead of \pos seemed nicer. (E.g. it allows the user to change
>> left/right alignment text margins more easily in the headers or by an
>> override).
>
>
> Okay I see. I'm rendering my subtitles with a background so the large
> half-empty bounding box looked strange to me.
>
>
>>
>>
>>
>>> It also looks like the "erase page" command is not being processed,
>>> which causes stale captions to stay on the screen in some cases.
>>> This is especially confusing when part of an old caption remains on one
>>> line, but then newer captions appear on another line.
>>>
>>> Here's a sample that shows lines not being cleared correctly:
>>> https://s3.amazonaws.com/tmm1/teletext/simple.mpg
>>>
>>>
>>> With this sample, for instance, my decoder produces:
>>>
>>> Dialogue: 0,0:00:01.90,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}and
>>> this, as you rightly say,\N
>>> Dialogue: 0,0:00:01.94,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}and
>>> this, as you rightly say,\N{\an8}{\pos(192,231)}        {\c&HFFFFFF&}is
>>> American.\N
>>> Dialogue: 0,0:00:04.86,9:59:59.99,Default,,0,0,0,,
>>> Dialogue: 0,0:00:04.94,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,231)}{\c&HFFFFFF&}It's
>>> rather nicely made.\N
>>> Dialogue: 0,0:00:06.62,9:59:59.99,Default,,0,0,0,,
>>> Dialogue: 0,0:00:06.70,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}It's
>>> got this fabulous\N
>>> Dialogue: 0,0:00:06.74,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}It's
>>> got this fabulous\N{\an8}{\pos(192,231)}   {\c&HFFFFFF&}cast finial
>>> here\N
>>> Dialogue: 0,0:00:10.34,9:59:59.99,Default,,0,0,0,,
>>> Dialogue: 0,0:00:10.42,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}of
>>> a very muscular figure\N
>>> Dialogue: 0,0:00:10.46,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}of
>>> a very muscular figure\N{\an8}{\pos(192,231)} {\c&HFFFFFF&}pulling this
>>> medallion.\N
>>> Dialogue: 0,0:00:15.50,9:59:59.99,Default,,0,0,0,,
>>> Dialogue: 0,0:00:15.58,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}{\c&HFFFFFF&}So
>>> it's got strength to it. It's a\N
>>> Dialogue: 0,0:00:15.62,9:59:59.99,Default,,0,0,0,,{\an8}{\pos(192,213)}
>>> {\c&HFFFFFF&}So it's got strength to it. It's a\N{\an8}{\pos(192,231)}{\c&HFFFFFF&}really
>>> characterful piece of silver.\N
>>> Dialogue: 0,0:00:19.14,9:59:59.99,Default,,0,0,0,,
>>>
>>> And the decoder in this patch produces:
>>>
>>> Dialogue: 0,0:00:02.02,9:59:59.99,Default,,0,0,0,,{\an2}and\hthis,\has\hyou\hrightly\hsay,
>>> \Nis\hAmerican. \N
>>> Dialogue: 0,0:00:05.02,9:59:59.99,Default,,0,0,0,,{\an2}and\hthis,\has\hyou\hrightly\hsay,
>>> \NIt's\hrather\hnicely\hmade. \N
>>> Dialogue: 0,0:00:06.82,9:59:59.99,Default,,0,0,0,,{\an2}It's\hgot\hthis\hfabulous
>>> \Ncast\hfinial\hhere \N
>>> Dialogue: 0,0:00:10.54,9:59:59.99,Default,,0,0,0,,{\an2}of\ha\hvery\hmuscular\hfigure
>>> \Npulling\hthis\hmedallion. \N
>>> Dialogue: 0,0:00:15.70,9:59:59.99,Default,,0,0,0,,{\an2}So\hit's\hgot\hstrength\hto\hit.\hIt's\ha
>>> \Nreally\hcharacterful\hpiece\hof\hsilver. \N
>>> Dialogue: 0,0:00:19.30,9:59:59.99,Default,,0,0,0,,{\an2}So\hit's\hgot\hstrength\hto\hit.\hIt's\ha
>>> \NBut\hmost\himportant\hof\hall, \N
>>>
>>> At the ~5s mark, the text on the screen should say "It's rather nicely
>>> made", but this decoder still displays the line "and this as you rightly
>>> say" from the previous sentence.
>>>
>>
>> The mix of different subtitles showing up seems like a libzvbi bug :( ...
>> I will try to send a patch to sourceforge hoping it will get picked up and
>> integrated to a future release...
>>
>
> I'm curious how much code there is in libzvbi that we're actually? i.e.
> How practical would it be to import it into ffmpeg?
>

For reference, the patch to fix the issue I described above was submitted
by Marton here: https://sourceforge.net/p/zapping/patches/20/

To answer my own question, it would be impractical to import libzvbi as
there is quite a lot of code. The file touched by that patch alone is
several thousand lines long.

Aman


>
>
>>
>> Also there is a slight difference in how empty subtitles are forwarded.
>> For empty subtitles I always used AVSubtitleRect type AVSUBTITLE_NONE, you
>> used AVSUBTITLE_ASS. I prefer AVSUBTITLE_NONE, because with it
>> ffmpeg -fix_sub_duration creates a valid ass file without empty subtitles.
>
>
> Ah. Do those show up at all when using `-f ass`?
>
> Aman
>
>
>>
>>
>> I also have several other samples which use various features, available
>>> at the same URL:
>>>
>>> capture_formatting1.mpg
>>> capture_formatting2.mpg
>>> capture_formatting3.mpg
>>> capture_live1.mpg
>>> capture_live2.mpg
>>> capture_notCaptionedMessage.mpg
>>> capture_threeLines1.mpg
>>> capture_threeLines2.mpg
>>> capture_threeLines3.mpg
>>> capture_threeLines4.mpg
>>> capture_yOffset1.mpg
>>> three.ts
>>> four.ts
>>>
>>
>> I will have a look at those, thanks for your feedback.
>>
>> Regards,
>> Marton
>>
>> _______________________________________________
>> ffmpeg-devel mailing list
>> ffmpeg-devel@ffmpeg.org
>> http://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>>
>
>
Marton Balint Sept. 29, 2018, 11:24 p.m. UTC | #6
On Thu, 17 May 2018, Aman Gupta wrote:

>>>>       On Sun, May 6, 2018 at 2:05 PM, Marton Balint <cus@passwd.hu>
>>>> wrote:
>>>>             Inspired by the VideoLAN text decoder and its port to FFmpeg
>>>> made by Aman
>>>>             Gupta.
>>>>

This got kind of forgotten, but now I pushed it after a rebase and a minor 
fix.

Regards,
Marton
diff mbox

Patch

diff --git a/doc/decoders.texi b/doc/decoders.texi
index 8f07bc1afb..cc9b2ef123 100644
--- a/doc/decoders.texi
+++ b/doc/decoders.texi
@@ -255,12 +255,18 @@  Default value is *.
 @item txt_chop_top
 Discards the top teletext line. Default value is 1.
 @item txt_format
-Specifies the format of the decoded subtitles. The teletext decoder is capable
-of decoding the teletext pages to bitmaps or to simple text, you should use
-"bitmap" for teletext pages, because certain graphics and colors cannot be
-expressed in simple text. You might use "text" for teletext based subtitles if
-your application can handle simple text based subtitles. Default value is
-bitmap.
+Specifies the format of the decoded subtitles.
+@table @option
+@item bitmap
+The default format, you should use this for teletext pages, because certain
+graphics and colors cannot be expressed in simple text or even ASS.
+@item text
+Simple text based output without formatting.
+@item ass
+Formatted ASS output, subtitle pages and teletext pages are returned in
+different styles, subtitle pages are stripped down to text, but an effort is
+made to keep the text alignment and the formatting.
+@end table
 @item txt_left
 X offset of generated bitmaps, default is 0.
 @item txt_top
diff --git a/libavcodec/libzvbi-teletextdec.c b/libavcodec/libzvbi-teletextdec.c
index 56a7182882..7541de0d53 100644
--- a/libavcodec/libzvbi-teletextdec.c
+++ b/libavcodec/libzvbi-teletextdec.c
@@ -26,6 +26,7 @@ 
 #include "libavutil/internal.h"
 #include "libavutil/intreadwrite.h"
 #include "libavutil/log.h"
+#include "libavutil/common.h"
 
 #include <libzvbi.h>
 
@@ -56,7 +57,7 @@  typedef struct TeletextContext
     char           *pgno;
     int             x_offset;
     int             y_offset;
-    int             format_id; /* 0 = bitmap, 1 = text/ass */
+    int             format_id; /* 0 = bitmap, 1 = text/ass, 2 = ass */
     int             chop_top;
     int             sub_duration; /* in msec */
     int             transparent_bg;
@@ -74,8 +75,55 @@  typedef struct TeletextContext
 
     int             readorder;
     uint8_t         subtitle_map[2048];
+    int             last_ass_alignment;
 } TeletextContext;
 
+static int my_ass_subtitle_header(AVCodecContext *avctx)
+{
+    int ret = ff_ass_subtitle_header_default(avctx);
+    char *new_header;
+    uint8_t *event_pos;
+
+    if (ret < 0)
+        return ret;
+
+    event_pos = strstr(avctx->subtitle_header, "\r\n[Events]\r\n");
+    if (!event_pos)
+        return AVERROR_BUG;
+
+    new_header = av_asprintf("%.*s%s%s",
+        (int)(event_pos - avctx->subtitle_header), avctx->subtitle_header,
+        "Style: "
+        "Teletext,"            /* Name */
+        "Monospace,11,"        /* Font{name,size} */
+        "&Hffffff,&Hffffff,&H0,&H0," /* {Primary,Secondary,Outline,Back}Colour */
+        "0,0,0,0,"             /* Bold, Italic, Underline, StrikeOut */
+        "160,100,"             /* Scale{X,Y} */
+        "0,0,"                 /* Spacing, Angle */
+        "3,0.1,0,"             /* BorderStyle, Outline, Shadow */
+        "5,1,1,1,"             /* Alignment, Margin[LRV] */
+        "0\r\n"                /* Encoding */
+        "Style: "
+        "Subtitle,"            /* Name */
+        "Monospace,16,"        /* Font{name,size} */
+        "&Hffffff,&Hffffff,&H0,&H0," /* {Primary,Secondary,Outline,Back}Colour */
+        "0,0,0,0,"             /* Bold, Italic, Underline, StrikeOut */
+        "100,100,"             /* Scale{X,Y} */
+        "0,0,"                 /* Spacing, Angle */
+        "1,1,1,"               /* BorderStyle, Outline, Shadow */
+        "8,48,48,20,"          /* Alignment, Margin[LRV] */
+        "0\r\n"                /* Encoding */
+        , event_pos);
+
+    if (!new_header)
+        return AVERROR(ENOMEM);
+
+    av_free(avctx->subtitle_header);
+    avctx->subtitle_header = new_header;
+    avctx->subtitle_header_size = strlen(new_header);
+    return 0;
+}
+
 static int chop_spaces_utf8(const unsigned char* t, int len)
 {
     t += len;
@@ -179,6 +227,186 @@  static int gen_sub_text(TeletextContext *ctx, AVSubtitleRect *sub_rect, vbi_page
     return 0;
 }
 
+static void bprint_color(const char *type, AVBPrint *buf, vbi_page *page, unsigned ci)
+{
+    int r = VBI_R(page->color_map[ci]);
+    int g = VBI_G(page->color_map[ci]);
+    int b = VBI_B(page->color_map[ci]);
+    av_bprintf(buf, "{\\%s&H%02X%02X%02X&}", type, b, g, r);
+}
+
+#define IS_TXT_SPACE(ch) ((ch).unicode < 0x0020 || (ch).unicode >= 0xe000 || (ch).unicode == 0x00a0 ||\
+                          (ch).size > VBI_DOUBLE_SIZE || (ch).opacity == VBI_TRANSPARENT_SPACE)
+
+static void get_trim_info(vbi_page *page, vbi_char *row, int *leading, int *trailing, int *olen)
+{
+    int i, len = 0;
+    int char_seen = 0;
+
+    *leading = 0;
+
+    for (i = 0; i < page->columns; i++) {
+        uint16_t out = IS_TXT_SPACE(row[i]) ? 32 : row[i].unicode;
+
+        if (out == 32 && !char_seen)
+            (*leading)++;
+        else if (out != 32)
+            char_seen = 1, len = i - (*leading) + 1;
+    }
+
+    *olen = len;
+    *trailing = len > 0 ? page->columns - *leading - len : page->columns;
+}
+
+static void decode_string(vbi_page *page, vbi_char *row, AVBPrint *buf,
+                          int start, int end, vbi_color *cur_color, vbi_color *cur_back_color)
+{
+    int i;
+
+    for (i = start; i < end; i++) {
+        uint16_t out = IS_TXT_SPACE(row[i]) ? 32 : row[i].unicode;
+
+        if (*cur_color != row[i].foreground) {
+            bprint_color("c", buf, page, row[i].foreground);
+            *cur_color = row[i].foreground;
+        }
+        if (*cur_back_color != row[i].background) {
+            bprint_color("3c", buf, page, row[i].background);
+            *cur_back_color = row[i].background;
+        }
+
+        if (out == 32) {
+            av_bprintf(buf, "\\h");
+        } else if (out == '\\' || out == '{' || out == '}') {
+            av_bprintf(buf, "\\%c", (char)out);
+        } else {
+            char tmp;
+            /* convert to utf-8 */
+            PUT_UTF8(out, tmp, av_bprint_chars(buf, tmp, 1););
+        }
+    }
+}
+
+/* Draw a page as ass formatted text */
+static int gen_sub_ass(TeletextContext *ctx, AVSubtitleRect *sub_rect, vbi_page *page, int chop_top)
+{
+    int i;
+    int leading, trailing, len;
+    int last_trailing = -1, last_leading = -1;
+    int min_trailing = page->columns, min_leading = page->columns;
+    int alignment = 2;
+    int vertical_align = -1;
+    int can_align_left = 1, can_align_right = 1, can_align_center = 1;
+    int is_subtitle_page = ctx->subtitle_map[page->pgno & 0x7ff];
+    int empty_lines = 0;
+    vbi_color cur_color = VBI_WHITE;
+    vbi_color cur_back_color = VBI_BLACK;
+    AVBPrint buf;
+
+    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
+
+    for (i = chop_top; i < page->rows; i++) {
+        vbi_char *row = page->text + i * page->columns;
+
+        get_trim_info(page, row, &leading, &trailing, &len);
+
+        if (len) {
+            if (last_leading != -1 && last_leading != leading || leading > 5)
+                can_align_left = 0;
+            if (last_trailing != -1 && last_trailing != trailing || trailing > 2)
+                can_align_right = 0;
+            if (last_trailing != -1 && (FFABS((trailing - leading) - (last_trailing - last_leading)) > 1) || trailing - leading > 4)
+                can_align_center = 0;
+            last_leading = leading;
+            last_trailing = trailing;
+            min_leading = FFMIN(leading, min_leading);
+            min_trailing = FFMIN(trailing, min_trailing);
+        }
+    }
+
+    if ((can_align_right == can_align_left) && !can_align_center) {
+        alignment = ctx->last_ass_alignment;
+    } else if (!can_align_right && can_align_left && !can_align_center) {
+        ctx->last_ass_alignment = alignment = 1;
+    } else if (!can_align_right && !can_align_left && can_align_center) {
+        ctx->last_ass_alignment = alignment = 2;
+    } else if (can_align_right && !can_align_left && !can_align_center) {
+        ctx->last_ass_alignment = alignment = 3;
+    } else {
+        if (ctx->last_ass_alignment == 1 && can_align_left ||
+            ctx->last_ass_alignment == 2 && can_align_center ||
+            ctx->last_ass_alignment == 3 && can_align_right)
+            alignment = ctx->last_ass_alignment;
+    }
+
+    for (i = chop_top; i < page->rows; i++) {
+        int j;
+        vbi_char *row = page->text + i * page->columns;
+        int is_transparent_line;
+
+        for (j = 0; j < page->columns; j++)
+            if (row[j].opacity != VBI_TRANSPARENT_SPACE)
+                break;
+        is_transparent_line = (j == page->columns);
+
+        len = is_transparent_line ? 0 : page->columns;
+        leading = trailing = is_transparent_line ? page->columns : 0;
+
+        if (is_subtitle_page) {
+            if (!is_transparent_line)
+                get_trim_info(page, row, &leading, &trailing, &len);
+
+            if (vertical_align == -1 && len) {
+                vertical_align = (2 - (av_clip(i + 1, 0, 23) / 8));
+                av_bprintf(&buf, "{\\an%d}", alignment + vertical_align * 3);
+                if (vertical_align != 2)
+                    empty_lines = 0;
+            }
+
+            if (len && empty_lines > 1)
+                for (empty_lines /= 2; empty_lines > 0; empty_lines--)
+                    av_bprintf(&buf, " \\N");
+
+            if (alignment == 1)
+                leading = min_leading;
+            if (alignment == 3)
+                trailing = min_trailing;
+        }
+
+        if (len || !is_subtitle_page) {
+            decode_string(page, row, &buf, leading, page->columns - trailing, &cur_color, &cur_back_color);
+            av_bprintf(&buf, " \\N");
+            empty_lines = 0;
+        } else {
+            empty_lines++;
+        }
+    }
+
+    if (vertical_align == 0)
+        for (empty_lines = (empty_lines - 1) / 2; empty_lines > 0; empty_lines--)
+            av_bprintf(&buf, " \\N");
+
+    if (!av_bprint_is_complete(&buf)) {
+        av_bprint_finalize(&buf, NULL);
+        return AVERROR(ENOMEM);
+    }
+
+    if (buf.len) {
+        sub_rect->type = SUBTITLE_ASS;
+        sub_rect->ass = ff_ass_get_dialog(ctx->readorder++, 0, is_subtitle_page ? "Subtitle" : "Teletext", NULL, buf.str);
+
+        if (!sub_rect->ass) {
+            av_bprint_finalize(&buf, NULL);
+            return AVERROR(ENOMEM);
+        }
+        av_log(ctx, AV_LOG_DEBUG, "subtext:%s:txetbus\n", sub_rect->ass);
+    } else {
+        sub_rect->type = SUBTITLE_NONE;
+    }
+    av_bprint_finalize(&buf, NULL);
+    return 0;
+}
+
 static void fix_transparency(TeletextContext *ctx, AVSubtitleRect *sub_rect, vbi_page *page,
                              int chop_top, int resx, int resy)
 {
@@ -316,9 +544,20 @@  static void handler(vbi_event *ev, void *user_data)
             cur_page->pgno = ev->ev.ttx_page.pgno;
             cur_page->subno = ev->ev.ttx_page.subno;
             if (cur_page->sub_rect) {
-                res = (ctx->format_id == 0) ?
-                    gen_sub_bitmap(ctx, cur_page->sub_rect, &page, chop_top) :
-                    gen_sub_text  (ctx, cur_page->sub_rect, &page, chop_top);
+                switch (ctx->format_id) {
+                    case 0:
+                        res = gen_sub_bitmap(ctx, cur_page->sub_rect, &page, chop_top);
+                        break;
+                    case 1:
+                        res = gen_sub_text(ctx, cur_page->sub_rect, &page, chop_top);
+                        break;
+                    case 2:
+                        res = gen_sub_ass(ctx, cur_page->sub_rect, &page, chop_top);
+                        break;
+                    default:
+                        res = AVERROR_BUG;
+                        break;
+                }
                 if (res < 0) {
                     av_freep(&cur_page->sub_rect);
                     ctx->handler_ret = res;
@@ -435,7 +674,7 @@  static int teletext_decode_frame(AVCodecContext *avctx, void *data, int *data_si
     // is there a subtitle to pass?
     if (ctx->nb_pages) {
         int i;
-        sub->format = ctx->format_id;
+        sub->format = !!ctx->format_id;
         sub->start_display_time = 0;
         sub->end_display_time = ctx->sub_duration;
         sub->num_rects = 0;
@@ -494,12 +733,22 @@  static int teletext_init_decoder(AVCodecContext *avctx)
 
     ctx->vbi = NULL;
     ctx->pts = AV_NOPTS_VALUE;
+    ctx->last_ass_alignment = 2;
 
     if (ctx->opacity == -1)
         ctx->opacity = ctx->transparent_bg ? 0 : 255;
 
     av_log(avctx, AV_LOG_VERBOSE, "page filter: %s\n", ctx->pgno);
-    return (ctx->format_id == 1) ? ff_ass_subtitle_header_default(avctx) : 0;
+
+    switch (ctx->format_id) {
+        case 0:
+            return 0;
+        case 1:
+            return ff_ass_subtitle_header_default(avctx);
+        case 2:
+            return my_ass_subtitle_header(avctx);
+    }
+    return AVERROR_BUG;
 }
 
 static int teletext_close_decoder(AVCodecContext *avctx)
@@ -514,6 +763,7 @@  static int teletext_close_decoder(AVCodecContext *avctx)
     vbi_decoder_delete(ctx->vbi);
     ctx->vbi = NULL;
     ctx->pts = AV_NOPTS_VALUE;
+    ctx->last_ass_alignment = 2;
     memset(ctx->subtitle_map, 0, sizeof(ctx->subtitle_map));
     if (!(avctx->flags2 & AV_CODEC_FLAG2_RO_FLUSH_NOOP))
         ctx->readorder = 0;
@@ -530,9 +780,10 @@  static void teletext_flush(AVCodecContext *avctx)
 static const AVOption options[] = {
     {"txt_page",        "list of teletext page numbers to decode, * is all", OFFSET(pgno),           AV_OPT_TYPE_STRING, {.str = "*"},      0, 0,        SD},
     {"txt_chop_top",    "discards the top teletext line",                    OFFSET(chop_top),       AV_OPT_TYPE_INT,    {.i64 = 1},        0, 1,        SD},
-    {"txt_format",      "format of the subtitles (bitmap or text)",          OFFSET(format_id),      AV_OPT_TYPE_INT,    {.i64 = 0},        0, 1,        SD,  "txt_format"},
+    {"txt_format",      "format of the subtitles (bitmap or text or ass)",   OFFSET(format_id),      AV_OPT_TYPE_INT,    {.i64 = 0},        0, 2,        SD,  "txt_format"},
     {"bitmap",          NULL,                                                0,                      AV_OPT_TYPE_CONST,  {.i64 = 0},        0, 0,        SD,  "txt_format"},
     {"text",            NULL,                                                0,                      AV_OPT_TYPE_CONST,  {.i64 = 1},        0, 0,        SD,  "txt_format"},
+    {"ass",             NULL,                                                0,                      AV_OPT_TYPE_CONST,  {.i64 = 2},        0, 0,        SD,  "txt_format"},
     {"txt_left",        "x offset of generated bitmaps",                     OFFSET(x_offset),       AV_OPT_TYPE_INT,    {.i64 = 0},        0, 65535,    SD},
     {"txt_top",         "y offset of generated bitmaps",                     OFFSET(y_offset),       AV_OPT_TYPE_INT,    {.i64 = 0},        0, 65535,    SD},
     {"txt_chop_spaces", "chops leading and trailing spaces from text",       OFFSET(chop_spaces),    AV_OPT_TYPE_INT,    {.i64 = 1},        0, 1,        SD},