You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
502 lines
15 KiB
502 lines
15 KiB
/*****************************************************************************
|
|
* stl.c: EBU STL decoder
|
|
*****************************************************************************
|
|
* Copyright (C) 2010 Laurent Aimar
|
|
*
|
|
* Authors: Laurent Aimar <fenrir _AT_ videolan _DOT_ org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
|
|
/*****************************************************************************
|
|
* Preamble
|
|
*****************************************************************************/
|
|
#ifdef HAVE_CONFIG_H
|
|
# include "config.h"
|
|
#endif
|
|
#include <assert.h>
|
|
|
|
#include <vlc_common.h>
|
|
#include <vlc_plugin.h>
|
|
#include <vlc_codec.h>
|
|
#include <vlc_charset.h>
|
|
|
|
#include "substext.h" /* required for font scaling / updater */
|
|
|
|
/*****************************************************************************
|
|
* Module descriptor
|
|
*****************************************************************************/
|
|
static int Open (vlc_object_t *);
|
|
static void Close(vlc_object_t *);
|
|
|
|
vlc_module_begin()
|
|
set_description(N_("EBU STL subtitles decoder"))
|
|
set_subcategory(SUBCAT_INPUT_SCODEC)
|
|
set_capability("spu decoder", 10)
|
|
set_callbacks(Open, Close)
|
|
vlc_module_end()
|
|
|
|
/*****************************************************************************
|
|
* Local definitions/prototypes
|
|
*****************************************************************************/
|
|
#define GSI_BLOCK_SIZE 1024
|
|
|
|
#define STL_GROUPS_MAX 255
|
|
|
|
#define STL_TEXTFIELD_SIZE 112
|
|
#define STL_TTI_HEADER_SIZE 16
|
|
#define STL_TTI_SIZE (STL_TTI_HEADER_SIZE + STL_TEXTFIELD_SIZE)
|
|
|
|
#define STL_TF_TELETEXT_FIRST 0x00
|
|
#define STL_TF_TELETEXT_LAST 0x1f
|
|
#define STL_TF_CHARCODE1_FIRST 0x20
|
|
#define STL_TF_CHARCODE1_LAST 0x7f
|
|
#define STL_TF_ITALICS_ON 0x80
|
|
#define STL_TF_ITALICS_OFF 0x81
|
|
#define STL_TF_UNDERLINE_ON 0x82
|
|
#define STL_TF_UNDERLINE_OFF 0x83
|
|
#define STL_TF_BOXING_ON 0x84
|
|
#define STL_TF_BOXING_OFF 0x85
|
|
#define STL_TF_LINEBREAK 0x8a
|
|
#define STL_TF_END_FILL 0x8f
|
|
#define STL_TF_CHARCODE2_FIRST 0xa1
|
|
|
|
typedef enum {
|
|
CCT_ISO_6937_2 = 0x3030, CCT_BEGIN = CCT_ISO_6937_2,
|
|
CCT_ISO_8859_5 = 0x3031,
|
|
CCT_ISO_8859_6 = 0x3032,
|
|
CCT_ISO_8859_7 = 0x3033,
|
|
CCT_ISO_8859_8 = 0x3034, CCT_END = CCT_ISO_8859_8
|
|
} cct_number_value_t;
|
|
|
|
typedef struct
|
|
{
|
|
uint8_t i_accumulating;
|
|
uint8_t i_justify;
|
|
vlc_tick_t i_start;
|
|
vlc_tick_t i_end;
|
|
text_style_t *p_style;
|
|
text_segment_t *p_segment;
|
|
text_segment_t **pp_segment_last;
|
|
} stl_sg_t;
|
|
|
|
typedef struct {
|
|
cct_number_value_t value;
|
|
const char *str;
|
|
} cct_number_t;
|
|
|
|
typedef struct
|
|
{
|
|
stl_sg_t groups[STL_GROUPS_MAX + 1];
|
|
cct_number_value_t cct;
|
|
uint8_t i_fps;
|
|
} decoder_sys_t;
|
|
|
|
static cct_number_t cct_nums[] = { {CCT_ISO_6937_2, "ISO_6937-2"},
|
|
{CCT_ISO_8859_5, "ISO_8859-5"},
|
|
{CCT_ISO_8859_6, "ISO_8859-6"},
|
|
{CCT_ISO_8859_7, "ISO_8859-7"},
|
|
{CCT_ISO_8859_8, "ISO_8859-8"} };
|
|
|
|
static text_style_t * CreateGroupStyle(void)
|
|
{
|
|
text_style_t *p_style = text_style_Create(STYLE_NO_DEFAULTS);
|
|
if(p_style)
|
|
{
|
|
p_style->i_features = STYLE_HAS_FLAGS|STYLE_HAS_BACKGROUND_ALPHA|STYLE_HAS_BACKGROUND_COLOR;
|
|
/* Teletext needs default background to black */
|
|
p_style->i_background_alpha = STYLE_ALPHA_OPAQUE;
|
|
p_style->i_background_color = 0x000000;
|
|
p_style->i_font_size = 0;
|
|
p_style->f_font_relsize = STYLE_DEFAULT_REL_FONT_SIZE;
|
|
}
|
|
return p_style;
|
|
}
|
|
|
|
static void TextBufferFlush(stl_sg_t *p_group, uint8_t *p_buf, uint8_t *pi_buf,
|
|
const char *psz_charset)
|
|
{
|
|
if(*pi_buf == 0)
|
|
return;
|
|
|
|
char *psz_utf8 = FromCharset(psz_charset, p_buf, *pi_buf);
|
|
if(psz_utf8)
|
|
{
|
|
*p_group->pp_segment_last = text_segment_New(psz_utf8);
|
|
if(*p_group->pp_segment_last)
|
|
{
|
|
if(p_group->p_style)
|
|
(*p_group->pp_segment_last)->style = text_style_Duplicate(p_group->p_style);
|
|
p_group->pp_segment_last = &((*p_group->pp_segment_last)->p_next);
|
|
}
|
|
free(psz_utf8);
|
|
}
|
|
|
|
*pi_buf = 0;
|
|
}
|
|
|
|
static void GroupParseTeletext(stl_sg_t *p_group, uint8_t code)
|
|
{
|
|
if(p_group->p_style == NULL &&
|
|
!(p_group->p_style = CreateGroupStyle()))
|
|
return;
|
|
|
|
/* See ETS 300 706 Table 26 as EBU 3264 does only name values
|
|
and does not explain at all */
|
|
|
|
static const uint32_t colors[] =
|
|
{
|
|
0x000000,
|
|
0xFF0000,
|
|
0x00FF00,
|
|
0xFFFF00,
|
|
0x0000FF,
|
|
0xFF00FF,
|
|
0x00FFFF,
|
|
0xFFFFFF,
|
|
};
|
|
|
|
/* Teletext data received, so we need to enable background */
|
|
p_group->p_style->i_style_flags |= STYLE_BACKGROUND;
|
|
|
|
switch(code)
|
|
{
|
|
case 0x0c:
|
|
p_group->p_style->f_font_relsize = STYLE_DEFAULT_REL_FONT_SIZE;
|
|
p_group->p_style->i_style_flags &= ~(STYLE_DOUBLEWIDTH|STYLE_HALFWIDTH);
|
|
break;
|
|
|
|
case 0x0d: /* double height */
|
|
p_group->p_style->f_font_relsize = STYLE_DEFAULT_REL_FONT_SIZE * 2;
|
|
p_group->p_style->i_style_flags &= ~STYLE_DOUBLEWIDTH;
|
|
p_group->p_style->i_style_flags |= STYLE_HALFWIDTH;
|
|
break;
|
|
|
|
case 0x0e: /* double width */
|
|
p_group->p_style->f_font_relsize = STYLE_DEFAULT_REL_FONT_SIZE;
|
|
p_group->p_style->i_style_flags &= ~STYLE_HALFWIDTH;
|
|
p_group->p_style->i_style_flags |= STYLE_DOUBLEWIDTH;
|
|
break;
|
|
|
|
case 0x0f: /* double size */
|
|
p_group->p_style->f_font_relsize = STYLE_DEFAULT_REL_FONT_SIZE * 2;
|
|
p_group->p_style->i_style_flags &= ~(STYLE_DOUBLEWIDTH|STYLE_HALFWIDTH);
|
|
break;
|
|
|
|
case 0x1d:
|
|
p_group->p_style->i_background_color = p_group->p_style->i_font_color;
|
|
p_group->p_style->i_features &= ~STYLE_HAS_FONT_COLOR;
|
|
break;
|
|
|
|
case 0x1c:
|
|
p_group->p_style->i_background_color = colors[0];
|
|
break;
|
|
|
|
default:
|
|
if(code < 8)
|
|
{
|
|
p_group->p_style->i_font_color = colors[code];
|
|
p_group->p_style->i_features |= STYLE_HAS_FONT_COLOR;
|
|
}
|
|
|
|
/* Need to handle Mosaic ? Really ? */
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
static void GroupApplyStyle(stl_sg_t *p_group, uint8_t code)
|
|
{
|
|
if(p_group->p_style == NULL &&
|
|
!(p_group->p_style = CreateGroupStyle()))
|
|
return;
|
|
|
|
switch(code)
|
|
{
|
|
case STL_TF_ITALICS_ON:
|
|
p_group->p_style->i_style_flags |= STYLE_ITALIC;
|
|
break;
|
|
case STL_TF_ITALICS_OFF:
|
|
p_group->p_style->i_style_flags &= ~STYLE_ITALIC;
|
|
break;
|
|
case STL_TF_UNDERLINE_ON:
|
|
p_group->p_style->i_style_flags |= STYLE_UNDERLINE;
|
|
break;
|
|
case STL_TF_UNDERLINE_OFF:
|
|
p_group->p_style->i_style_flags &= ~STYLE_UNDERLINE;
|
|
break;
|
|
case STL_TF_BOXING_ON:
|
|
case STL_TF_BOXING_OFF:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static vlc_tick_t ParseTimeCode(const uint8_t *data, double fps)
|
|
{
|
|
return vlc_tick_from_sec( data[0] * 3600 +
|
|
data[1] * 60 +
|
|
data[2] * 1 +
|
|
data[3] / fps);
|
|
}
|
|
|
|
static void ClearTeletextStyles(stl_sg_t *p_group)
|
|
{
|
|
if(p_group->p_style)
|
|
{
|
|
p_group->p_style->i_features &= ~STYLE_HAS_FONT_COLOR;
|
|
p_group->p_style->i_background_color = 0x000000;
|
|
p_group->p_style->f_font_relsize = STYLE_DEFAULT_REL_FONT_SIZE;
|
|
p_group->p_style->i_style_flags &= ~(STYLE_DOUBLEWIDTH|STYLE_HALFWIDTH);
|
|
}
|
|
}
|
|
|
|
/* Returns true if group is we need to output group */
|
|
static bool ParseTTI(stl_sg_t *p_group, const uint8_t *p_data, const char *psz_charset, double fps)
|
|
{
|
|
uint8_t p_buffer[STL_TEXTFIELD_SIZE];
|
|
uint8_t i_buffer = 0;
|
|
|
|
/* Header */
|
|
uint8_t ebn = p_data[3];
|
|
if(ebn > 0xef && ebn != 0xff)
|
|
return false;
|
|
|
|
if(p_data[15] != 0x00) /* comment flag */
|
|
return false;
|
|
|
|
if(p_data[14] > 0x00)
|
|
p_group->i_justify = p_data[14];
|
|
|
|
/* Accumulating started or continuing.
|
|
* We must not flush current segments on output and continue on next block */
|
|
p_group->i_accumulating = (p_data[4] == 0x01 || p_data[4] == 0x02);
|
|
|
|
p_group->i_start = ParseTimeCode( &p_data[5], fps );
|
|
p_group->i_end = ParseTimeCode( &p_data[9], fps );
|
|
|
|
/* Text Field */
|
|
for (size_t i = STL_TTI_HEADER_SIZE; i < STL_TTI_SIZE; i++)
|
|
{
|
|
const uint8_t code = p_data[i];
|
|
switch(code)
|
|
{
|
|
case STL_TF_LINEBREAK:
|
|
p_buffer[i_buffer++] = '\n';
|
|
TextBufferFlush(p_group, p_buffer, &i_buffer, psz_charset);
|
|
/* Clear teletext styles on each new row */
|
|
ClearTeletextStyles(p_group);
|
|
break;
|
|
|
|
case STL_TF_END_FILL:
|
|
TextBufferFlush(p_group, p_buffer, &i_buffer, psz_charset);
|
|
ClearTeletextStyles(p_group);
|
|
return true;
|
|
|
|
default:
|
|
if(code <= STL_TF_TELETEXT_LAST)
|
|
{
|
|
TextBufferFlush(p_group, p_buffer, &i_buffer, psz_charset);
|
|
GroupParseTeletext(p_group, code);
|
|
}
|
|
else if((code >= STL_TF_CHARCODE1_FIRST && code <= STL_TF_CHARCODE1_LAST) ||
|
|
code >= STL_TF_CHARCODE2_FIRST)
|
|
{
|
|
p_buffer[i_buffer++] = code;
|
|
}
|
|
else if(code >= STL_TF_ITALICS_ON && code <= STL_TF_BOXING_OFF)
|
|
{
|
|
TextBufferFlush(p_group, p_buffer, &i_buffer, psz_charset);
|
|
GroupApplyStyle(p_group, code);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
TextBufferFlush(p_group, p_buffer, &i_buffer, psz_charset);
|
|
|
|
return false;
|
|
}
|
|
|
|
static void FillSubpictureUpdater(stl_sg_t *p_group, subtext_updater_sys_t *p_spu_sys)
|
|
{
|
|
if(p_group->i_accumulating)
|
|
{
|
|
p_spu_sys->region.p_segments = text_segment_Copy(p_group->p_segment);
|
|
}
|
|
else
|
|
{
|
|
p_spu_sys->region.p_segments = p_group->p_segment;
|
|
p_group->p_segment = NULL;
|
|
p_group->pp_segment_last = &p_group->p_segment;
|
|
}
|
|
|
|
p_spu_sys->region.b_absolute = false;
|
|
p_spu_sys->region.b_in_window = false;
|
|
p_spu_sys->region.align = SUBPICTURE_ALIGN_BOTTOM;
|
|
if(p_group->i_justify == 0x01)
|
|
p_spu_sys->region.inner_align = SUBPICTURE_ALIGN_LEFT;
|
|
else if(p_group->i_justify == 0x03)
|
|
p_spu_sys->region.inner_align = SUBPICTURE_ALIGN_RIGHT;
|
|
}
|
|
|
|
static void ResetGroups(decoder_sys_t *p_sys)
|
|
{
|
|
for(size_t i=0; i<=STL_GROUPS_MAX; i++)
|
|
{
|
|
stl_sg_t *p_group = &p_sys->groups[i];
|
|
if(p_group->p_segment)
|
|
{
|
|
text_segment_ChainDelete(p_group->p_segment);
|
|
p_group->p_segment = NULL;
|
|
p_group->pp_segment_last = &p_group->p_segment;
|
|
}
|
|
|
|
if(p_group->p_style)
|
|
{
|
|
text_style_Delete(p_group->p_style);
|
|
p_group->p_style = NULL;
|
|
}
|
|
|
|
p_group->i_accumulating = false;
|
|
p_group->i_end = VLC_TICK_INVALID;
|
|
p_group->i_start = VLC_TICK_INVALID;
|
|
p_group->i_justify = 0;
|
|
}
|
|
}
|
|
|
|
static int Decode(decoder_t *p_dec, block_t *p_block)
|
|
{
|
|
if (p_block == NULL) /* No Drain */
|
|
return VLCDEC_SUCCESS;
|
|
|
|
decoder_sys_t *p_sys = p_dec->p_sys;
|
|
|
|
if(p_block->i_buffer < STL_TTI_SIZE)
|
|
p_block->i_flags |= BLOCK_FLAG_CORRUPTED;
|
|
|
|
if(p_block->i_flags & (BLOCK_FLAG_CORRUPTED|BLOCK_FLAG_DISCONTINUITY))
|
|
{
|
|
ResetGroups(p_dec->p_sys);
|
|
|
|
if(p_block->i_flags & BLOCK_FLAG_CORRUPTED)
|
|
{
|
|
block_Release(p_block);
|
|
return VLCDEC_SUCCESS;
|
|
}
|
|
}
|
|
|
|
const char *psz_charset = cct_nums[p_sys->cct - CCT_BEGIN].str;
|
|
for (size_t i = 0; i < p_block->i_buffer / STL_TTI_SIZE; i++)
|
|
{
|
|
stl_sg_t *p_group = &p_sys->groups[p_block->p_buffer[0]];
|
|
if(ParseTTI(p_group, &p_block->p_buffer[i * STL_TTI_SIZE], psz_charset, p_sys->i_fps) &&
|
|
p_group->p_segment != NULL )
|
|
{
|
|
/* output */
|
|
subpicture_t *p_sub = decoder_NewSubpictureText(p_dec);
|
|
if( p_sub )
|
|
{
|
|
FillSubpictureUpdater(p_group, p_sub->updater.sys );
|
|
|
|
if(p_group->i_end != VLC_TICK_INVALID && p_group->i_start >= p_block->i_dts)
|
|
{
|
|
p_sub->i_start = VLC_TICK_0 + p_group->i_start;
|
|
p_sub->i_stop = VLC_TICK_0 + p_group->i_end;
|
|
}
|
|
else
|
|
{
|
|
p_sub->i_start = p_block->i_pts;
|
|
p_sub->i_stop = p_block->i_pts + p_block->i_length;
|
|
p_sub->b_ephemer = (p_block->i_length == 0);
|
|
}
|
|
decoder_QueueSub(p_dec, p_sub);
|
|
}
|
|
}
|
|
}
|
|
|
|
ResetGroups(p_sys);
|
|
|
|
block_Release(p_block);
|
|
return VLCDEC_SUCCESS;
|
|
}
|
|
|
|
static int ParseGSI(decoder_t *dec, decoder_sys_t *p_sys)
|
|
{
|
|
uint8_t *header = dec->fmt_in->p_extra;
|
|
if (!header) {
|
|
msg_Err(dec, "NULL EBU header (GSI block)\n");
|
|
return VLC_EGENERIC;
|
|
}
|
|
|
|
if (GSI_BLOCK_SIZE != dec->fmt_in->i_extra) {
|
|
msg_Err(dec, "EBU header is not in expected size (%zu)\n", dec->fmt_in->i_extra);
|
|
return VLC_EGENERIC;
|
|
}
|
|
|
|
char dfc_fps_str[] = { header[6], header[7], '\0' };
|
|
int fps = strtol(dfc_fps_str, NULL, 10);
|
|
if (1 > fps || 60 < fps) {
|
|
msg_Warn(dec, "EBU header contains unsupported DFC fps ('%s'); falling back to 25\n", dfc_fps_str);
|
|
fps = 25;
|
|
}
|
|
|
|
int cct = (header[12] << 8) | header[13];
|
|
if (CCT_BEGIN > cct || CCT_END < cct) {
|
|
msg_Err(dec, "EBU header contains illegal CCT (0x%x)\n", cct);
|
|
return VLC_EGENERIC;
|
|
}
|
|
|
|
msg_Dbg(dec, "DFC fps=%d, CCT=0x%x", fps, cct);
|
|
p_sys->i_fps = fps;
|
|
p_sys->cct = cct;
|
|
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
static int Open(vlc_object_t *object)
|
|
{
|
|
decoder_t *dec = (decoder_t*)object;
|
|
|
|
if (dec->fmt_in->i_codec != VLC_CODEC_EBU_STL)
|
|
return VLC_EGENERIC;
|
|
|
|
decoder_sys_t *sys = calloc(1, sizeof(*sys));
|
|
if (!sys)
|
|
return VLC_ENOMEM;
|
|
|
|
int rc = ParseGSI(dec, sys);
|
|
if (VLC_SUCCESS != rc) {
|
|
free(sys);
|
|
return rc;
|
|
}
|
|
|
|
for(size_t i=0; i<=STL_GROUPS_MAX; i++)
|
|
sys->groups[i].pp_segment_last = &sys->groups[i].p_segment;
|
|
|
|
dec->p_sys = sys;
|
|
dec->pf_decode = Decode;
|
|
dec->fmt_out.i_codec = 0;
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
static void Close(vlc_object_t *object)
|
|
{
|
|
decoder_t *dec = (decoder_t*)object;
|
|
decoder_sys_t *p_sys = dec->p_sys;
|
|
|
|
ResetGroups(p_sys);
|
|
free(p_sys);
|
|
}
|
|
|