From 47b97e52a87726ed39c55ea1622aa876253ae32b Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Fri, 7 Mar 2014 08:39:00 +0100 Subject: [PATCH] Add esfilter (elementary stream filter) Some feeds (especially from satellite) includes many language mutations and other elementary streams. It may be useful to define the filters and order scheme for the streaming and DVR. The service information dialog is extended to show all PIDs and filtered PIDs to easy show the used PIDs. See the included help file for more description. --- Makefile | 4 +- docs/html/config_esfilter.html | 109 +++ src/api.c | 1 + src/api.h | 2 + src/api/api_esfilter.c | 101 +++ src/api/api_idnode.c | 41 +- src/api/api_service.c | 74 +- src/esfilter.c | 1023 ++++++++++++++++++++++++++ src/esfilter.h | 110 +++ src/idnode.c | 28 +- src/idnode.h | 17 +- src/main.c | 3 + src/service.c | 183 ++++- src/service.h | 9 +- src/subscriptions.c | 4 +- src/tvheadend.h | 3 + src/webui/extjs.c | 1 + src/webui/static/app/esfilter.js | 108 +++ src/webui/static/app/ext.css | 13 + src/webui/static/app/idnode.js | 68 +- src/webui/static/app/mpegts.js | 54 +- src/webui/static/app/tvheadend.js | 11 + src/webui/static/icons/film_edit.png | 1 + 23 files changed, 1888 insertions(+), 80 deletions(-) create mode 100644 docs/html/config_esfilter.html create mode 100644 src/api/api_esfilter.c create mode 100644 src/esfilter.c create mode 100644 src/esfilter.h create mode 100644 src/webui/static/app/esfilter.js create mode 120000 src/webui/static/icons/film_edit.png diff --git a/Makefile b/Makefile index 6ec18b03..609a5a0b 100644 --- a/Makefile +++ b/Makefile @@ -117,6 +117,7 @@ SRCS = src/version.c \ src/rtsp.c \ src/fsmonitor.c \ src/cron.c \ + src/esfilter.c SRCS-${CONFIG_UPNP} += \ src/upnp.c @@ -131,7 +132,8 @@ SRCS += \ src/api/api_mpegts.c \ src/api/api_epg.c \ src/api/api_epggrab.c \ - src/api/api_imagecache.c + src/api/api_imagecache.c \ + src/api/api_esfilter.c SRCS += \ src/parsers/parsers.c \ diff --git a/docs/html/config_esfilter.html b/docs/html/config_esfilter.html new file mode 100644 index 00000000..75a03de4 --- /dev/null +++ b/docs/html/config_esfilter.html @@ -0,0 +1,109 @@ +
+ +This table defines rules to filter and order the elementary streams +like video or audio from the input feed. + +

+The execution order of commands is granted. It means that first rule +is executed for all available streams then second and so on. + +

+If any elementary stream is not marked as ignored or exclusive, it is +used. If you like to ignore unknown elementary streams, add a rule +to the end of grid with the any (not defined) comparisons and +with the action ignore. + +

+The rules for different elementary stream groups (video, audio, +teletext, subtitle, CA, other) are executed separately (as visually edited). + +

+For the visual verification of the filtering, there is a service info +dialog in the Configuration / DVB Inputs / Services window . This dialog +shows the received PIDs and filtered PIDs in one window. + +

+The rules are listed / edited in a grid. + +

+ +

+The columns have the following functions: + +

