diff --git a/Makefile b/Makefile index 275e343595..18f38a072a 100644 --- a/Makefile +++ b/Makefile @@ -113,6 +113,7 @@ SRCS = src/version.c \ src/input.c \ src/http/http_client.c \ src/fsmonitor.c \ + src/esfilter.c SRCS += \ src/api.c \ @@ -124,7 +125,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 0000000000..4cbde768b2 --- /dev/null +++ b/docs/html/config_esfilter.html @@ -0,0 +1,98 @@ +
+ +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, other) are executed separately (as visually edited). + +

+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. + +
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 97546982c4..833926225a 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 629330e818..e4e9204eb9 100644 --- a/src/api.h +++ b/src/api.h @@ -66,12 +66,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 0000000000..07cf24f148 --- /dev/null +++ b/src/api/api_esfilter.c @@ -0,0 +1,96 @@ +/* + * 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(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/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 e8452ac43b..6b87efb975 100644 --- a/src/api/api_idnode.c +++ b/src/api/api_idnode.c @@ -380,8 +380,8 @@ api_idnode_class } 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; @@ -403,7 +403,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 */ @@ -412,7 +412,7 @@ api_idnode_delete if (!(in = idnode_find(uuid, NULL))) err = ENOENT; else - idnode_delete(in); + handler(in); } pthread_mutex_unlock(&global_lock); @@ -420,14 +420,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/esfilter.c b/src/esfilter.c new file mode 100644 index 0000000000..9381ea92ac --- /dev/null +++ b/src/esfilter.c @@ -0,0 +1,790 @@ +/* + * 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 "esfilter.h" + +struct esfilter_entry_queue esfilters[ESF_CLASS_LAST]; + +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_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_other +}; + +/* + * 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; + + 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); + } + tvhlog(LOG_INFO, "test", "moveup %s", idnode_uuid_as_str(self)); +} + +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); + } + tvhlog(LOG_INFO, "test", "movedown %s", idnode_uuid_as_str(self)); +} + +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(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_STR_LEN)) { + strncpy(esf->esf_service, s, UUID_STR_LEN); + esf->esf_service[UUID_STR_LEN-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; +} + +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_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 0000000000..665a660f5e --- /dev/null +++ b/src/esfilter.h @@ -0,0 +1,102 @@ +/* + * 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_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_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_OTHER \ + (SCT_MASK(SCT_CA) | 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_STR_LEN]; + int esf_pid; + 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_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 64ac2c5bcb..80b48ad10d 100644 --- a/src/idnode.c +++ b/src/idnode.c @@ -206,20 +206,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 a10c1056b8..4d5443aba6 100644 --- a/src/idnode.h +++ b/src/idnode.h @@ -44,7 +44,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 @@ -52,11 +53,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 @@ -123,6 +126,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 4216fee253..aa207bbd0a 100644 --- a/src/main.c +++ b/src/main.c @@ -60,6 +60,7 @@ #include "timeshift.h" #include "fsmonitor.h" #include "lang_codes.h" +#include "esfilter.h" #if ENABLE_LIBAV #include "libav.h" #include "plumbing/transcoding.h" @@ -754,6 +755,8 @@ main(int argc, char **argv) imagecache_init(); + esfilter_init(); + service_init(); #if ENABLE_TSFILE @@ -871,6 +874,7 @@ main(int argc, char **argv) tvhftrace("main", hts_settings_done); tvhftrace("main", dvb_done); tvhftrace("main", lang_str_done); + tvhftrace("main", esfilter_done); tvhlog(LOG_NOTICE, "STOP", "Exiting HTS Tvheadend"); tvhlog_end(); diff --git a/src/service.c b/src/service.c index a3ff609c4f..d606b64a08 100644 --- a/src/service.c +++ b/src/service.c @@ -44,6 +44,7 @@ #include "lang_codes.h" #include "descrambler.h" #include "input/mpegts.h" +#include "esfilter.h" static void service_data_timeout(void *aux); static void service_class_save(struct idnode *self); @@ -862,6 +863,35 @@ service_restart(service_t *t, int had_components) t->s_refresh_feed(t); } +/** + * + */ +#define ESFM_USED (1<<0) +#define ESFM_IGNORE (1<<1) + +static void +ss_copy_info(streaming_start_t *ss, elementary_stream_t *st) +{ + streaming_start_component_t *ssc; + + if (st->es_filter & ESFM_USED) + return; + st->es_filter |= ESFM_USED; + + ssc = &ss->ss_components[ss->ss_num_components++]; + + ssc->ssc_index = st->es_index; + ssc->ssc_type = st->es_type; + + memcpy(ssc->ssc_lang, st->es_lang, 4); + ssc->ssc_audio_type = st->es_audio_type; + ssc->ssc_composition_id = st->es_composition_id; + ssc->ssc_ancillary_id = st->es_ancillary_id; + ssc->ssc_pid = st->es_pid; + ssc->ssc_width = st->es_width; + ssc->ssc_height = st->es_height; + ssc->ssc_frameduration = st->es_frame_duration; +} /** * Generate a message containing info about all components @@ -870,36 +900,129 @@ streaming_start_t * service_build_stream_start(service_t *t) { extern const idclass_t mpegts_service_class; - elementary_stream_t *st; + elementary_stream_t *st, *st2; int n = 0; streaming_start_t *ss; + esfilter_t *esf; + int i, exclusive; + uint32_t mask; lock_assert(&t->s_stream_mutex); - TAILQ_FOREACH(st, &t->s_components, es_link) + TAILQ_FOREACH(st, &t->s_components, es_link) { + st->es_filter = 0; n++; + } ss = calloc(1, sizeof(streaming_start_t) + sizeof(streaming_start_component_t) * n); - ss->ss_num_components = n; - - n = 0; - TAILQ_FOREACH(st, &t->s_components, es_link) { - streaming_start_component_t *ssc = &ss->ss_components[n++]; - ssc->ssc_index = st->es_index; - ssc->ssc_type = st->es_type; + for (i = ESF_CLASS_VIDEO; i <= ESF_CLASS_LAST; i++) + if (!TAILQ_EMPTY(&esfilters[i])) + goto filter; - memcpy(ssc->ssc_lang, st->es_lang, 4); - ssc->ssc_audio_type = st->es_audio_type; - ssc->ssc_composition_id = st->es_composition_id; - ssc->ssc_ancillary_id = st->es_ancillary_id; - ssc->ssc_pid = st->es_pid; - ssc->ssc_width = st->es_width; - ssc->ssc_height = st->es_height; - ssc->ssc_frameduration = st->es_frame_duration; + /* Copy all elementary streams without modification */ + TAILQ_FOREACH(st, &t->s_components, es_link) + ss_copy_info(ss, st); + goto filter_end; + +filter: + for (i = ESF_CLASS_VIDEO; i <= ESF_CLASS_LAST; i++) { + n = ss->ss_num_components; + mask = esfilterclsmask[i]; + if (TAILQ_EMPTY(&esfilters[i])) { + TAILQ_FOREACH(st, &t->s_components, es_link) { + if ((mask & SCT_MASK(st->es_type)) != 0) + ss_copy_info(ss, st); + } + 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 (esf->esf_log) + tvhlog(LOG_INFO, "service", "esfilter: %03d %05d %s %s %s %s", + 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: + ss_copy_info(ss, st); + break; + case ESFA_ONCE: + if (esf->esf_language[0] == '\0') { + ss_copy_info(ss, st); + } 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) + ss_copy_info(ss, st); + } + break; + case ESFA_EXCLUSIVE: + break; + case ESFA_EMPTY: + if (n == ss->ss_num_components) + ss_copy_info(ss, st); + break; + default: + tvhlog(LOG_DEBUG, "service", "Unknown esfilter action %d", esf->esf_action); + break; + } + if (esf->esf_action == ESFA_EXCLUSIVE) { + /* forget previous work */ + if (n < ss->ss_num_components) { + memset(&ss->ss_components[n-1], 0, + (ss->ss_num_components - n) * + sizeof(streaming_start_component_t)); + ss->ss_num_components = n; + } + ss_copy_info(ss, st); + 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) + ss_copy_info(ss, st); + } + } } +filter_end: + ss->ss_num_components = n; + t->s_setsourceinfo(t, &ss->ss_si); ss->ss_refcount = 1; diff --git a/src/service.h b/src/service.h index f572a217a3..5c0323391e 100644 --- a/src/service.h +++ b/src/service.h @@ -125,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; diff --git a/src/tvheadend.h b/src/tvheadend.h index 34b5cf1302..930f1db07a 100644 --- a/src/tvheadend.h +++ b/src/tvheadend.h @@ -213,8 +213,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 443769dbfe..84920a243d 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 0000000000..78ac1f73e2 --- /dev/null +++ b/src/webui/static/app/esfilter.js @@ -0,0 +1,91 @@ +/* + * 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/other', + comet : 'esfilter_other', + titleS : 'Other Stream Filter', + titleP : 'Other Stream Filters', + tabIndex : 4, + 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 4152748a76..810ed5df11 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 238c76b2f0..7379bdfbda 100644 --- a/src/webui/static/app/idnode.js +++ b/src/webui/static/app/idnode.js @@ -112,7 +112,7 @@ tvheadend.IdNodeField = function (conf) this.id = conf.id; this.text = conf.caption || this.id; this.type = conf.type; - this.list = conf.list; + this.list = conf.list; this.rdonly = conf.rdonly; this.wronly = conf.wronly; this.wronce = conf.wronce; @@ -137,7 +137,6 @@ tvheadend.IdNodeField = function (conf) return { dataIndex: this.id, header : this.text, - sortable : true, editor : this.editor({create: false}), renderer : this.renderer(), hidden : this.hidden, @@ -609,10 +608,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.fields[i]; var c = f.column(); @@ -652,8 +653,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 }); @@ -671,6 +675,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); @@ -753,7 +761,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/tvheadend.js b/src/webui/static/app/tvheadend.js index 4649c17363..6d041487e2 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -294,6 +294,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 0000000000..03a05bfbfa --- /dev/null +++ b/src/webui/static/icons/film_edit.png @@ -0,0 +1 @@ +../../../../vendor/famfamsilk/film_edit.png \ No newline at end of file