src/rtsp.c \
src/fsmonitor.c \
src/cron.c \
+ src/esfilter.c
SRCS-${CONFIG_UPNP} += \
src/upnp.c
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 \
--- /dev/null
+<div class="hts-doc-text">
+
+This table defines rules to filter and order the elementary streams
+like video or audio from the input feed.
+
+<p>
+The execution order of commands is granted. It means that first rule
+is executed for all available streams then second and so on.
+
+<p>
+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.
+
+<p>
+The rules for different elementary stream groups (video, audio,
+teletext, subtitle, CA, other) are executed separately (as visually edited).
+
+<p>
+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.
+
+<p>
+The rules are listed / edited in a grid.
+
+<ul>
+ <li>To edit a cell, double click on it. After a cell is changed it
+ will flags one of its corner to red to indicated that it has been
+ changed. To commit these changes back to Tvheadend press the
+ 'Save changes' button. In order to change a Checkbox cell you only
+ have to click once in it.
+
+ <li>To add a new entry, press the 'Add entry' button. The new (empty) entry
+ will be created on the server but will not be in its enabled state.
+ You can now change all the cells to the desired values, check the
+ 'enable' box and then press 'Save changes' to activate the new entry.
+
+ <li>To delete one or more entries, select the lines (by clicking once on
+ them), and press the 'Delete selected' button. A pop up
+ will ask you to confirm your request.
+
+ <li>To move up or down one or more entries, select the lines (by clicking
+ once on them), and press the 'Move up' or 'Move down' button.
+</ul>
+
+<p>
+The columns have the following functions:
+
+<dl>
+ <dt>Enabled
+ <dd>If selected, the rule will be enabled.
+
+ <dt>Stream Type
+ <dd>Select the elementary stream type to compare. Empty field means any.
+
+ <dt>Language
+ <dd>Select the language to compare. Empty field means any.
+
+ <dt>Service
+ <dd>The service to compare. Empty field means any.
+
+ <dt>CA Identification
+ <dd>The CAID to compare. Empty field means any.
+
+ <dt>CA Provider
+ <dd>The CA provider to compare. Empty field means any.
+
+ <dt>PID
+ <dd>Program identification (PID) number to compare. Zero means any.
+ This comparison is processed only when service comparison is active.
+
+ <dt>Action
+ <dd>The rule action defines the operation when all comparisons succeeds.
+
+ <dl>
+
+ <dt>NONE
+ <dd>No action, may be used for the logging and a comparison verification.
+
+ <dt>USE
+ <dd>Use this elementary stream.
+
+ <dt>ONCE
+ <dd>Use this elementary stream only once per selected language.
+ The first successfully compared rule wins.
+
+ <dt>EXCLUSIVE
+ <dd>Use only this elementary stream. No other elementary streams
+ will be used.
+
+ <dt>EMPTY
+ <dd>Add this elementary stream only when no elementary streams are
+ used from previous rules.
+
+ <dt>IGNORE
+ <dd>Ignore this elementary stream. This stream is not used. Another
+ successfully compared rule with different action may override it.
+
+ </dl>
+
+ <dt>Log
+ <dd>Write a short message to log identifying the matched parameters.
+ It is useful for debugging your setup or structure of incoming
+ streams.
+
+ </dl>
+</div>
api_epggrab_init();
api_status_init();
api_imagecache_init();
+ api_esfilter_init();
}
void api_done ( 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;
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#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);
+}
}
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;
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 */
if (!(in = idnode_find(uuid, NULL)))
err = ENOENT;
else
- idnode_delete(in);
+ handler(in);
}
pthread_mutex_unlock(&global_lock);
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 },
};
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;
/* 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);
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 */
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#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);
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#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__ */
/**
*
*/
-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
* *************************************************************************/
/*
* 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
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
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);
#include "timeshift.h"
#include "fsmonitor.h"
#include "lang_codes.h"
+#include "esfilter.h"
#if ENABLE_LIBAV
#include "libav.h"
#include "plumbing/transcoding.h"
imagecache_init();
http_client_init();
+ esfilter_init();
service_init();
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");
#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);
/**
* 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;
}
+/**
+ *
+ */
+#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);
+ }
+ }
+ }
+}
+
/**
*
*/
t->s_streaming_status = 0;
t->s_scrambled_seen = 0;
+ service_build_filter(t);
+
if((r = t->s_start_feed(t, instance)))
return r;
/**
* 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);
t->s_status = SERVICE_ZOMBIE;
+ TAILQ_INIT(&t->s_filt_components);
while((st = TAILQ_FIRST(&t->s_components)) != NULL)
service_stream_destroy(t, st);
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);
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);
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;
}
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);
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) +
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;
typedef struct elementary_stream {
TAILQ_ENTRY(elementary_stream) es_link;
+ TAILQ_ENTRY(elementary_stream) es_filt_link;
int es_position;
struct service *es_service;
/* SI section processing (horrible hack) */
void *es_section;
+ /* Filter temporary variable */
+ uint32_t es_filter;
+
} elementary_stream_t;
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;
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)\
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);
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);
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)
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
--- /dev/null
+/*
+ * 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');
+ }
+ });
+}
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;
}
.arrow_switch {
background-image: url(../icons/arrow_switch.png) !important;
+
+}
+
+.stream_config {
+ background-image: url(../icons/film_edit.png) !important;
}
.x-smallhdr {
width : w,
dataIndex: this.id,
header : this.text,
- sortable : true,
editor : this.editor({create: false}),
renderer : this.renderer(),
hidden : this.hidden,
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();
});
/* Model */
+ var sortable = true;
+ if (conf.move)
+ sortable = false;
var model = new Ext.grid.ColumnModel({
- defaultSortable : true,
+ defaultSortable : sortable,
columns : columns
});
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);
});
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',
var i, j;
var html = '';
- html += '<table style="font-size:8pt;font-family:mono;padding:2px"';
- html += '<tr>';
- html += '<th style="width:50px;font-weight:bold">Index</th>';
- html += '<th style="width:120px;font-weight:bold">PID</th>';
- html += '<th style="width:100px;font-weight:bold">Type</th>';
- html += '<th style="width:75px;font-weight:bold">Language</th>';
- html += '<th style="width:*;font-weight:bold">Details</th>';
- html += '</tr>';
-
function hexstr ( d ) {
return ('0000' + d.toString(16)).slice(-4);
}
return r;
}
- for (i = 0; i < data.streams.length; i++) {
- var s = data.streams[i];
+ function header ( ) {
+ html += '<table style="font-size:8pt;font-family:mono;padding:2px"';
+ html += '<tr>';
+ html += '<th style="width:50px;font-weight:bold">Index</th>';
+ html += '<th style="width:120px;font-weight:bold">PID</th>';
+ html += '<th style="width:100px;font-weight:bold">Type</th>';
+ html += '<th style="width:75px;font-weight:bold">Language</th>';
+ html += '<th style="width:*;font-weight:bold">Details</th>';
+ html += '</tr>';
+
+ }
+
+ function single ( s ) {
+ html += '<tr><td colspan="5">' + s + '</td></tr>';
+ }
+
+ function stream ( s ) {
var d = ' ';
var p = '0x' + hexstr(s.pid) + ' / ' + fixstr(s.pid);
}
html += '<td>' + d + '</td>';
html += '</tr>';
- }
+ }
+
+ header();
+
+ if (data.streams.length) {
+ for (i = 0; i < data.streams.length; i++)
+ stream(data.streams[i]);
+ } else
+ single('None');
+
+ single(' ');
+ single('<h3>After filtering and reordering (without PCR and PMT)</h3>');
+ header();
+
+ if (data.fstreams.length)
+ for (i = 0; i < data.fstreams.length; i++)
+ stream(data.fstreams[i]);
+ else
+ single('<p>None</p>');
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();
}
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);
--- /dev/null
+../../../../vendor/famfamsilk/film_edit.png
\ No newline at end of file