+
Enabled +
If selected, the rule will be enabled. + +
Stream Type +
Select the elementary stream type to compare. Empty field means any. + +
Language +
Select the language to compare. Empty field means any. + +
Service +
The service to compare. Empty field means any. + +
CA Identification +
The CAID to compare. Empty field means any. + +
CA Provider +
The CA provider to compare. Empty field means any. + +
PID +
Program identification (PID) number to compare. Zero means any. + This comparison is processed only when service comparison is active. + +
Action +
The rule action defines the operation when all comparisons succeeds. + +
+ +
NONE +
No action, may be used for the logging and a comparison verification. + +
USE +
Use this elementary stream. + +
ONCE +
Use this elementary stream only once per selected language. + The first successfully compared rule wins. + +
EXCLUSIVE +
Use only this elementary stream. No other elementary streams + will be used. + +
EMPTY +
Add this elementary stream only when no elementary streams are + used from previous rules. + +
IGNORE +
Ignore this elementary stream. This stream is not used. Another + successfully compared rule with different action may override it. + +
+ +
Log +
Write a short message to log identifying the matched parameters. + It is useful for debugging your setup or structure of incoming + streams. + +
+
diff --git a/src/api.c b/src/api.c index 97546982..83392622 100644 --- a/src/api.c +++ b/src/api.c @@ -125,6 +125,7 @@ void api_init ( void ) api_epggrab_init(); api_status_init(); api_imagecache_init(); + api_esfilter_init(); } void api_done ( void ) diff --git a/src/api.h b/src/api.h index 6a275732..f51f7ebe 100644 --- a/src/api.h +++ b/src/api.h @@ -67,12 +67,14 @@ void api_epg_init ( void ); void api_epggrab_init ( void ); void api_status_init ( void ); void api_imagecache_init ( void ); +void api_esfilter_init ( void ); /* * IDnode */ typedef struct api_idnode_grid_conf { + int tindex; int start; int limit; idnode_filter_t filter; diff --git a/src/api/api_esfilter.c b/src/api/api_esfilter.c new file mode 100644 index 00000000..2769f8c3 --- /dev/null +++ b/src/api/api_esfilter.c @@ -0,0 +1,101 @@ +/* + * API - elementary stream filter related calls + * + * Copyright (C) 2014 Jaroslav Kysela + * + * 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 . + */ + +#include "tvheadend.h" +#include "esfilter.h" +#include "lang_codes.h" +#include "access.h" +#include "api.h" + +static void +api_esfilter_grid + ( idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args, + esfilter_class_t cls ) +{ + esfilter_t *esf; + + TAILQ_FOREACH(esf, &esfilters[cls], esf_link) { + idnode_set_add(ins, (idnode_t*)esf, &conf->filter); + } +} + +static int +api_esfilter_create + ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp, + esfilter_class_t cls ) +{ + htsmsg_t *conf; + + if (!(conf = htsmsg_get_map(args, "conf"))) + return EINVAL; + + pthread_mutex_lock(&global_lock); + esfilter_create(cls, NULL, conf, 1); + pthread_mutex_unlock(&global_lock); + + return 0; +} + +#define ESFILTER(func, t) \ +static void api_esfilter_grid_##func \ + ( idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) \ +{ return api_esfilter_grid(ins, conf, args, (t)); } \ +static int api_esfilter_create_##func \ + ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) \ +{ return api_esfilter_create(opaque, op, args, resp, (t)); } + +ESFILTER(video, ESF_CLASS_VIDEO); +ESFILTER(audio, ESF_CLASS_AUDIO); +ESFILTER(teletext, ESF_CLASS_TELETEXT); +ESFILTER(subtit, ESF_CLASS_SUBTIT); +ESFILTER(ca, ESF_CLASS_CA); +ESFILTER(other, ESF_CLASS_OTHER); + +void api_esfilter_init ( void ) +{ + static api_hook_t ah[] = { + { "esfilter/video/class", ACCESS_ANONYMOUS, api_idnode_class, (void*)&esfilter_class_video }, + { "esfilter/video/grid", ACCESS_ANONYMOUS, api_idnode_grid, api_esfilter_grid_video }, + { "esfilter/video/create", ACCESS_ADMIN, api_esfilter_create_video, NULL }, + + { "esfilter/audio/class", ACCESS_ANONYMOUS, api_idnode_class, (void*)&esfilter_class_audio }, + { "esfilter/audio/grid", ACCESS_ANONYMOUS, api_idnode_grid, api_esfilter_grid_audio }, + { "esfilter/audio/create", ACCESS_ADMIN, api_esfilter_create_audio, NULL }, + + { "esfilter/teletext/class", ACCESS_ANONYMOUS, api_idnode_class, (void*)&esfilter_class_teletext }, + { "esfilter/teletext/grid", ACCESS_ANONYMOUS, api_idnode_grid, api_esfilter_grid_teletext }, + { "esfilter/teletext/create",ACCESS_ADMIN, api_esfilter_create_teletext, NULL }, + + { "esfilter/subtit/class", ACCESS_ANONYMOUS, api_idnode_class, (void*)&esfilter_class_subtit }, + { "esfilter/subtit/grid", ACCESS_ANONYMOUS, api_idnode_grid, api_esfilter_grid_subtit }, + { "esfilter/subtit/create", ACCESS_ADMIN, api_esfilter_create_subtit, NULL }, + + { "esfilter/ca/class", ACCESS_ANONYMOUS, api_idnode_class, (void*)&esfilter_class_ca }, + { "esfilter/ca/grid", ACCESS_ANONYMOUS, api_idnode_grid, api_esfilter_grid_ca }, + { "esfilter/ca/create", ACCESS_ADMIN, api_esfilter_create_ca, NULL }, + + { "esfilter/other/class", ACCESS_ANONYMOUS, api_idnode_class, (void*)&esfilter_class_other }, + { "esfilter/other/grid", ACCESS_ANONYMOUS, api_idnode_grid, api_esfilter_grid_other }, + { "esfilter/other/create", ACCESS_ADMIN, api_esfilter_create_other, NULL }, + + { NULL }, + }; + + api_register_all(ah); +} diff --git a/src/api/api_idnode.c b/src/api/api_idnode.c index 3e951ea1..689a51b5 100644 --- a/src/api/api_idnode.c +++ b/src/api/api_idnode.c @@ -377,8 +377,8 @@ exit: } static int -api_idnode_delete - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +api_idnode_handler + ( htsmsg_t *args, htsmsg_t **resp, void (*handler)(idnode_t *in) ) { int err = 0; idnode_t *in; @@ -400,7 +400,7 @@ api_idnode_delete HTSMSG_FOREACH(f, uuids) { if (!(uuid = htsmsg_field_get_string(f))) continue; if (!(in = idnode_find(uuid, NULL))) continue; - idnode_delete(in); + handler(in); } /* Single */ @@ -409,7 +409,7 @@ api_idnode_delete if (!(in = idnode_find(uuid, NULL))) err = ENOENT; else - idnode_delete(in); + handler(in); } pthread_mutex_unlock(&global_lock); @@ -417,14 +417,37 @@ api_idnode_delete return err; } +static int +api_idnode_delete + ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + return api_idnode_handler(args, resp, idnode_delete); +} + +static int +api_idnode_moveup + ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + return api_idnode_handler(args, resp, idnode_moveup); +} + +static int +api_idnode_movedown + ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + return api_idnode_handler(args, resp, idnode_movedown); +} + void api_idnode_init ( void ) { static api_hook_t ah[] = { - { "idnode/load", ACCESS_ANONYMOUS, api_idnode_load, NULL }, - { "idnode/save", ACCESS_ADMIN, api_idnode_save, NULL }, - { "idnode/tree", ACCESS_ANONYMOUS, api_idnode_tree, NULL }, - { "idnode/class", ACCESS_ANONYMOUS, api_idnode_class, NULL }, - { "idnode/delete", ACCESS_ADMIN, api_idnode_delete, NULL }, + { "idnode/load", ACCESS_ANONYMOUS, api_idnode_load, NULL }, + { "idnode/save", ACCESS_ADMIN, api_idnode_save, NULL }, + { "idnode/tree", ACCESS_ANONYMOUS, api_idnode_tree, NULL }, + { "idnode/class", ACCESS_ANONYMOUS, api_idnode_class, NULL }, + { "idnode/delete", ACCESS_ADMIN, api_idnode_delete, NULL }, + { "idnode/moveup", ACCESS_ADMIN, api_idnode_moveup, NULL }, + { "idnode/movedown", ACCESS_ADMIN, api_idnode_movedown, NULL }, { NULL }, }; diff --git a/src/api/api_service.c b/src/api/api_service.c index 211d12ed..e8b847e2 100644 --- a/src/api/api_service.c +++ b/src/api/api_service.c @@ -92,12 +92,45 @@ api_service_mapper_notify ( void ) notify_by_msg("servicemapper", api_mapper_status_msg()); } +static htsmsg_t * +api_service_streams_get_one ( elementary_stream_t *es ) +{ + htsmsg_t *e = htsmsg_create_map(); + htsmsg_add_u32(e, "index", es->es_index); + htsmsg_add_u32(e, "pid", es->es_pid); + htsmsg_add_str(e, "type", streaming_component_type2txt(es->es_type)); + htsmsg_add_str(e, "language", es->es_lang); + if (SCT_ISSUBTITLE(es->es_type)) { + htsmsg_add_u32(e, "composition_id", es->es_composition_id); + htsmsg_add_u32(e, "ancillary_id", es->es_ancillary_id); + } else if (SCT_ISAUDIO(es->es_type)) { + htsmsg_add_u32(e, "audio_type", es->es_audio_type); + } else if (SCT_ISVIDEO(es->es_type)) { + htsmsg_add_u32(e, "width", es->es_width); + htsmsg_add_u32(e, "height", es->es_height); + htsmsg_add_u32(e, "duration", es->es_frame_duration); + htsmsg_add_u32(e, "aspect_num", es->es_aspect_num); + htsmsg_add_u32(e, "aspect_den", es->es_aspect_den); + } else if (es->es_type == SCT_CA) { + caid_t *ca; + htsmsg_t *e2, *l2 = htsmsg_create_list(); + LIST_FOREACH(ca, &es->es_caids, link) { + e2 = htsmsg_create_map(); + htsmsg_add_u32(e2, "caid", ca->caid); + htsmsg_add_u32(e2, "provider", ca->providerid); + htsmsg_add_msg(l2, NULL, e2); + } + htsmsg_add_msg(e, "caids", l2); + } + return e; +} + static int api_service_streams ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { const char *uuid; - htsmsg_t *e, *st; + htsmsg_t *e, *st, *stf; service_t *s; elementary_stream_t *es; @@ -116,6 +149,7 @@ api_service_streams /* Build response */ pthread_mutex_lock(&s->s_stream_mutex); st = htsmsg_create_list(); + stf = htsmsg_create_list(); if (s->s_pcr_pid) { e = htsmsg_create_map(); htsmsg_add_u32(e, "pid", s->s_pcr_pid); @@ -128,39 +162,17 @@ api_service_streams htsmsg_add_str(e, "type", "PMT"); htsmsg_add_msg(st, NULL, e); } - TAILQ_FOREACH(es, &s->s_components, es_link) { - htsmsg_t *e = htsmsg_create_map(); - htsmsg_add_u32(e, "index", es->es_index); - htsmsg_add_u32(e, "pid", es->es_pid); - htsmsg_add_str(e, "type", streaming_component_type2txt(es->es_type)); - htsmsg_add_str(e, "language", es->es_lang); - if (SCT_ISSUBTITLE(es->es_type)) { - htsmsg_add_u32(e, "composition_id", es->es_composition_id); - htsmsg_add_u32(e, "ancillary_id", es->es_ancillary_id); - } else if (SCT_ISAUDIO(es->es_type)) { - htsmsg_add_u32(e, "audio_type", es->es_audio_type); - } else if (SCT_ISVIDEO(es->es_type)) { - htsmsg_add_u32(e, "width", es->es_width); - htsmsg_add_u32(e, "height", es->es_height); - htsmsg_add_u32(e, "duration", es->es_frame_duration); - htsmsg_add_u32(e, "aspect_num", es->es_aspect_num); - htsmsg_add_u32(e, "aspect_den", es->es_aspect_den); - } else if (es->es_type == SCT_CA) { - caid_t *ca; - htsmsg_t *e2, *l2 = htsmsg_create_list(); - LIST_FOREACH(ca, &es->es_caids, link) { - e2 = htsmsg_create_map(); - htsmsg_add_u32(e2, "caid", ca->caid); - htsmsg_add_u32(e2, "provider", ca->providerid); - htsmsg_add_msg(l2, NULL, e2); - } - htsmsg_add_msg(e, "caids", l2); - } - htsmsg_add_msg(st, NULL, e); - } + TAILQ_FOREACH(es, &s->s_components, es_link) + htsmsg_add_msg(st, NULL, api_service_streams_get_one(es)); + if (TAILQ_FIRST(&s->s_filt_components) == NULL || + s->s_status == SERVICE_IDLE) + service_build_filter(s); + TAILQ_FOREACH(es, &s->s_filt_components, es_filt_link) + htsmsg_add_msg(stf, NULL, api_service_streams_get_one(es)); *resp = htsmsg_create_map(); htsmsg_add_str(*resp, "name", s->s_nicename); htsmsg_add_msg(*resp, "streams", st); + htsmsg_add_msg(*resp, "fstreams", stf); pthread_mutex_unlock(&s->s_stream_mutex); /* Done */ diff --git a/src/esfilter.c b/src/esfilter.c new file mode 100644 index 00000000..d5de70a6 --- /dev/null +++ b/src/esfilter.c @@ -0,0 +1,1023 @@ +/* + * tvheadend, Elementary Stream Filter + * Copyright (C) 2014 Jaroslav Kysela + * + * 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 . + */ + +#include "tvheadend.h" +#include "settings.h" +#include "lang_codes.h" +#include "service.h" +#include "esfilter.h" + +struct esfilter_entry_queue esfilters[ESF_CLASS_LAST + 1]; + +static void esfilter_class_save(idnode_t *self); + +/* + * Class masks + */ +uint32_t esfilterclsmask[ESF_CLASS_LAST+1] = { + 0, + ESF_MASK_VIDEO, + ESF_MASK_AUDIO, + ESF_MASK_TELETEXT, + ESF_MASK_SUBTIT, + ESF_MASK_CA, + ESF_MASK_OTHER +}; + +static const idclass_t *esfilter_classes[ESF_CLASS_LAST+1] = { + NULL, + &esfilter_class_video, + &esfilter_class_audio, + &esfilter_class_teletext, + &esfilter_class_subtit, + &esfilter_class_ca, + &esfilter_class_other +}; + +/* + * Class types + */ + +static struct strtab esfilterclasstab[] = { + { "NONE", ESF_CLASS_NONE }, + { "VIDEO", ESF_CLASS_VIDEO }, + { "AUDIO", ESF_CLASS_AUDIO }, + { "TELETEXT", ESF_CLASS_TELETEXT }, + { "SUBTIT", ESF_CLASS_SUBTIT }, + { "CA", ESF_CLASS_CA }, + { "OTHER", ESF_CLASS_OTHER }, +}; + +const char * +esfilter_class2txt(int cls) +{ + return val2str(cls, esfilterclasstab) ?: "INVALID"; +} + +#if 0 +static int +esfilter_txt2class(const char *s) +{ + return s ? str2val(s, esfilterclasstab) : ESF_CLASS_NONE; +} +#endif + +/* + * Action types + */ + +static struct strtab esfilteractiontab[] = { + { "NONE", ESFA_NONE }, + { "USE", ESFA_USE }, + { "ONCE", ESFA_ONCE }, + { "EXCLUSIVE", ESFA_EXCLUSIVE }, + { "EMPTY", ESFA_EMPTY }, + { "IGNORE", ESFA_IGNORE } +}; + +const char * +esfilter_action2txt(esfilter_action_t a) +{ + return val2str(a, esfilteractiontab) ?: "INVALID"; +} + +#if 0 +static esfilter_action_t +esfilter_txt2action(const char *s) +{ + return s ? str2val(s, esfilteractiontab) : ESFA_NONE; +} +#endif + +/* + * Create / delete + */ + +static void +esfilter_reindex(esfilter_class_t cls) +{ + esfilter_t *esf; + int i = 1; + + TAILQ_FOREACH(esf, &esfilters[cls], esf_link) + esf->esf_save = 0; + TAILQ_FOREACH(esf, &esfilters[cls], esf_link) { + if (esf->esf_index != i) { + esf->esf_index = i; + esf->esf_save = 1; + } + i++; + } + TAILQ_FOREACH(esf, &esfilters[cls], esf_link) + if (esf->esf_save) { + esf->esf_save = 0; + esfilter_class_save((idnode_t *)esf); + } +} + +static int +esfilter_cmp(esfilter_t *a, esfilter_t *b) +{ + return a->esf_index - b->esf_index; +} + +esfilter_t * +esfilter_create + (esfilter_class_t cls, const char *uuid, htsmsg_t *conf, int save) +{ + esfilter_t *esf = calloc(1, sizeof(*esf)); + const idclass_t *c = NULL; + uint32_t ct; + + esf->esf_caid = -1; + esf->esf_caprovider = -1; + if (ESF_CLASS_IS_VALID(cls)) { + c = esfilter_classes[cls]; + } else { + if (!htsmsg_get_u32(conf, "class", &ct)) { + cls = ct; + if (ESF_CLASS_IS_VALID(cls)) + c = esfilter_classes[cls]; + } + } + if (!c) { + tvherror("esfilter", "wrong class %d!", cls); + abort(); + } + lock_assert(&global_lock); + idnode_insert(&esf->esf_id, uuid, c); + if (conf) + idnode_load(&esf->esf_id, conf); + if (ESF_CLASS_IS_VALID(cls)) + esf->esf_class = cls; + else if (!ESF_CLASS_IS_VALID(esf->esf_class)) { + tvherror("esfilter", "wrong class %d!", esf->esf_class); + abort(); + } + if (esf->esf_index) { + TAILQ_INSERT_SORTED(&esfilters[esf->esf_class], esf, esf_link, esfilter_cmp); + } else { + TAILQ_INSERT_TAIL(&esfilters[esf->esf_class], esf, esf_link); + esfilter_reindex(esf->esf_class); + } + if (save) + esfilter_class_save((idnode_t *)esf); + return esf; +} + +static void +esfilter_delete(esfilter_t *esf, int delconf) +{ + if (delconf) + hts_settings_remove("esfilter/%s", idnode_uuid_as_str(&esf->esf_id)); + TAILQ_REMOVE(&esfilters[esf->esf_class], esf, esf_link); + idnode_unlink(&esf->esf_id); + free(esf->esf_comment); + free(esf); +} + +/* + * Class functions + */ + +static void +esfilter_class_save(idnode_t *self) +{ + htsmsg_t *c = htsmsg_create_map(); + idnode_save(self, c); + hts_settings_save(c, "esfilter/%s", idnode_uuid_as_str(self)); + htsmsg_destroy(c); +} + +static const char * +esfilter_class_get_title(idnode_t *self) +{ + esfilter_t *esf = (esfilter_t *)self; + return idnode_uuid_as_str(&esf->esf_id); +} + +static void +esfilter_class_delete(idnode_t *self) +{ + esfilter_t *esf = (esfilter_t *)self; + esfilter_delete(esf, 1); +} + +static void +esfilter_class_moveup(idnode_t *self) +{ + esfilter_t *esf = (esfilter_t *)self; + esfilter_t *prev = TAILQ_PREV(esf, esfilter_entry_queue, esf_link); + if (prev) { + TAILQ_REMOVE(&esfilters[esf->esf_class], esf, esf_link); + TAILQ_INSERT_BEFORE(prev, esf, esf_link); + esfilter_reindex(esf->esf_class); + } +} + +static void +esfilter_class_movedown(idnode_t *self) +{ + esfilter_t *esf = (esfilter_t *)self; + esfilter_t *next = TAILQ_NEXT(esf, esf_link); + if (next) { + TAILQ_REMOVE(&esfilters[esf->esf_class], esf, esf_link); + TAILQ_INSERT_AFTER(&esfilters[esf->esf_class], next, esf, esf_link); + esfilter_reindex(esf->esf_class); + } +} + +static const void * +esfilter_class_type_get(void *o) +{ + esfilter_t *esf = o; + htsmsg_t *l = htsmsg_create_list(); + int i; + + for (i = SCT_UNKNOWN; i <= SCT_LAST; i++) + if ((esf->esf_type & SCT_MASK(i)) != 0) + htsmsg_add_u32(l, NULL, i); + return l; +} + +static char * +esfilter_class_type_rend (void *o) +{ + char *str; + htsmsg_t *l = htsmsg_create_list(); + esfilter_t *esf = o; + int i; + + for (i = SCT_UNKNOWN; i <= SCT_LAST; i++) { + if (SCT_MASK(i) & esf->esf_type) + htsmsg_add_str(l, NULL, streaming_component_type2txt(i)); + } + + str = htsmsg_list_2_csv(l); + htsmsg_destroy(l); + return str; +} + +static int +esfilter_class_type_set_(void *o, const void *v, esfilter_class_t cls) +{ + esfilter_t *esf = o; + htsmsg_t *types = (htsmsg_t*)v; + htsmsg_field_t *f; + uint32_t mask = 0, u32; + uint32_t vmask = esfilterclsmask[cls]; + int save; + + HTSMSG_FOREACH(f, types) { + if (!htsmsg_field_get_u32(f, &u32)) { + if (SCT_MASK(u32) & vmask) + mask |= SCT_MASK(u32); + } else { + return 0; + } + } + save = esf->esf_type != mask; + esf->esf_type = mask; + return save; +} + +static htsmsg_t * +esfilter_class_type_enum_(void *o, esfilter_class_t cls) +{ + uint32_t mask = esfilterclsmask[cls]; + htsmsg_t *l = htsmsg_create_list(); + int i; + + for (i = SCT_UNKNOWN; i <= SCT_LAST; i++) { + if (mask & SCT_MASK(i)) { + htsmsg_t *e = htsmsg_create_map(); + htsmsg_add_u32(e, "key", i); + htsmsg_add_str(e, "val", + i == SCT_UNKNOWN ? "ANY" : streaming_component_type2txt(i)); + htsmsg_add_msg(l, NULL, e); + } + } + return l; +} + +#define ESFILTER_CLS(func, type) \ +static int esfilter_class_type_set_##func(void *o, const void *v) \ + { return esfilter_class_type_set_(o, v, type); } \ +static htsmsg_t * esfilter_class_type_enum_##func(void *o) \ + { return esfilter_class_type_enum_(o, type); } + +ESFILTER_CLS(video, ESF_CLASS_VIDEO); +ESFILTER_CLS(audio, ESF_CLASS_AUDIO); +ESFILTER_CLS(teletext, ESF_CLASS_TELETEXT); +ESFILTER_CLS(subtit, ESF_CLASS_SUBTIT); +ESFILTER_CLS(ca, ESF_CLASS_CA); +ESFILTER_CLS(other, ESF_CLASS_OTHER); + +static const void * +esfilter_class_language_get(void *o) +{ + static __thread char *ret; + esfilter_t *esf = o; + ret = esf->esf_language; + return &ret; +} + +static int +esfilter_class_language_set(void *o, const void *v) +{ + esfilter_t *esf = o; + const char *s = v; + char n[4]; + int save; + strncpy(n, s && s[0] ? lang_code_get(s) : "", 4); + n[3] = 0; + save = strcmp(esf->esf_language, n); + strcpy(esf->esf_language, n); + return save; +} + +static htsmsg_t * +esfilter_class_language_enum(void *o) +{ + htsmsg_t *l = htsmsg_create_list(); + const lang_code_t *lc = lang_codes; + char buf[128]; + + while (lc->code2b) { + htsmsg_t *e = htsmsg_create_map(); + if (!strcmp(lc->code2b, "und")) { + htsmsg_add_str(e, "key", ""); + htsmsg_add_str(e, "val", "ANY"); + } else { + htsmsg_add_str(e, "key", lc->code2b); + snprintf(buf, sizeof(buf), "%s (%s)", lc->desc, lc->code2b); + buf[sizeof(buf)-1] = '\0'; + htsmsg_add_str(e, "val", buf); + } + htsmsg_add_msg(l, NULL, e); + lc++; + } + return l; +} + +static const void * +esfilter_class_service_get(void *o) +{ + static __thread char *ret; + esfilter_t *esf = o; + ret = esf->esf_service; + return &ret; +} + +static int +esfilter_class_service_set(void *o, const void *v) +{ + esfilter_t *esf = o; + const char *s = v; + int save = 0; + if (strncmp(esf->esf_service, s, UUID_HEX_SIZE)) { + strncpy(esf->esf_service, s, UUID_HEX_SIZE); + esf->esf_service[UUID_HEX_SIZE-1] = '\0'; + save = 1; + } + return save; +} + +static htsmsg_t * +esfilter_class_service_enum(void *o) +{ + htsmsg_t *e, *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "service/list"); + htsmsg_add_str(m, "event", "service"); + e = htsmsg_create_map(); + htsmsg_add_bool(e, "enum", 1); + htsmsg_add_msg(m, "params", e); + return m; +} + +#define MAX_ITEMS 256 + +static int +esfilter_build_ca_cmp(const void *_a, const void *_b) +{ + uint32_t a = *(uint32_t *)_a; + uint32_t b = *(uint32_t *)_b; + if (a < b) + return -1; + if (a > b) + return 1; + return 0; +} + +static htsmsg_t * +esfilter_build_ca_enum(int provider) +{ + htsmsg_t *e, *l; + uint32_t *a = alloca(sizeof(uint32_t) * MAX_ITEMS); + char buf[16], buf2[128]; + service_t *s; + elementary_stream_t *es; + caid_t *ca; + uint32_t v; + int i, count = 0; + + lock_assert(&global_lock); + TAILQ_FOREACH(s, &service_all, s_all_link) { + pthread_mutex_lock(&s->s_stream_mutex); + TAILQ_FOREACH(es, &s->s_components, es_link) { + LIST_FOREACH(ca, &es->es_caids, link) { + v = provider ? ca->providerid : ca->caid; + for (i = 0; i < count; i++) + if (a[i] == v) + break; + if (i >= count) + a[count++] = v; + } + } + pthread_mutex_unlock(&s->s_stream_mutex); + } + qsort(a, count, sizeof(uint32_t), esfilter_build_ca_cmp); + + l = htsmsg_create_list(); + + e = htsmsg_create_map(); + htsmsg_add_str(e, "key", provider ? "ffffff" : "ffff"); + htsmsg_add_str(e, "val", "ANY"); + htsmsg_add_msg(l, NULL, e); + + for (i = 0; i < count; i++) { + e = htsmsg_create_map(); + snprintf(buf, sizeof(buf), provider ? "%06x" : "%04x", a[i]); + if (!provider) + snprintf(buf2, sizeof(buf2), provider ? "%06x %s" : "%04x - %s", + a[i], descrambler_caid2name(a[i])); + htsmsg_add_str(e, "key", buf); + htsmsg_add_str(e, "val", provider ? buf : buf2); + htsmsg_add_msg(l, NULL, e); + } + return l; + +} + +static const void * +esfilter_class_caid_get(void *o) +{ + static __thread char *ret; + static __thread char buf[16]; + esfilter_t *esf = o; + snprintf(buf, sizeof(buf), "%04x", esf->esf_caid); + ret = buf; + return &ret; +} + +static int +esfilter_class_caid_set(void *o, const void *v) +{ + esfilter_t *esf = o; + uint16_t u; + int save = 0; + u = strtol(v, NULL, 16); + if (u != esf->esf_caid) { + esf->esf_caid = u; + save = 1; + } + return save; +} + +static htsmsg_t * +esfilter_class_caid_enum(void *o) +{ + return esfilter_build_ca_enum(0); +} + +static const void * +esfilter_class_caprovider_get(void *o) +{ + static __thread char *ret; + static __thread char buf[16]; + esfilter_t *esf = o; + if (esf->esf_caprovider == -1) + strcpy(buf, "ffffff"); + else + snprintf(buf, sizeof(buf), "%06x", esf->esf_caprovider); + ret = buf; + return &ret; +} + +static int +esfilter_class_caprovider_set(void *o, const void *v) +{ + esfilter_t *esf = o; + uint32_t u; + int save = 0; + if (strcmp(v, "ffffff") == 0) + u = -1; + else + u = strtol(v, NULL, 16); + if (u != esf->esf_caprovider) { + esf->esf_caprovider = u; + save = 1; + } + return save; +} + +static htsmsg_t * +esfilter_class_caprovider_enum(void *o) +{ + return esfilter_build_ca_enum(1); +} + +static const void * +esfilter_class_action_get(void *o) +{ + esfilter_t *esf = o; + return &esf->esf_action; +} + +static int +esfilter_class_action_set(void *o, const void *v) +{ + esfilter_t *esf = o; + int n = *(int *)v; + int save = 0; + if (n >= ESFA_USE && n <= ESFA_LAST) { + save = esf->esf_action != n; + esf->esf_action = n; + } + return save; +} + +static htsmsg_t * +esfilter_class_action_enum(void *o) +{ + htsmsg_t *l = htsmsg_create_list(); + int i; + + for (i = ESFA_NONE; i <= ESFA_LAST; i++) { + htsmsg_t *e = htsmsg_create_map(); + htsmsg_add_u32(e, "key", i); + htsmsg_add_str(e, "val", esfilter_action2txt(i)); + htsmsg_add_msg(l, NULL, e); + } + return l; +} + +const idclass_t esfilter_class = { + .ic_class = "esfilter", + .ic_caption = "Elementary Stream Filter", + .ic_save = esfilter_class_save, + .ic_get_title = esfilter_class_get_title, + .ic_delete = esfilter_class_delete, + .ic_moveup = esfilter_class_moveup, + .ic_movedown = esfilter_class_movedown, + .ic_properties = (const property_t[]){ + { + .type = PT_INT, + .id = "class", + .name = "Class", + .opts = PO_RDONLY | PO_HIDDEN, + .off = offsetof(esfilter_t, esf_class), + }, + { + .type = PT_INT, + .id = "index", + .name = "Index", + .opts = PO_RDONLY | PO_HIDDEN, + .off = offsetof(esfilter_t, esf_index), + }, + { + .type = PT_BOOL, + .id = "enabled", + .name = "Enabled", + .off = offsetof(esfilter_t, esf_enabled), + }, + {} + } +}; + +const idclass_t esfilter_class_video = { + .ic_super = &esfilter_class, + .ic_class = "esfilter_video", + .ic_caption = "Video Stream Filter", + .ic_properties = (const property_t[]){ + { + .type = PT_STR, + .islist = 1, + .id = "type", + .name = "Stream Type", + .get = esfilter_class_type_get, + .set = esfilter_class_type_set_video, + .list = esfilter_class_type_enum_video, + .rend = esfilter_class_type_rend, + }, + { + .type = PT_STR, + .id = "language", + .name = "Language", + .get = esfilter_class_language_get, + .set = esfilter_class_language_set, + .list = esfilter_class_language_enum, + }, + { + .type = PT_STR, + .id = "service", + .name = "Service", + .get = esfilter_class_service_get, + .set = esfilter_class_service_set, + .list = esfilter_class_service_enum, + }, + { + .type = PT_INT, + .id = "pid", + .name = "PID", + .off = offsetof(esfilter_t, esf_pid), + }, + { + .type = PT_INT, + .id = "action", + .name = "Action", + .get = esfilter_class_action_get, + .set = esfilter_class_action_set, + .list = esfilter_class_action_enum, + }, + { + .type = PT_BOOL, + .id = "log", + .name = "Log", + .off = offsetof(esfilter_t, esf_log), + }, + { + .type = PT_STR, + .id = "comment", + .name = "Comment", + .off = offsetof(esfilter_t, esf_comment), + }, + {} + } +}; + +const idclass_t esfilter_class_audio = { + .ic_super = &esfilter_class, + .ic_class = "esfilter_audio", + .ic_caption = "Audio Stream Filter", + .ic_properties = (const property_t[]){ + { + .type = PT_STR, + .islist = 1, + .id = "type", + .name = "Stream Type", + .get = esfilter_class_type_get, + .set = esfilter_class_type_set_audio, + .list = esfilter_class_type_enum_audio, + .rend = esfilter_class_type_rend, + }, + { + .type = PT_STR, + .id = "language", + .name = "Language", + .get = esfilter_class_language_get, + .set = esfilter_class_language_set, + .list = esfilter_class_language_enum, + }, + { + .type = PT_STR, + .id = "service", + .name = "Service", + .get = esfilter_class_service_get, + .set = esfilter_class_service_set, + .list = esfilter_class_service_enum, + }, + { + .type = PT_INT, + .id = "pid", + .name = "PID", + .off = offsetof(esfilter_t, esf_pid), + }, + { + .type = PT_INT, + .id = "action", + .name = "Action", + .get = esfilter_class_action_get, + .set = esfilter_class_action_set, + .list = esfilter_class_action_enum, + }, + { + .type = PT_BOOL, + .id = "log", + .name = "Log", + .off = offsetof(esfilter_t, esf_log), + }, + { + .type = PT_STR, + .id = "comment", + .name = "Comment", + .off = offsetof(esfilter_t, esf_comment), + }, + {} + } +}; + +const idclass_t esfilter_class_teletext = { + .ic_super = &esfilter_class, + .ic_class = "esfilter_teletext", + .ic_caption = "Teletext Stream Filter", + .ic_properties = (const property_t[]){ + { + .type = PT_STR, + .islist = 1, + .id = "type", + .name = "Stream Type", + .get = esfilter_class_type_get, + .set = esfilter_class_type_set_teletext, + .list = esfilter_class_type_enum_teletext, + .rend = esfilter_class_type_rend, + }, + { + .type = PT_STR, + .id = "language", + .name = "Language", + .get = esfilter_class_language_get, + .set = esfilter_class_language_set, + .list = esfilter_class_language_enum, + }, + { + .type = PT_STR, + .id = "service", + .name = "Service", + .get = esfilter_class_service_get, + .set = esfilter_class_service_set, + .list = esfilter_class_service_enum, + }, + { + .type = PT_INT, + .id = "pid", + .name = "PID", + .off = offsetof(esfilter_t, esf_pid), + }, + { + .type = PT_INT, + .id = "action", + .name = "Action", + .get = esfilter_class_action_get, + .set = esfilter_class_action_set, + .list = esfilter_class_action_enum, + }, + { + .type = PT_BOOL, + .id = "log", + .name = "Log", + .off = offsetof(esfilter_t, esf_log), + }, + { + .type = PT_STR, + .id = "comment", + .name = "Comment", + .off = offsetof(esfilter_t, esf_comment), + }, + {} + } +}; + +const idclass_t esfilter_class_subtit = { + .ic_super = &esfilter_class, + .ic_class = "esfilter_subtit", + .ic_caption = "Subtitle Stream Filter", + .ic_properties = (const property_t[]){ + { + .type = PT_STR, + .islist = 1, + .id = "type", + .name = "Stream Type", + .get = esfilter_class_type_get, + .set = esfilter_class_type_set_subtit, + .list = esfilter_class_type_enum_subtit, + .rend = esfilter_class_type_rend, + }, + { + .type = PT_STR, + .id = "language", + .name = "Language", + .get = esfilter_class_language_get, + .set = esfilter_class_language_set, + .list = esfilter_class_language_enum, + }, + { + .type = PT_STR, + .id = "service", + .name = "Service", + .get = esfilter_class_service_get, + .set = esfilter_class_service_set, + .list = esfilter_class_service_enum, + }, + { + .type = PT_INT, + .id = "pid", + .name = "PID", + .off = offsetof(esfilter_t, esf_pid), + }, + { + .type = PT_INT, + .id = "action", + .name = "Action", + .get = esfilter_class_action_get, + .set = esfilter_class_action_set, + .list = esfilter_class_action_enum, + }, + { + .type = PT_BOOL, + .id = "log", + .name = "Log", + .off = offsetof(esfilter_t, esf_log), + }, + { + .type = PT_STR, + .id = "comment", + .name = "Comment", + .off = offsetof(esfilter_t, esf_comment), + }, + {} + } +}; + +const idclass_t esfilter_class_ca = { + .ic_super = &esfilter_class, + .ic_class = "esfilter_ca", + .ic_caption = "CA Stream Filter", + .ic_properties = (const property_t[]){ + { + .type = PT_STR, + .islist = 1, + .id = "type", + .name = "Stream Type", + .get = esfilter_class_type_get, + .set = esfilter_class_type_set_ca, + .list = esfilter_class_type_enum_ca, + .rend = esfilter_class_type_rend, + }, + { + .type = PT_STR, + .id = "CAid", + .name = "CA Identification", + .get = esfilter_class_caid_get, + .set = esfilter_class_caid_set, + .list = esfilter_class_caid_enum, + }, + { + .type = PT_STR, + .id = "CAprovider", + .name = "CA Provider", + .get = esfilter_class_caprovider_get, + .set = esfilter_class_caprovider_set, + .list = esfilter_class_caprovider_enum, + }, + { + .type = PT_STR, + .id = "service", + .name = "Service", + .get = esfilter_class_service_get, + .set = esfilter_class_service_set, + .list = esfilter_class_service_enum, + }, + { + .type = PT_INT, + .id = "pid", + .name = "PID", + .off = offsetof(esfilter_t, esf_pid), + }, + { + .type = PT_INT, + .id = "action", + .name = "Action", + .get = esfilter_class_action_get, + .set = esfilter_class_action_set, + .list = esfilter_class_action_enum, + }, + { + .type = PT_BOOL, + .id = "log", + .name = "Log", + .off = offsetof(esfilter_t, esf_log), + }, + { + .type = PT_STR, + .id = "comment", + .name = "Comment", + .off = offsetof(esfilter_t, esf_comment), + }, + {} + } +}; + +const idclass_t esfilter_class_other = { + .ic_super = &esfilter_class, + .ic_class = "esfilter_other", + .ic_caption = "Other Stream Filter", + .ic_properties = (const property_t[]){ + { + .type = PT_STR, + .islist = 1, + .id = "type", + .name = "Stream Type", + .get = esfilter_class_type_get, + .set = esfilter_class_type_set_other, + .list = esfilter_class_type_enum_other, + .rend = esfilter_class_type_rend, + }, + { + .type = PT_STR, + .id = "language", + .name = "Language", + .get = esfilter_class_language_get, + .set = esfilter_class_language_set, + .list = esfilter_class_language_enum, + }, + { + .type = PT_STR, + .id = "service", + .name = "Service", + .get = esfilter_class_service_get, + .set = esfilter_class_service_set, + .list = esfilter_class_service_enum, + }, + { + .type = PT_INT, + .id = "pid", + .name = "PID", + .off = offsetof(esfilter_t, esf_pid), + }, + { + .type = PT_INT, + .id = "action", + .name = "Action", + .get = esfilter_class_action_get, + .set = esfilter_class_action_set, + .list = esfilter_class_action_enum, + }, + { + .type = PT_BOOL, + .id = "log", + .name = "Log", + .off = offsetof(esfilter_t, esf_log), + }, + { + .type = PT_STR, + .id = "comment", + .name = "Comment", + .off = offsetof(esfilter_t, esf_comment), + }, + {} + } +}; + +/** + * Initialize + */ +void +esfilter_init(void) +{ + htsmsg_t *c, *e; + htsmsg_field_t *f; + int i; + + for (i = 0; i <= ESF_CLASS_LAST; i++) + TAILQ_INIT(&esfilters[i]); + + if (!(c = hts_settings_load_r(1, "esfilter"))) + return; + HTSMSG_FOREACH(f, c) { + if (!(e = htsmsg_field_get_map(f))) + continue; + esfilter_create(-1, f->hmf_name, e, 0); + } + htsmsg_destroy(c); +} + +void +esfilter_done(void) +{ + esfilter_t *esf; + int i; + + pthread_mutex_lock(&global_lock); + for (i = 0; i <= ESF_CLASS_LAST; i++) { + while ((esf = TAILQ_FIRST(&esfilters[i])) != NULL) + esfilter_delete(esf, 0); + } + pthread_mutex_unlock(&global_lock); +} diff --git a/src/esfilter.h b/src/esfilter.h new file mode 100644 index 00000000..e65ae9c6 --- /dev/null +++ b/src/esfilter.h @@ -0,0 +1,110 @@ +/* + * tvheadend, Elementary Stream Filter + * Copyright (C) 2014 Jaroslav Kysela + * + * 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 . + */ + +#ifndef __TVH_ESFILTER_H__ +#define __TVH_ESFILTER_H__ + +#include "tvheadend.h" +#include "idnode.h" + +typedef enum { + ESF_CLASS_NONE = 0, + ESF_CLASS_VIDEO, + ESF_CLASS_AUDIO, + ESF_CLASS_TELETEXT, + ESF_CLASS_SUBTIT, + ESF_CLASS_CA, + ESF_CLASS_OTHER, + ESF_CLASS_LAST = ESF_CLASS_OTHER +} esfilter_class_t; + +#define ESF_CLASS_IS_VALID(i) \ + ((i) >= ESF_CLASS_VIDEO && (i) <= ESF_CLASS_LAST) + +extern const idclass_t esfilter_class; +extern const idclass_t esfilter_class_video; +extern const idclass_t esfilter_class_audio; +extern const idclass_t esfilter_class_teletext; +extern const idclass_t esfilter_class_subtit; +extern const idclass_t esfilter_class_ca; +extern const idclass_t esfilter_class_other; + +#define ESF_MASK_VIDEO \ + (SCT_MASK(SCT_MPEG2VIDEO) | SCT_MASK(SCT_H264) | SCT_MASK(SCT_VP8)) + +#define ESF_MASK_AUDIO \ + (SCT_MASK(SCT_MPEG2AUDIO) | SCT_MASK(SCT_AC3) | SCT_MASK(SCT_AAC) | \ + SCT_MASK(SCT_EAC3) | SCT_MASK(SCT_MP4A) | SCT_MASK(SCT_VORBIS)) + +#define ESF_MASK_TELETEXT \ + SCT_MASK(SCT_TELETEXT) + +#define ESF_MASK_SUBTIT \ + (SCT_MASK(SCT_DVBSUB) | SCT_MASK(SCT_TEXTSUB)) + +#define ESF_MASK_CA \ + SCT_MASK(SCT_CA) + +#define ESF_MASK_OTHER \ + SCT_MASK(SCT_MPEGTS) + +extern uint32_t esfilterclsmask[]; + +TAILQ_HEAD(esfilter_entry_queue, esfilter); + +extern struct esfilter_entry_queue esfilters[]; + +typedef enum { + ESFA_NONE = 0, + ESFA_USE, /* use this stream */ + ESFA_ONCE, /* use this stream once per language */ + ESFA_EXCLUSIVE, /* use this stream exclusively */ + ESFA_EMPTY, /* use this stream when no streams were added */ + ESFA_IGNORE, + ESFA_LAST = ESFA_IGNORE +} esfilter_action_t; + +typedef struct esfilter { + idnode_t esf_id; + TAILQ_ENTRY(esfilter) esf_link; + + int esf_class; + int esf_save; + int esf_index; + int esf_enabled; + uint32_t esf_type; + char esf_language[4]; + char esf_service[UUID_HEX_SIZE]; + int esf_pid; + uint16_t esf_caid; + uint32_t esf_caprovider; + int esf_action; + int esf_log; + char *esf_comment; +} esfilter_t; + +esfilter_t *esfilter_create + (esfilter_class_t esf_class, const char *uuid, htsmsg_t *conf, int save); + +const char * esfilter_class2txt(int cls); +const char * esfilter_action2txt(esfilter_action_t a); + +void esfilter_init(void); +void esfilter_done(void); + +#endif /* __TVH_ESFILTER_H__ */ diff --git a/src/idnode.c b/src/idnode.c index 13ba418c..192e929a 100644 --- a/src/idnode.c +++ b/src/idnode.c @@ -140,20 +140,40 @@ idnode_unlink(idnode_t *in) /** * */ -void -idnode_delete(idnode_t *in) +static void +idnode_handler(size_t off, idnode_t *in) { + void (**fcn)(idnode_t *); lock_assert(&global_lock); const idclass_t *idc = in->in_class; while (idc) { - if (idc->ic_delete) { - idc->ic_delete(in); + fcn = (void *)idc + off; + if (*fcn) { + (*fcn)(in); break; } idc = idc->ic_super; } } +void +idnode_delete(idnode_t *in) +{ + return idnode_handler(offsetof(idclass_t, ic_delete), in); +} + +void +idnode_moveup(idnode_t *in) +{ + return idnode_handler(offsetof(idclass_t, ic_moveup), in); +} + +void +idnode_movedown(idnode_t *in) +{ + return idnode_handler(offsetof(idclass_t, ic_movedown), in); +} + /* ************************************************************************** * Info * *************************************************************************/ diff --git a/src/idnode.h b/src/idnode.h index 4bab5b9c..e5aaf7db 100644 --- a/src/idnode.h +++ b/src/idnode.h @@ -42,7 +42,8 @@ typedef struct idnode_set /* * Class definition */ -typedef struct idclass { +typedef struct idclass idclass_t; +struct idclass { const struct idclass *ic_super; /// Parent class const char *ic_class; /// Class name const char *ic_caption; /// Class description @@ -51,11 +52,13 @@ typedef struct idclass { const char *ic_event; /// Events to fire on add/delete/title /* Callbacks */ - idnode_set_t *(*ic_get_childs)(idnode_t *self); - const char *(*ic_get_title) (idnode_t *self); - void (*ic_save) (idnode_t *self); - void (*ic_delete) (idnode_t *self); -} idclass_t; + idnode_set_t *(*ic_get_childs) (idnode_t *self); + const char *(*ic_get_title) (idnode_t *self); + void (*ic_save) (idnode_t *self); + void (*ic_delete) (idnode_t *self); + void (*ic_moveup) (idnode_t *self); + void (*ic_movedown) (idnode_t *self); +}; /* * Node definition @@ -120,6 +123,8 @@ const char *idnode_get_title (idnode_t *in); int idnode_is_leaf (idnode_t *in); int idnode_is_instance (idnode_t *in, const idclass_t *idc); void idnode_delete (idnode_t *in); +void idnode_moveup (idnode_t *in); +void idnode_movedown (idnode_t *in); void *idnode_find (const char *uuid, const idclass_t *idc); idnode_set_t *idnode_find_all(const idclass_t *idc); diff --git a/src/main.c b/src/main.c index 8d4add02..3dcd09c9 100644 --- a/src/main.c +++ b/src/main.c @@ -61,6 +61,7 @@ #include "timeshift.h" #include "fsmonitor.h" #include "lang_codes.h" +#include "esfilter.h" #if ENABLE_LIBAV #include "libav.h" #include "plumbing/transcoding.h" @@ -767,6 +768,7 @@ main(int argc, char **argv) imagecache_init(); http_client_init(); + esfilter_init(); service_init(); @@ -877,6 +879,7 @@ main(int argc, char **argv) tvhftrace("main", hts_settings_done); tvhftrace("main", dvb_done); tvhftrace("main", lang_str_done); + tvhftrace("main", esfilter_done); tvhftrace("main", urlparse_done); tvhlog(LOG_NOTICE, "STOP", "Exiting HTS Tvheadend"); diff --git a/src/service.c b/src/service.c index fd765e79..7c34e652 100644 --- a/src/service.c +++ b/src/service.c @@ -44,6 +44,7 @@ #include "lang_codes.h" #include "descrambler.h" #include "input.h" +#include "esfilter.h" static void service_data_timeout(void *aux); static void service_class_save(struct idnode *self); @@ -284,7 +285,7 @@ service_stop(service_t *t) /** * Clean up each stream */ - TAILQ_FOREACH(st, &t->s_components, es_link) + TAILQ_FOREACH(st, &t->s_filt_components, es_link) stream_clean(st); t->s_status = SERVICE_IDLE; @@ -319,6 +320,161 @@ service_remove_subscriber(service_t *t, th_subscription_t *s, } +/** + * + */ +#define ESFM_USED (1<<0) +#define ESFM_IGNORE (1<<1) + +static void +service_build_filter_add(service_t *t, elementary_stream_t *st, + elementary_stream_t **sta, int *p) +{ + /* only once */ + if (st->es_filter & ESFM_USED) + return; + st->es_filter |= ESFM_USED; + TAILQ_INSERT_TAIL(&t->s_filt_components, st, es_filt_link); + sta[*p] = st; + (*p)++; +} + +/** + * + */ +void +service_build_filter(service_t *t) +{ + elementary_stream_t *st, *st2, **sta; + esfilter_t *esf; + caid_t *ca; + int i, n, p, o, exclusive; + uint32_t mask; + + /* rebuild the filtered and ordered components */ + TAILQ_INIT(&t->s_filt_components); + + for (i = ESF_CLASS_VIDEO; i <= ESF_CLASS_LAST; i++) + if (!TAILQ_EMPTY(&esfilters[i])) + goto filter; + + TAILQ_FOREACH(st, &t->s_components, es_link) + TAILQ_INSERT_TAIL(&t->s_filt_components, st, es_filt_link); + return; + +filter: + n = 0; + TAILQ_FOREACH(st, &t->s_components, es_link) { + st->es_filter = 0; + n++; + } + + sta = alloca(sizeof(elementary_stream_t *) * n); + + for (i = ESF_CLASS_VIDEO, p = 0; i <= ESF_CLASS_LAST; i++) { + o = p; + mask = esfilterclsmask[i]; + if (TAILQ_EMPTY(&esfilters[i])) { + TAILQ_FOREACH(st, &t->s_components, es_link) { + if ((mask & SCT_MASK(st->es_type)) != 0) + service_build_filter_add(t, st, sta, &p); + } + continue; + } + exclusive = 0; + TAILQ_FOREACH(esf, &esfilters[i], esf_link) { + if (!esf->esf_enabled) + continue; + TAILQ_FOREACH(st, &t->s_components, es_link) { + if ((mask & SCT_MASK(st->es_type)) == 0) + continue; + if (esf->esf_type && (esf->esf_type & SCT_MASK(st->es_type)) == 0) + continue; + if (esf->esf_language[0] && + strncmp(esf->esf_language, st->es_lang, 4)) + continue; + if (esf->esf_service && esf->esf_service[0]) { + if (strcmp(esf->esf_service, idnode_uuid_as_str(&t->s_id))) + continue; + if (esf->esf_pid && esf->esf_pid != st->es_pid) + continue; + } + if (i == ESF_CLASS_CA && + (esf->esf_caid != -1 || esf->esf_caprovider != -1)) { + LIST_FOREACH(ca, &st->es_caids, link) { + if (esf->esf_caid != -1 && ca->caid != esf->esf_caid) + continue; + if (esf->esf_caprovider != -1 && ca->providerid != esf->esf_caprovider) + continue; + break; + } + if (ca == NULL) + continue; + } + if (esf->esf_log) + tvhlog(LOG_INFO, "service", "esfilter: %s %03d %05d %s %s %s %s", + esfilter_class2txt(i), esf->esf_index, + st->es_pid, streaming_component_type2txt(st->es_type), + lang_code_get(st->es_lang), t->s_nicename, + esfilter_action2txt(esf->esf_action)); + switch (esf->esf_action) { + case ESFA_NONE: + break; + case ESFA_IGNORE: + st->es_filter |= ESFM_IGNORE; + break; + case ESFA_USE: + service_build_filter_add(t, st, sta, &p); + break; + case ESFA_ONCE: + if (esf->esf_language[0] == '\0') { + service_build_filter_add(t, st, sta, &p); + } else { + int present = 0; + TAILQ_FOREACH(st2, &t->s_components, es_link) { + if ((st2->es_filter & ESFM_USED) == 0) + continue; + if (strcmp(st2->es_lang, st->es_lang) == 0) { + present = 1; + break; + } + } + if (!present) + service_build_filter_add(t, st, sta, &p); + } + break; + case ESFA_EXCLUSIVE: + break; + case ESFA_EMPTY: + if (p == o) + service_build_filter_add(t, st, sta, &p); + break; + default: + tvhlog(LOG_DEBUG, "service", "Unknown esfilter action %d", esf->esf_action); + break; + } + if (esf->esf_action == ESFA_EXCLUSIVE) { + /* forget previous work */ + while (p > o) { + p--; + TAILQ_REMOVE(&t->s_filt_components, sta[p], es_filt_link); + } + service_build_filter_add(t, st, sta, &p); + exclusive = 1; + break; + } + } + } + if (!exclusive) { + TAILQ_FOREACH(st, &t->s_components, es_link) { + if ((mask & SCT_MASK(st->es_type)) != 0 && + (st->es_filter & (ESFM_USED|ESFM_IGNORE)) == 0) + service_build_filter_add(t, st, sta, &p); + } + } + } +} + /** * */ @@ -336,6 +492,8 @@ service_start(service_t *t, int instance) t->s_streaming_status = 0; t->s_scrambled_seen = 0; + service_build_filter(t); + if((r = t->s_start_feed(t, instance))) return r; @@ -349,7 +507,7 @@ service_start(service_t *t, int instance) /** * Initialize stream */ - TAILQ_FOREACH(st, &t->s_components, es_link) + TAILQ_FOREACH(st, &t->s_filt_components, es_link) stream_init(st); pthread_mutex_unlock(&t->s_stream_mutex); @@ -501,6 +659,7 @@ service_destroy(service_t *t, int delconf) t->s_status = SERVICE_ZOMBIE; + TAILQ_INIT(&t->s_filt_components); while((st = TAILQ_FIRST(&t->s_components)) != NULL) service_stream_destroy(t, st); @@ -552,6 +711,7 @@ service_create0 t->s_channel_name = service_channel_name; t->s_provider_name = service_provider_name; TAILQ_INIT(&t->s_components); + TAILQ_INIT(&t->s_filt_components); t->s_last_pid = -1; streaming_pad_init(&t->s_streaming_pad); @@ -631,7 +791,7 @@ elementary_stream_t * service_stream_create(service_t *t, int pid, streaming_component_type_t type) { - elementary_stream_t *st; + elementary_stream_t *st, *st2; int i = 0; int idx = 0; lock_assert(&t->s_stream_mutex); @@ -662,8 +822,14 @@ service_stream_create(service_t *t, int pid, if(t->s_flags & S_DEBUG) tvhlog(LOG_DEBUG, "service", "Add stream %s", st->es_nicename); - if(t->s_status == SERVICE_RUNNING) - stream_init(st); + if(t->s_status == SERVICE_RUNNING) { + service_build_filter(t); + TAILQ_FOREACH(st2, &t->s_filt_components, es_filt_link) + if (st2 == st) { + stream_init(st); + break; + } + } return st; } @@ -842,9 +1008,10 @@ service_restart(service_t *t, int had_components) streaming_msg_free(sm); } + service_build_filter(t); descrambler_service_start(t); - if(TAILQ_FIRST(&t->s_components) != NULL) { + if(TAILQ_FIRST(&t->s_filt_components) != NULL) { sm = streaming_msg_create_data(SMT_START, service_build_stream_start(t)); streaming_pad_deliver(&t->s_streaming_pad, sm); @@ -871,7 +1038,7 @@ service_build_stream_start(service_t *t) lock_assert(&t->s_stream_mutex); - TAILQ_FOREACH(st, &t->s_components, es_link) + TAILQ_FOREACH(st, &t->s_filt_components, es_filt_link) n++; ss = calloc(1, sizeof(streaming_start_t) + @@ -880,7 +1047,7 @@ service_build_stream_start(service_t *t) ss->ss_num_components = n; n = 0; - TAILQ_FOREACH(st, &t->s_components, es_link) { + TAILQ_FOREACH(st, &t->s_filt_components, es_filt_link) { streaming_start_component_t *ssc = &ss->ss_components[n++]; ssc->ssc_index = st->es_index; ssc->ssc_type = st->es_type; diff --git a/src/service.h b/src/service.h index ab53082b..2aa5b56f 100644 --- a/src/service.h +++ b/src/service.h @@ -35,6 +35,7 @@ struct channel; typedef struct elementary_stream { TAILQ_ENTRY(elementary_stream) es_link; + TAILQ_ENTRY(elementary_stream) es_filt_link; int es_position; struct service *es_service; @@ -124,6 +125,9 @@ typedef struct elementary_stream { /* SI section processing (horrible hack) */ void *es_section; + /* Filter temporary variable */ + uint32_t es_filter; + } elementary_stream_t; @@ -400,9 +404,10 @@ typedef struct service { int s_caid; /** - * List of all components. + * List of all and filtered components. */ struct elementary_stream_queue s_components; + struct elementary_stream_queue s_filt_components; int s_last_pid; elementary_stream_t *s_last_es; @@ -429,6 +434,8 @@ void service_done(void); int service_start(service_t *t, int instance); +void service_build_filter(service_t *t); + service_t *service_create0(service_t *t, const idclass_t *idc, const char *uuid, int source_type, htsmsg_t *conf); #define service_create(t, c, u, s, m)\ diff --git a/src/subscriptions.c b/src/subscriptions.c index 0154291b..80985199 100644 --- a/src/subscriptions.c +++ b/src/subscriptions.c @@ -74,7 +74,7 @@ subscription_link_service(th_subscription_t *s, service_t *t) pthread_mutex_lock(&t->s_stream_mutex); - if(TAILQ_FIRST(&t->s_components) != NULL) { + if(TAILQ_FIRST(&t->s_filt_components) != NULL) { if(s->ths_start_message != NULL) streaming_msg_free(s->ths_start_message); @@ -122,7 +122,7 @@ subscription_unlink_service0(th_subscription_t *s, int reason, int stop) streaming_target_disconnect(&t->s_streaming_pad, &s->ths_input); if(stop && - TAILQ_FIRST(&t->s_components) != NULL && + TAILQ_FIRST(&t->s_filt_components) != NULL && s->ths_state == SUBSCRIPTION_GOT_SERVICE) { // Send a STOP message to the subscription client sm = streaming_msg_create_code(SMT_STOP, reason); diff --git a/src/tvheadend.h b/src/tvheadend.h index f0601ab1..83d29fe3 100644 --- a/src/tvheadend.h +++ b/src/tvheadend.h @@ -220,8 +220,11 @@ typedef enum { SCT_MP4A, SCT_VP8, SCT_VORBIS, + SCT_LAST = SCT_VORBIS } streaming_component_type_t; +#define SCT_MASK(t) (1 << (t)) + #define SCT_ISVIDEO(t) ((t) == SCT_MPEG2VIDEO || (t) == SCT_H264 || \ (t) == SCT_VP8) diff --git a/src/webui/extjs.c b/src/webui/extjs.c index bd828bbc..ed7e5b2e 100755 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -149,6 +149,7 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque) extjs_load(hq, "static/app/capmteditor.js"); extjs_load(hq, "static/app/tvadapters.js"); extjs_load(hq, "static/app/idnode.js"); + extjs_load(hq, "static/app/esfilter.js"); #if ENABLE_LINUXDVB extjs_load(hq, "static/app/mpegts.js"); #endif diff --git a/src/webui/static/app/esfilter.js b/src/webui/static/app/esfilter.js new file mode 100644 index 00000000..781b46ad --- /dev/null +++ b/src/webui/static/app/esfilter.js @@ -0,0 +1,108 @@ +/* + * Elementary Stream Filters + */ + +tvheadend.esfilter_tab = function(panel) +{ + tvheadend.idnode_grid(panel, { + url : 'api/esfilter/video', + comet : 'esfilter_video', + titleS : 'Video Stream Filter', + titleP : 'Video Stream Filters', + tabIndex : 0, + add : { + url : 'api/esfilter/video', + create : {} + }, + del : true, + move : true, + help : function() { + new tvheadend.help('Elementary Stream Filter', 'config_esfilter.html'); + } + }); + + tvheadend.idnode_grid(panel, { + url : 'api/esfilter/audio', + comet : 'esfilter_audio', + titleS : 'Audio Stream Filter', + titleP : 'Audio Stream Filters', + tabIndex : 1, + add : { + url : 'api/esfilter/audio', + create : {} + }, + del : true, + move : true, + help : function() { + new tvheadend.help('Elementary Stream Filter', 'config_esfilter.html'); + } + }); + + tvheadend.idnode_grid(panel, { + url : 'api/esfilter/teletext', + comet : 'esfilter_teletext', + titleS : 'Teletext Stream Filter', + titleP : 'Teletext Stream Filters', + tabIndex : 2, + add : { + url : 'api/esfilter/teletext', + create : {} + }, + del : true, + move : true, + help : function() { + new tvheadend.help('Elementary Stream Filter', 'config_esfilter.html'); + } + }); + + tvheadend.idnode_grid(panel, { + url : 'api/esfilter/subtit', + comet : 'esfilter_subtit', + titleS : 'Subtitle Stream Filter', + titleP : 'Subtitle Stream Filters', + tabIndex : 3, + add : { + url : 'api/esfilter/subtit', + create : {} + }, + del : true, + move : true, + help : function() { + new tvheadend.help('Elementary Stream Filter', 'config_esfilter.html'); + } + }); + + tvheadend.idnode_grid(panel, { + url : 'api/esfilter/ca', + comet : 'esfilter_ca', + titleS : 'CA Stream Filter', + titleP : 'CA Stream Filters', + tabIndex : 4, + add : { + url : 'api/esfilter/ca', + create : {} + }, + del : true, + move : true, + help : function() { + new tvheadend.help('Elementary Stream Filter', 'config_esfilter.html'); + } + }); + + tvheadend.idnode_grid(panel, { + url : 'api/esfilter/other', + comet : 'esfilter_other', + titleS : 'Other Stream Filter', + titleP : 'Other Stream Filters', + tabIndex : 5, + add : { + url : 'api/esfilter/other', + create : {} + }, + del : true, + move : true, + help : function() { + new tvheadend.help('Elementary Stream Filter', 'config_esfilter.html'); + } + }); +} diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index 4152748a..810ed5df 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -148,6 +148,14 @@ background-image: url(../icons/delete.png) !important; } +.moveup { + background-image: url(../icons/arrow_up.png) !important; +} + +.movedown { + background-image: url(../icons/arrow_down.png) !important; +} + .save { background-image: url(../icons/save.png) !important; } @@ -286,6 +294,11 @@ .arrow_switch { background-image: url(../icons/arrow_switch.png) !important; + +} + +.stream_config { + background-image: url(../icons/film_edit.png) !important; } .x-smallhdr { diff --git a/src/webui/static/app/idnode.js b/src/webui/static/app/idnode.js index 880565d6..8c7108d1 100644 --- a/src/webui/static/app/idnode.js +++ b/src/webui/static/app/idnode.js @@ -145,7 +145,6 @@ tvheadend.IdNodeField = function (conf) width : w, dataIndex: this.id, header : this.text, - sortable : true, editor : this.editor({create: false}), renderer : this.renderer(), hidden : this.hidden, @@ -665,10 +664,12 @@ tvheadend.idnode_grid = function(panel, conf) var undoBtn = null; var addBtn = null; var delBtn = null; + var upBtn = null; + var downBtn = null; var editBtn = null; /* Model */ - var idnode = new tvheadend.IdNode(d); + var idnode = new tvheadend.IdNode(d); for (var i = 0; i < idnode.length(); i++) { var f = idnode.field(i); var c = f.column(); @@ -708,8 +709,11 @@ tvheadend.idnode_grid = function(panel, conf) }); /* Model */ + var sortable = true; + if (conf.move) + sortable = false; var model = new Ext.grid.ColumnModel({ - defaultSortable : true, + defaultSortable : sortable, columns : columns }); @@ -727,6 +731,10 @@ tvheadend.idnode_grid = function(panel, conf) select.on('selectionchange', function(s){ if (delBtn) delBtn.setDisabled(s.getCount() == 0); + if (upBtn) { + upBtn.setDisabled(s.getCount() == 0); + downBtn.setDisabled(s.getCount() == 0); + } editBtn.setDisabled(s.getCount() != 1); if (conf.selected) conf.selected(s); @@ -809,7 +817,59 @@ tvheadend.idnode_grid = function(panel, conf) }); buttons.push(delBtn); } - if (conf.add || conf.del) + if (conf.move) { + upBtn = new Ext.Toolbar.Button({ + tooltip : 'Move selected entries up', + iconCls : 'moveup', + text : 'Move Up', + disabled : true, + handler : function() { + var r = select.getSelections(); + if (r && r.length > 0) { + var uuids = [] + for ( var i = 0; i < r.length; i++ ) + uuids.push(r[i].id) + Ext.Ajax.request({ + url : 'api/idnode/moveup', + params : { + uuid: Ext.encode(uuids) + }, + success : function(d) + { + store.reload(); + } + }); + } + } + }); + buttons.push(upBtn); + downBtn = new Ext.Toolbar.Button({ + tooltip : 'Move selected entries down', + iconCls : 'movedown', + text : 'Move Down', + disabled : true, + handler : function() { + var r = select.getSelections(); + if (r && r.length > 0) { + var uuids = [] + for ( var i = 0; i < r.length; i++ ) + uuids.push(r[i].id) + Ext.Ajax.request({ + url : 'api/idnode/movedown', + params : { + uuid: Ext.encode(uuids) + }, + success : function(d) + { + store.reload(); + } + }); + } + } + }); + buttons.push(downBtn); + } + if (conf.add || conf.del || conf.move) buttons.push('-'); editBtn = new Ext.Toolbar.Button({ tooltip : 'Edit selected entry', diff --git a/src/webui/static/app/mpegts.js b/src/webui/static/app/mpegts.js index 1dedd02a..3249bbec 100644 --- a/src/webui/static/app/mpegts.js +++ b/src/webui/static/app/mpegts.js @@ -99,15 +99,6 @@ tvheadend.show_service_streams = function ( data ) { var i, j; var html = ''; - html += ''; + } + + function stream ( s ) { var d = ' '; var p = '0x' + hexstr(s.pid) + ' / ' + fixstr(s.pid); @@ -146,16 +152,36 @@ tvheadend.show_service_streams = function ( data ) { } html += ''; html += ''; - } + } + + header(); + + if (data.streams.length) { + for (i = 0; i < data.streams.length; i++) + stream(data.streams[i]); + } else + single('None'); + + single(' '); + single('

After filtering and reordering (without PCR and PMT)

'); + header(); + + if (data.fstreams.length) + for (i = 0; i < data.fstreams.length; i++) + stream(data.fstreams[i]); + else + single('

None

'); var win = new Ext.Window({ title : 'Service details for ' + data.name, layout : 'fit', width : 650, - height : 300, + height : 400, plain : true, bodyStyle : 'padding: 5px', - html : html + html : html, + autoScroll: true, + autoShow: true }); win.show(); } diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index 36b188bf..dd9806a9 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -295,6 +295,17 @@ function accessUpdate(o) { tabs1.push(tvheadend.conf_csa); } + /* Stream Config */ + tvheadend.conf_stream = new Ext.TabPanel({ + activeTab: 0, + autoScroll: true, + title: 'Stream', + iconCls: 'stream_config', + items: [] + }); + tvheadend.esfilter_tab(tvheadend.conf_stream); + tabs1.push(tvheadend.conf_stream); + /* Debug */ tabs1.push(new tvheadend.tvhlog); diff --git a/src/webui/static/icons/film_edit.png b/src/webui/static/icons/film_edit.png new file mode 120000 index 00000000..03a05bfb --- /dev/null +++ b/src/webui/static/icons/film_edit.png @@ -0,0 +1 @@ +../../../../vendor/famfamsilk/film_edit.png \ No newline at end of file
' + d + '