/* * libzvbi - Triggers * * Based on EACEM TP 14-99-16 "Data Broadcasting", rev 0.8; * ATVEF "Enhanced Content Specification", v1.1 (www.atvef.com); * and http://developer.webtv.net * * Copyright (C) 2001 Michael H. Schimek * * 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 2 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, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* $Id: trigger.c,v 1.10 2006/05/31 03:55:52 mschimek Exp $ */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include "misc.h" #include "trigger.h" #include "tables.h" #include "vbi.h" struct vbi_trigger { vbi_trigger * next; vbi_link link; double fire; unsigned char view; vbi_bool _delete; }; static vbi_bool verify_checksum(const char *s, int count, int checksum) { register unsigned long sum2, sum1 = checksum; for (; count > 1; count -= 2) { sum1 += (unsigned long) *s++ << 8; sum1 += (unsigned long) *s++; } sum2 = sum1; /* * There seems to be confusion about how left-over * bytes shall be added, the example C code in * RFC 1071 subclause 4.1 contradicts the definition * in subclause 1 (zero pad to 16 bit). */ if (count > 0) { sum1 += (unsigned long) *s << 8; /* correct */ sum2 += (unsigned long) *s << 0; /* wrong */ } while (sum1 >= (1 << 16)) sum1 = (sum1 & 0xFFFFUL) + (sum1 >> 16); while (sum2 >= (1 << 16)) sum2 = (sum2 & 0xFFFFUL) + (sum2 >> 16); return sum1 == 0xFFFFUL || sum2 == 0xFFFFUL; } static int parse_dec(const char *s, int digits) { int n = 0; while (digits-- > 0) { if (!isdigit(*s)) return -1; n = n * 10 + *s++ - '0'; } return n; } static int parse_hex(const char *s, int digits) { int n = 0; while (digits-- > 0) { if (!isxdigit(*s)) return -1; n = n * 16 + (*s & 15) + ((*s > '9') ? 9 : 0); s++; } return n; } /* * XXX http://developer.webtv.net/itv/tvlink/main.htm adds more ...??? */ static time_t parse_date(const char *s) { struct tm tm; memset(&tm, 0, sizeof(tm)); if ((tm.tm_year = parse_dec(s + 0, 4)) < 0 || (tm.tm_mon = parse_dec(s + 4, 2)) < 0 || (tm.tm_mday = parse_dec(s + 6, 2)) < 0) return (time_t) -1; if (s[8]) { if (s[8] != 'T' || (tm.tm_hour = parse_dec(s + 9, 2)) < 0 || (tm.tm_min = parse_dec(s + 11, 2)) < 0) return (time_t) -1; if (s[13] && (tm.tm_sec = parse_dec(s + 13, 2)) < 0) return (time_t) -1; } tm.tm_year -= 1900; return mktime(&tm); } static int parse_time(const char *s) { int seconds, frames = 0; seconds = strtoul(s, (char **) &s, 10); if (*s) if (*s != 'F' || (frames = parse_dec(s + 1, 2)) < 0) return -1; return seconds * 25 + frames; } static int parse_bool(char *s) { return (strcmp(s, "1") == 0) || (strcasecmp(s, "true") == 0); } static int keyword(char *s, const char **keywords, int num) { int i; if (!s[0]) return -1; else if (!s[1]) { for (i = 0; i < num; i++) if (tolower(s[0]) == keywords[i][0]) return i; } else for (i = 0; i < num; i++) if (strcasecmp(s, keywords[i]) == 0) return i; return -1; } static char * parse_eacem(vbi_trigger *t, char *s1, unsigned int nuid, double now) { static const char *attributes[] = { "active", "countdown", "delete", "expires", "name", "priority", "script" }; char buf[256]; char *s, *e, *d, *dx; int active, countdown; int c; t->link.url[0] = 0; t->link.name[0] = 0; t->link.script[0] = 0; t->link.priority = 9; t->link.expires = 0.0; t->link.autoload = FALSE; t->_delete = FALSE; t->fire = now; t->view = 'w'; t->link.itv_type = 0; active = INT_MAX; for (s = s1;; s++) { e = s; c = *s; if (c == '<') { if (s != s1) return NULL; d = (char *) t->link.url; dx = d + sizeof(t->link.url) - 2; for (s++; (c = *s) != '>'; s++) if (c && d < dx) *d++ = c; else return NULL; *d++ = 0; } else if (c == '[' || c == '(') { int delim = (c == '[') ? ']' : ')'; char *attr, *text = ""; vbi_bool quote = FALSE; attr = d = buf; dx = d + sizeof(buf) - 2; for (s++; c = *s, c != ':' && c != delim; s++) { if (c == '%') { if ((c = parse_hex(s + 1, 2)) < 0x20) return NULL; s += 2; } if (c && d < dx) *d++ = c; else return NULL; } *d++ = 0; if (!attr[0]) return NULL; s++; if (c != ':') { if (!verify_checksum(s1, e - s1, strtoul(attr, NULL, 16))) { if (0) fprintf(stderr, "checksum mismatch\n"); return NULL; } break; } for (text = d; quote || (c = *s) != delim; s++) { if (c == '"') quote ^= TRUE; else if (c == '%') { if ((c = parse_hex(s + 1, 2)) < 0x20) return NULL; s += 2; } if (c && d < dx) *d++ = c; else return NULL; } *d++ = 0; switch (keyword(attr, attributes, sizeof(attributes) / sizeof(attributes[0]))) { case 0: /* active */ active = parse_time(text); if (active < 0) return NULL; break; case 1: /* countdown */ countdown = parse_time(text); if (countdown < 0) return NULL; t->fire = now + countdown / 25.0; break; case 2: /* delete */ t->_delete = TRUE; break; case 3: /* expires */ t->link.expires = parse_date(text); if (t->link.expires == (time_t) -1) return NULL; break; case 4: /* name */ strncpy((char *) t->link.name, text, sizeof(t->link.name) - 1); t->link.name[sizeof(t->link.name) - 1] = 0; break; case 5: /* priority */ t->link.priority = strtoul(text, NULL, 10); if (t->link.priority > 9) return NULL; break; case 6: /* script */ strncpy((char *) t->link.script, text, sizeof(t->link.script) - 1); t->link.script[sizeof(t->link.script) - 1] = 0; break; default: /* ignored */ break; } } else if (c == 0) break; else return NULL; } if (t->link.expires <= 0.0) t->link.expires = t->fire + active / 25.0; /* EACEM eqv PAL/SECAM land, 25 fps */ if (!t->link.url) return NULL; if (strncmp((char *) t->link.url, "http://", 7) == 0) t->link.type = VBI_LINK_HTTP; else if (strncmp((char *) t->link.url, "lid://", 6) == 0) t->link.type = VBI_LINK_LID; else if (strncmp((char *) t->link.url, "tw://", 5) == 0) t->link.type = VBI_LINK_TELEWEB; else if (strncmp((char *) t->link.url, "dummy", 5) == 0) { t->link.pgno = parse_dec((char *) t->link.url + 5, 2); if (!t->link.name || t->link.pgno < 0 || t->link.url[7]) return NULL; t->link.type = VBI_LINK_MESSAGE; } else if (strncmp((char *) t->link.url, "ttx://", 6) == 0) { const struct vbi_cni_entry *p; int cni; cni = parse_hex((char *) t->link.url + 6, 4); if (cni < 0 || t->link.url[10] != '/') return NULL; t->link.pgno = parse_hex((char *) t->link.url + 11, 3); if (t->link.pgno < 0x100 || t->link.url[14] != '/') return NULL; t->link.subno = parse_hex((char *) t->link.url + 15, 4); if (t->link.subno < 0) return NULL; if (cni > 0) { for (p = vbi_cni_table; p->name; p++) if (p->cni1 == cni || p->cni4 == cni) break; if (!p->name) return NULL; t->link.nuid = p->id; } else t->link.nuid = nuid; t->link.type = VBI_LINK_PAGE; } else return NULL; return s; } static char * parse_atvef(vbi_trigger *t, char *s1, double now) { static const char *attributes[] = { "auto", "expires", "name", "script", "type" /* "t" */, "time", "tve", "tve-level", "view" /* "v" */ }; static const char *type_attrs[] = { "program", "network", "station", "sponsor", "operator", "tve" }; char buf[256]; char *s, *e, *d, *dx; int c; t->link.url[0] = 0; t->link.name[0] = 0; t->link.script[0] = 0; t->link.priority = 9; t->fire = now; t->link.expires = 0.0; t->link.autoload = FALSE; t->_delete = FALSE; t->view = 'w'; t->link.itv_type = 0; for (s = s1;; s++) { e = s; c = *s; if (c == '<') { if (s != s1) return NULL; d = (char *) t->link.url; dx = (char *) d + sizeof(t->link.url) - 1; for (s++; (c = *s) != '>'; s++) if (c && d < dx) *d++ = c; else return NULL; *d++ = 0; } else if (c == '[') { char *attr, *text = ""; vbi_bool quote = FALSE; attr = d = buf; dx = d + sizeof(buf) - 2; for (s++; c = *s, c != ':' && c != ']'; s++) { if (c == '%') { if ((c = parse_hex(s + 1, 2)) < 0x20) return NULL; s += 2; } if (c && d < dx) *d++ = c; else return NULL; } *d++ = 0; if (!attr[0]) return NULL; s++; if (c != ':') { unsigned int i; for (i = 1; i < (sizeof(type_attrs) / sizeof(type_attrs[0]) - 1); i++) if (strcasecmp(type_attrs[i], attr) == 0) break; if (i < (sizeof(type_attrs) / sizeof(type_attrs[0]) - 1)) { t->link.itv_type = i + 1; continue; } if (!verify_checksum(s1, e - s1, strtoul(attr, NULL, 16))) return NULL; break; } for (text = d; quote || (c = *s) != ']'; s++) { if (c == '"') quote ^= TRUE; else if (c == '%') { if ((c = parse_hex(s + 1, 2)) < 0x20) return NULL; s += 2; } if (c && d < dx) *d++ = c; else return NULL; } *d++ = 0; switch (keyword(attr, attributes, sizeof(attributes) / sizeof(attributes[0]))) { case 0: /* auto */ t->link.autoload = parse_bool(text); break; case 1: /* expires */ t->link.expires = parse_date(text); if (t->link.expires < 0.0) return NULL; break; case 2: /* name */ strncpy((char *) t->link.name, text, sizeof(t->link.name) - 1); t->link.name[sizeof(t->link.name) - 1] = 0; break; case 3: /* script */ strncpy((char *) t->link.script, text, sizeof(t->link.script)); t->link.script[sizeof(t->link.script) - 1] = 0; break; case 4: /* type */ t->link.itv_type = keyword(text, type_attrs, sizeof(type_attrs) / sizeof(type_attrs[0])) + 1; break; case 5: /* time */ t->fire = parse_date(text); if (t->fire < 0.0) return NULL; break; case 6: /* tve */ case 7: /* tve-level */ /* ignored */ break; case 8: /* view (tve == v) */ t->view = *text; break; default: /* ignored */ break; } } else if (c == 0) break; else return NULL; } if (!t->link.url) return NULL; if (strncmp((char *) t->link.url, "http://", 7) == 0) t->link.type = VBI_LINK_HTTP; else if (strncmp((char *) t->link.url, "lid://", 6) == 0) t->link.type = VBI_LINK_LID; else return NULL; return s; } /** * vbi_trigger_flush: * @param vbi Initialized vbi decoding context. * * Discard all triggers stored to fire at a later time. This function * must be called before deleting the @vbi context. **/ void vbi_trigger_flush(vbi_decoder *vbi) { vbi_trigger *t; while ((t = vbi->triggers)) { vbi->triggers = t->next; free(t); } } /** * vbi_deferred_trigger: * @param vbi Initialized vbi decoding context. * * This function must be called at regular intervals, * preferably once per video frame, to fire (send a trigger * event) previously received triggers which reached their * fire time. 'Now' is supposed to be @vbi->time. **/ void vbi_deferred_trigger(vbi_decoder *vbi) { vbi_trigger *t, **tp; for (tp = &vbi->triggers; (t = *tp); tp = &t->next) if (t->fire <= vbi->time) { vbi_event ev; ev.type = VBI_EVENT_TRIGGER; ev.ev.trigger = &t->link; vbi_send_event(vbi, &ev); *tp = t->next; free(t); } else tp = &t->next; } static void add_trigger(vbi_decoder *vbi, vbi_trigger *a) { vbi_trigger *t; if (a->_delete) { vbi_trigger **tp; for (tp = &vbi->triggers; (t = *tp); tp = &t->next) if (strcmp((char *) a->link.url, (char *) t->link.url) == 0 && fabs(a->fire - t->fire) < 0.1) { *tp = t->next; free(t); } else tp = &t->next; return; } for (t = vbi->triggers; t; t = t->next) if (strcmp((char *) a->link.url, (char *) t->link.url) == 0 && fabs(a->fire - t->fire) < 0.1) return; if (a->fire <= vbi->time) { vbi_event ev; ev.type = VBI_EVENT_TRIGGER; ev.ev.trigger = &a->link; vbi_send_event(vbi, &ev); return; } if (!(t = malloc(sizeof(*t)))) return; t->next = vbi->triggers; vbi->triggers = t; } /** * vbi_atvef_trigger: * @param vbi Initialized vbi decoding context. * @param s EACEM string (supposedly ASCII). * * Parse an EACEM string and add it to the trigger list (where it * may fire immediately or at a later time). **/ void vbi_eacem_trigger(vbi_decoder *vbi, unsigned char *s) { vbi_trigger t; char *r; r = (char *) s; while ((r = parse_eacem(&t, r, vbi->network.ev.network.nuid, vbi->time))) { if (0) fprintf(stderr, "At %f eacem link type %d '%s', <%s> '%s', " "%08x %03x.%04x, exp %f, pri %d %d, auto %d; " "fire %f view %d del %d\n", vbi->time, t.link.type, t.link.name, t.link.url, t.link.script, t.link.nuid, t.link.pgno, t.link.subno, t.link.expires, t.link.priority, t.link.itv_type, t.link.autoload, t.fire, t.view, t._delete); t.link.eacem = TRUE; if (t.link.type == VBI_LINK_LID || t.link.type == VBI_LINK_TELEWEB) return; add_trigger(vbi, &t); } } /** * vbi_atvef_trigger: * @param vbi Initialized vbi context. * @param s ATVEF string (ASCII). * * Parse an ATVEF string and add it to the trigger list (where it * may fire immediately or at a later time). **/ void vbi_atvef_trigger(vbi_decoder *vbi, unsigned char *s) { vbi_trigger t; if (parse_atvef(&t, (char *) s, vbi->time)) { if (0) fprintf(stderr, "At %f atvef link type %d '%s', <%s> '%s', " "%08x %03x.%04x, exp %f, pri %d %d, auto %d; " "fire %f view %d del %d\n", vbi->time, t.link.type, t.link.name, t.link.url, t.link.script, t.link.nuid, t.link.pgno, t.link.subno, t.link.expires, t.link.priority, t.link.itv_type, t.link.autoload, t.fire, t.view, t._delete); t.link.eacem = FALSE; if (t.view == 't' /* WebTV */ || strchr((char *) t.link.url, '*') /* trigger matching */ || t.link.type == VBI_LINK_LID) return; add_trigger(vbi, &t); } }