tvheadend/src/teletext.c

562 lines
14 KiB
C

/*
* Teletext parsing functions
* Copyright (C) 2007 Andreas Öman
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include "tvheadend.h"
#include "teletext.h"
#include "packet.h"
#include "streaming.h"
#include "service.h"
/**
*
*/
typedef struct tt_mag {
int ttm_curpage;
int64_t ttm_current_pts;
uint8_t ttm_lang;
uint8_t ttm_page[23*40 + 1];
} tt_mag_t;
/**
*
*/
typedef struct tt_private {
tt_mag_t ttp_mags[8];
int ttp_rundown_valid;
uint8_t ttp_rundown[23*40 + 1];
} tt_private_t;
static void teletext_rundown_copy(tt_private_t *ttp, tt_mag_t *ttm);
static void teletext_rundown_scan(service_t *t, tt_private_t *ttp);
#define bitreverse(b) \
(((b) * 0x0202020202ULL & 0x010884422010ULL) % 1023)
static const uint8_t hamtable[] = {
0x01, 0xff, 0x81, 0x01, 0xff, 0x00, 0x01, 0xff,
0xff, 0x02, 0x01, 0xff, 0x0a, 0xff, 0xff, 0x07,
0xff, 0x00, 0x01, 0xff, 0x00, 0x80, 0xff, 0x00,
0x06, 0xff, 0xff, 0x0b, 0xff, 0x00, 0x03, 0xff,
0xff, 0x0c, 0x01, 0xff, 0x04, 0xff, 0xff, 0x07,
0x06, 0xff, 0xff, 0x07, 0xff, 0x07, 0x07, 0x87,
0x06, 0xff, 0xff, 0x05, 0xff, 0x00, 0x0d, 0xff,
0x86, 0x06, 0x06, 0xff, 0x06, 0xff, 0xff, 0x07,
0xff, 0x02, 0x01, 0xff, 0x04, 0xff, 0xff, 0x09,
0x02, 0x82, 0xff, 0x02, 0xff, 0x02, 0x03, 0xff,
0x08, 0xff, 0xff, 0x05, 0xff, 0x00, 0x03, 0xff,
0xff, 0x02, 0x03, 0xff, 0x03, 0xff, 0x83, 0x03,
0x04, 0xff, 0xff, 0x05, 0x84, 0x04, 0x04, 0xff,
0xff, 0x02, 0x0f, 0xff, 0x04, 0xff, 0xff, 0x07,
0xff, 0x05, 0x05, 0x85, 0x04, 0xff, 0xff, 0x05,
0x06, 0xff, 0xff, 0x05, 0xff, 0x0e, 0x03, 0xff,
0xff, 0x0c, 0x01, 0xff, 0x0a, 0xff, 0xff, 0x09,
0x0a, 0xff, 0xff, 0x0b, 0x8a, 0x0a, 0x0a, 0xff,
0x08, 0xff, 0xff, 0x0b, 0xff, 0x00, 0x0d, 0xff,
0xff, 0x0b, 0x0b, 0x8b, 0x0a, 0xff, 0xff, 0x0b,
0x0c, 0x8c, 0xff, 0x0c, 0xff, 0x0c, 0x0d, 0xff,
0xff, 0x0c, 0x0f, 0xff, 0x0a, 0xff, 0xff, 0x07,
0xff, 0x0c, 0x0d, 0xff, 0x0d, 0xff, 0x8d, 0x0d,
0x06, 0xff, 0xff, 0x0b, 0xff, 0x0e, 0x0d, 0xff,
0x08, 0xff, 0xff, 0x09, 0xff, 0x09, 0x09, 0x89,
0xff, 0x02, 0x0f, 0xff, 0x0a, 0xff, 0xff, 0x09,
0x88, 0x08, 0x08, 0xff, 0x08, 0xff, 0xff, 0x09,
0x08, 0xff, 0xff, 0x0b, 0xff, 0x0e, 0x03, 0xff,
0xff, 0x0c, 0x0f, 0xff, 0x04, 0xff, 0xff, 0x09,
0x0f, 0xff, 0x8f, 0x0f, 0xff, 0x0e, 0x0f, 0xff,
0x08, 0xff, 0xff, 0x05, 0xff, 0x0e, 0x0d, 0xff,
0xff, 0x0e, 0x0f, 0xff, 0x0e, 0x8e, 0xff, 0x0e,
};
static const uint8_t laG0_nat_opts_lookup[16][8] = {
{1, 4, 11, 5, 3, 8, 0, 1},
{7, 4, 11, 5, 3, 1, 0, 1},
{1, 4, 11, 5, 3, 8, 12, 1},
{1, 1, 1, 1, 1, 10, 1, 9},
{1, 4, 2, 6, 1, 1, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1}, // 5 - reserved
{1, 1, 1, 1, 1, 1, 12, 1},
{1, 1, 1, 1, 1, 1, 1, 1}, // 7 - reserved
{1, 1, 1, 1, 3, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1}, // 9 - reserved
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1}, // 11 - reserved
{1, 1, 1, 1, 1, 1, 1, 1}, // 12 - reserved
{1, 1, 1, 1, 1, 1, 1, 1}, // 13 - reserved
{1, 1, 1, 1, 1, 1, 1, 1}, // 14 - reserved
{1, 1, 1, 1, 1, 1, 1, 1} // 15 - reserved
};
static const uint8_t laG0_nat_replace_map[128] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 5, 6, 7, 8,
9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 0
};
/*
* Latin National Option Sub-Sets
* ETSI EN 300 706 Table 36
*/
static const uint16_t laG0_nat_opts16[13][14] = {
{0, '#', 'u', 'c', 't', 'z', 0xc3bd, 0xc3ad, 'r', 0xc3a9, 0xc3a1, 'e', 0xc3ba, 's' }, // 0 - Czech/Slovak
{0, 0xc2a3, '$', '@', '-', 0xc2bd, '-', '|', '#', '-', 0xc2bc, '#', 0xc2be, 0xc3b7}, // 1 - English
{0, '#', 0xc3b5, 'S', 0xc384, 0xc396, 'Z', 0xc39c, 0xc395, 's', 0xc3a4, 0xc3b6, 'z', 0xc3bc}, // 2 - Estonian
{0, 0xc3a9, 0xc3af, 0xc3a0, 0xc3ab, 0xc3aa, 0xc3b9, 0xc3ae, '#', 0xc3a8, 0xc3a2, 0xc3b4, 0xc3bb, 0xc3a7}, // 3 - French
{0, '#', '$', 0xc2a7, 0xc384, 0xc396, 0xc39c, '^', '_', 0xc2ba, 0xc3a4, 0xc3b6, 0xc3bc, 0xc39f}, // 4 - German
{0, 0xc2a3, '$', 0xc3a9, 0xc2ba, 0xc3a7, '-', '|', '#', 0xc3b9, 0xc3a0, 0xc3b2, 0xc3a8, 0xc3ac}, // 5 - Italian
{0, '#', '$', 'S', 'e', 'e', 'Z', 'c', 'u', 's', 'a', 'u', 'z', 'i' }, // 6 - Lettish/Lithuanian
{0, '#', 'n', 'a', 'Z', 'S', 'L', 'c', 0xc3b3, 'e', 'z', 's', 'l', 'z' }, // 7 - Polish
{0, 0xc3a7, '$', 'i', 0xc3a1, 0xc3a9, 0xc3ad, 0xc3b3, 0xc3ba, 0xc2bf, 0xc3bc, 0xc3b1, 0xc3a8, 0xc3a0}, // 8 - Portuguese/Spanish
{0, '#', 0xc2a4, 'T', 0xc382, 'S', 'A', 0xc38e, 'i', 't', 0xc3a2, 's', 'a', 0xc3ae}, // 9 - Rumanian
{0, '#', 0xc38b, 'C', 'C', 'Z', 'D', 'S', 0xc3ab, 'c', 'c', 'z', 'd', 's' }, // 10 - Serbian/Croation/Slovenian
{0, '#', 0xc2a4, 0xc389, 0xc384, 0xc396, 0xc385, 0xc39c, '_', 0xc3a9, 0xc3a4, 0xc3b6, 0xc3a5, 0xc3bc}, // 11 - Swedish/Finnish/Hungarian
{0, 'T', 'g', 'I', 'S', 0xc396, 0xc387, 0xc39c, 'G', 'i', 's', 0xc3b6, 0xc3a7, 0xc3bc} // 12 - Turkish
};
/*
* Map Latin G0 teletext characters into a ISO-8859-1 approximation.
* Trying to use similar looking or similar meaning characters.
* Gtriplet - 4 bits = triplet-1 bits 14-11 (14-8) of a X/28 or M/29 packet, if unknown - use 0
* natopts - 3 bits = national_options field of a Y/0 packet (or triplet-1 bits 10-8 as above?)
* inchar - 7 bits = characted to remap
* Also strips parity
*/
static uint16_t
tt_convert_char(int Gtriplet, int natopts, uint8_t inchar)
{
int no = laG0_nat_opts_lookup[Gtriplet & 0xf][natopts & 0x7];
uint8_t c = inchar & 0x7f;
if(!laG0_nat_replace_map[c])
return c;
else
return laG0_nat_opts16[no][laG0_nat_replace_map[c]];
}
static uint8_t
ham_decode(uint8_t a, uint8_t b)
{
a = hamtable[a];
b = hamtable[b];
return (b << 4) | (a & 0xf);
}
/**
*
*/
static time_t
tt_construct_unix_time(uint8_t *buf)
{
time_t t, r[3], v[3];
int i;
struct tm tm;
t = dispatch_clock;
localtime_r(&t, &tm);
tm.tm_hour = atoi((char *)buf);
tm.tm_min = atoi((char *)buf + 3);
tm.tm_sec = atoi((char *)buf + 6);
r[0] = mktime(&tm);
tm.tm_mday--;
r[1] = mktime(&tm);
tm.tm_mday += 2;
r[2] = mktime(&tm);
for(i = 0; i < 3; i++)
v[i] = labs(r[i] - t);
if(v[0] < v[1] && v[0] < v[2])
return r[0];
if(v[1] < v[2] && v[1] < v[0])
return r[1];
return r[2];
}
/**
*
*/
static int
is_tt_clock(const uint8_t *str)
{
return
isdigit(str[0]) && isdigit(str[1]) && str[2] == ':' &&
isdigit(str[3]) && isdigit(str[4]) && str[5] == ':' &&
isdigit(str[6]) && isdigit(str[7]);
}
/**
*
*/
static int
update_tt_clock(service_t *t, const uint8_t *buf)
{
uint8_t str[10];
int i;
time_t ti;
for(i = 0; i < 8; i++)
str[i] = buf[i] & 0x7f;
str[8] = 0;
if(!is_tt_clock(str))
return 0;
ti = tt_construct_unix_time(str);
if(t->s_tt_clock == ti)
return 0;
t->s_tt_clock = ti;
// printf("teletext clock is: %s", ctime(&ti));
return 1;
}
static void
extract_subtitle(service_t *t, elementary_stream_t *st,
tt_mag_t *ttm, int64_t pts)
{
int i, j, off = 0;
uint8_t sub[2000];
int is_box = 0;
for (i = 0; i < 23; i++) {
int start = off;
for (j = 0; j < 40; j++) {
char ch = ttm->ttm_page[40 * i + j];
switch(ch) {
case 0 ... 7:
break;
case 0x0a:
is_box = 0;
break;
case 0x0b:
is_box = 1;
break;
default:
if (ch >= 0x20 && is_box && (start != off || ch > 0x20)) {
uint16_t code = tt_convert_char(0, ttm->ttm_lang, ch);
if(code & 0xff00)
sub[off++] = (code & 0xff00) >> 8;
sub[off++] = code;
}
}
}
if(start != off)
sub[off++] = '\n';
}
if(off == 0 && st->es_blank)
return; // Avoid multiple blank subtitles
st->es_blank = !off;
if(st->es_curpts == pts)
pts++; // Avoid non-monotonic PTS
st->es_curpts = pts;
sub[off++] = 0;
th_pkt_t *pkt = pkt_alloc(sub, off, pts, pts);
pkt->pkt_componentindex = st->es_index;
streaming_message_t *sm = streaming_msg_create_pkt(pkt);
streaming_pad_deliver(&t->s_streaming_pad, sm);
streaming_msg_free(sm);
/* Decrease our own reference to the packet */
pkt_ref_dec(pkt);
}
/**
*
*/
#if 0
static void
dump_page(tt_mag_t *ttm)
{
int i, j, v;
char buf[41];
printf("------------------------------------------------\n");
printf("------------------------------------------------\n");
for(i = 0; i < 23; i++) {
for(j = 0; j < 40; j++) {
v = ttm->ttm_page[40 * i + j];
v &= 0x7f;
if(v < 32)
v = ' ';
buf[j] = v;
}
buf[j] = 0;
printf("%s | %x %x %x\n", buf,
ttm->ttm_page[40 * i + 0],
ttm->ttm_page[40 * i + 1],
ttm->ttm_page[40 * i + 2]);
}
}
#endif
static void
tt_subtitle_deliver(service_t *t, elementary_stream_t *parent, tt_mag_t *ttm)
{
elementary_stream_t *st;
if(ttm->ttm_current_pts == PTS_UNSET)
return;
TAILQ_FOREACH(st, &t->s_components, es_link) {
if(parent->es_pid == st->es_parent_pid &&
ttm->ttm_curpage == st->es_pid - PID_TELETEXT_BASE) {
extract_subtitle(t, st, ttm, ttm->ttm_current_pts);
}
}
}
/**
*
*/
static void
tt_decode_line(service_t *t, elementary_stream_t *st, uint8_t *buf)
{
uint8_t mpag, line, s12, c;
int page, magidx, i;
tt_mag_t *ttm;
tt_private_t *ttp;
if(st->es_priv == NULL) {
/* Allocate privdata for reassembly */
ttp = st->es_priv = calloc(1, sizeof(tt_private_t));
} else {
ttp = st->es_priv;
}
mpag = ham_decode(buf[0], buf[1]);
magidx = mpag & 7;
ttm = &ttp->ttp_mags[magidx];
line = mpag >> 3;
switch(line) {
case 0:
if(ttm->ttm_curpage != 0) {
tt_subtitle_deliver(t, st, ttm);
if(ttm->ttm_curpage == 192)
teletext_rundown_copy(ttp, ttm);
memset(ttm->ttm_page, ' ', 23 * 40);
ttm->ttm_curpage = 0;
}
if((page = ham_decode(buf[2], buf[3])) == 0xff)
return;
/* The page is BDC encoded, mag 0 is displayed as page 800+ */
page = (magidx ?: 8) * 100 + (page >> 4) * 10 + (page & 0xf);
ttm->ttm_curpage = page;
s12 = ham_decode(buf[4], buf[5]);
c = ham_decode(buf[8], buf[9]);
ttm->ttm_lang = c >> 5;
if(s12 & 0x80) {
/* Erase page */
memset(ttm->ttm_page, ' ', 23 * 40);
}
if(update_tt_clock(t, buf + 34))
teletext_rundown_scan(t, ttp);
ttm->ttm_current_pts = t->s_current_pts;
break;
case 1 ... 23:
for(i = 0; i < 40; i++) {
c = buf[i + 2] & 0x7f;
ttm->ttm_page[i + 40 * (line - 1)] = c;
}
break;
default:
break;
}
}
/**
*
*/
void
teletext_input(service_t *t, elementary_stream_t *st, const uint8_t *tsb)
{
int i, j;
const uint8_t *x;
uint8_t buf[42];
x = tsb + 4;
for(i = 0; i < 4; i++) {
if(*x == 2 || *x == 3) {
for(j = 0; j < 42; j++)
buf[j] = bitreverse(x[4 + j]);
tt_decode_line(t, st, buf);
}
x += 46;
}
}
/**
* Swedish TV4 rundown dump (page 192)
*
Starttid Titel L{ngd | 3 3 53
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | 14 2c 2c
19:00:00 TV4Nyheterna 00:14:00 | d 7 31
| 20 20 20
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | 14 2c 2c
19:14:00 Lokala nyheter 00:03:00 | 1 1 31
19:17:00 TV4Nyheterna 19.00 2 00:02:02 | 7 7 31
19:19:02 Vinjett 00:00:03 | 6 6 31
19:19:05 Reklam 00:02:30 | 3 3 31
19:21:35 Vinjett 00:00:06 | 6 6 31
19:21:41 LOKAL BILLBOARD 00:00:16 | 1 1 31
19:21:57 Lokalt v{der 00:01:00 | 1 1 31
19:22:57 S4NVMT1553 00:00:15 | 6 6 31
19:23:12 V{der 00:02:00 | 7 7 31
19:25:12 Trailer 00:01:00 | 2 2 31
19:26:12 Vinjett 00:00:03 | 6 6 31
19:26:15 Reklam 00:02:20 | 3 3 31
19:28:35 Vinjett 00:00:07 | 6 6 31
19:28:42 Hall} 16:9 00:00:30 | 7 7 31
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | 14 2c 2c
| 20 20 20
TV4 | 54 56 34
| 20 20 20
*/
static int
tt_time_to_len(const uint8_t *buf)
{
int l;
char str[10];
memcpy(str, buf, 8);
str[2] = 0;
str[5] = 0;
str[8] = 0;
l = atoi(str + 0) * 3600 + atoi(str + 3) * 60 + atoi(str + 6);
return l;
}
/*
* Decode the Swedish TV4 teletext rundown page to figure out if we are
* currently in a commercial break
*/
static void
teletext_rundown_copy(tt_private_t *ttp, tt_mag_t *ttm)
{
/* Sanity check */
if(memcmp((char *)ttm->ttm_page + 2, "Starttid", strlen("Starttid")) ||
memcmp((char *)ttm->ttm_page + 11, "Titel", strlen("Titel")) ||
memcmp((char *)ttm->ttm_page + 21 * 40, "TV4", strlen("TV4")))
return;
memcpy(ttp->ttp_rundown, ttm->ttm_page, 23 * 40);
ttp->ttp_rundown_valid = 1;
}
static void
teletext_rundown_scan(service_t *t, tt_private_t *ttp)
{
int i;
uint8_t *l;
time_t now = t->s_tt_clock, start, stop, last = 0;
th_commercial_advice_t ca;
if(ttp->ttp_rundown_valid == 0)
return;
for(i = 0; i < 23; i++) {
l = ttp->ttp_rundown + 40 * i;
if((l[1] & 0xf0) != 0x00 || !is_tt_clock(l + 32) || !is_tt_clock(l + 2))
continue;
if(!memcmp(l + 11, "Nyhetspuff", strlen("Nyhetspuff")))
ca = COMMERCIAL_YES;
else
ca = (l[1] & 0xf) == 7 ? COMMERCIAL_NO : COMMERCIAL_YES;
start = tt_construct_unix_time(l + 2);
stop = start + tt_time_to_len(l + 32);
if(start <= now && stop > now)
t->s_tt_commercial_advice = ca;
if(start > now && ca != t->s_tt_commercial_advice && last == 0)
last = start;
}
}