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 + '