diff --git a/Makefile b/Makefile
index 5cde334c..2e497792 100644
--- a/Makefile
+++ b/Makefile
@@ -152,7 +152,8 @@ SRCS = src/version.c \
src/cron.c \
src/esfilter.c \
src/intlconv.c \
- src/profile.c
+ src/profile.c \
+ src/bouquet.c
SRCS-${CONFIG_UPNP} += \
src/upnp.c
@@ -173,7 +174,8 @@ SRCS += \
src/api/api_access.c \
src/api/api_dvr.c \
src/api/api_caclient.c \
- src/api/api_profile.c
+ src/api/api_profile.c \
+ src/api/api_bouquet.c
SRCS += \
src/parsers/parsers.c \
@@ -229,6 +231,7 @@ SRCS-$(CONFIG_MPEGTS) += \
src/input/mpegts/dvb_support.c \
src/input/mpegts/dvb_charset.c \
src/input/mpegts/dvb_psi.c \
+ src/input/mpegts/fastscan.c \
src/input/mpegts/tsdemux.c \
src/input/mpegts/mpegts_mux_sched.c \
src/input/mpegts/mpegts_network_scan.c \
diff --git a/data/conf/fastscan b/data/conf/fastscan
new file mode 100644
index 00000000..3c92b10d
--- /dev/null
+++ b/data/conf/fastscan
@@ -0,0 +1,50 @@
+[
+ {
+ "name": "Canal Digitaal",
+ "position": 192,
+ "frequency": 12515000,
+ "pid": 900
+ },
+ {
+ "name": "TV Vlaanderen",
+ "position": 192,
+ "frequency": 12515000,
+ "pid": 901
+ },
+ {
+ "name": "TéléSAT",
+ "position": 192,
+ "frequency": 12515000,
+ "pid": 920
+ },
+ {
+ "name": "Mobistar NL",
+ "position": 192,
+ "frequency": 12515000,
+ "pid": 930
+ },
+ {
+ "name": "Mobistar FR",
+ "position": 192,
+ "frequency": 12515000,
+ "pid": 940
+ },
+ {
+ "name": "AustriaSat",
+ "position": 192,
+ "frequency": 12515000,
+ "pid": 950
+ },
+ {
+ "name": "Skylink: Czech Republic",
+ "position": 235,
+ "frequency": 12070000,
+ "pid": 30
+ },
+ {
+ "name": "Skylink: Slovak Republic",
+ "position": 235,
+ "frequency": 12070000,
+ "pid": 31
+ }
+]
diff --git a/src/api.c b/src/api.c
index 8594706a..b089b9b7 100644
--- a/src/api.c
+++ b/src/api.c
@@ -125,6 +125,7 @@ void api_init ( void )
api_mpegts_init();
api_service_init();
api_channel_init();
+ api_bouquet_init();
api_epg_init();
api_epggrab_init();
api_status_init();
diff --git a/src/api.h b/src/api.h
index 4928484d..e5b360d2 100644
--- a/src/api.h
+++ b/src/api.h
@@ -64,6 +64,7 @@ void api_idnode_init ( void );
void api_input_init ( void );
void api_service_init ( void );
void api_channel_init ( void );
+void api_bouquet_init ( void );
void api_mpegts_init ( void );
void api_epg_init ( void );
void api_epggrab_init ( void );
diff --git a/src/api/api_bouquet.c b/src/api/api_bouquet.c
new file mode 100644
index 00000000..bd3e5e9e
--- /dev/null
+++ b/src/api/api_bouquet.c
@@ -0,0 +1,70 @@
+/*
+ * API - bouquet 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 .
+ */
+
+#ifndef __TVH_API_BOUQUET_H__
+#define __TVH_API_BOUQUET_H__
+
+#include "tvheadend.h"
+#include "bouquet.h"
+#include "access.h"
+#include "api.h"
+
+static void
+api_bouquet_grid
+ ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf )
+{
+ bouquet_t *bq;
+
+ RB_FOREACH(bq, &bouquets, bq_link)
+ idnode_set_add(ins, (idnode_t*)bq, &conf->filter);
+}
+
+static int
+api_bouquet_create
+ ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+ htsmsg_t *conf;
+ bouquet_t *bq;
+
+ if (!(conf = htsmsg_get_map(args, "conf")))
+ return EINVAL;
+
+ pthread_mutex_lock(&global_lock);
+ bq = bouquet_create(NULL, conf, NULL, NULL);
+ if (bq)
+ bouquet_save(bq, 0);
+ pthread_mutex_unlock(&global_lock);
+
+ return 0;
+}
+
+void api_bouquet_init ( void )
+{
+ static api_hook_t ah[] = {
+ { "bouquet/class", ACCESS_ADMIN, api_idnode_class, (void*)&bouquet_class },
+ { "bouquet/grid", ACCESS_ADMIN, api_idnode_grid, api_bouquet_grid },
+ { "bouquet/create", ACCESS_ADMIN, api_bouquet_create, NULL },
+
+ { NULL },
+ };
+
+ api_register_all(ah);
+}
+
+#endif /* __TVH_API_BOUQUET_H__ */
diff --git a/src/api/api_channel.c b/src/api/api_channel.c
index aaf87b45..58ffe6bb 100644
--- a/src/api/api_channel.c
+++ b/src/api/api_channel.c
@@ -17,8 +17,8 @@
* along with this program. If not, see .
*/
-#ifndef __TVH_API_SERVICE_H__
-#define __TVH_API_SERVICE_H__
+#ifndef __TVH_API_CHANNEL_H__
+#define __TVH_API_CHANNEL_H__
#include "tvheadend.h"
#include "channels.h"
@@ -145,4 +145,4 @@ void api_channel_init ( void )
}
-#endif /* __TVH_API_IDNODE_H__ */
+#endif /* __TVH_API_CHANNEL_H__ */
diff --git a/src/api/api_service.c b/src/api/api_service.c
index 38012e42..f0bd3909 100644
--- a/src/api/api_service.c
+++ b/src/api/api_service.c
@@ -199,4 +199,4 @@ void api_service_init ( void )
}
-#endif /* __TVH_API_IDNODE_H__ */
+#endif /* __TVH_API_SERVICE_H__ */
diff --git a/src/bouquet.c b/src/bouquet.c
new file mode 100644
index 00000000..0881f91b
--- /dev/null
+++ b/src/bouquet.c
@@ -0,0 +1,387 @@
+/*
+ * tvheadend, bouquets
+ * 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 "access.h"
+#include "bouquet.h"
+#include "service.h"
+
+bouquet_tree_t bouquets;
+
+/**
+ *
+ */
+static int
+_bq_cmp(const void *a, const void *b)
+{
+ return strcmp(((bouquet_t *)a)->bq_src ?: "", ((bouquet_t *)b)->bq_src ?: "");
+}
+
+/**
+ *
+ */
+bouquet_t *
+bouquet_create(const char *uuid, htsmsg_t *conf,
+ const char *name, const char *src)
+{
+ bouquet_t *bq, *bq2;
+ int i;
+
+ lock_assert(&global_lock);
+
+ bq = calloc(1, sizeof(bouquet_t));
+ bq->bq_services = idnode_set_create();
+
+ if (idnode_insert(&bq->bq_id, uuid, &bouquet_class, 0)) {
+ if (uuid)
+ tvherror("bouquet", "invalid uuid '%s'", uuid);
+ free(bq);
+ return NULL;
+ }
+
+ if (conf) {
+ bq->bq_in_load = 1;
+ idnode_load(&bq->bq_id, conf);
+ bq->bq_in_load = 0;
+ if (!htsmsg_get_bool(conf, "shield", &i) && i)
+ bq->bq_shield = 1;
+ }
+
+ if (name) {
+ free(bq->bq_name);
+ bq->bq_name = strdup(name);
+ }
+
+ if (src) {
+ free(bq->bq_src);
+ bq->bq_src = strdup(src);
+ }
+
+ bq2 = RB_INSERT_SORTED(&bouquets, bq, bq_link, _bq_cmp);
+ assert(bq2 == NULL);
+
+ bq->bq_saveflag = 1;
+
+ return bq;
+}
+
+/**
+ *
+ */
+static void
+bouquet_destroy(bouquet_t *bq)
+{
+ if (!bq)
+ return;
+
+ RB_REMOVE(&bouquets, bq, bq_link);
+ idnode_unlink(&bq->bq_id);
+
+ idnode_set_free(bq->bq_services);
+ free(bq->bq_name);
+ free(bq->bq_src);
+ free(bq);
+}
+
+/**
+ *
+ */
+void
+bouquet_destroy_by_service(service_t *t)
+{
+ bouquet_t *bq;
+
+ lock_assert(&global_lock);
+
+ RB_FOREACH(bq, &bouquets, bq_link)
+ if (idnode_set_exists(bq->bq_services, &t->s_id))
+ idnode_set_remove(bq->bq_services, &t->s_id);
+}
+
+/*
+ *
+ */
+bouquet_t *
+bouquet_find_by_source(const char *name, const char *src, int create)
+{
+ bouquet_t *bq;
+ bouquet_t bqs;
+
+ assert(src);
+
+ lock_assert(&global_lock);
+
+ bqs.bq_src = (char *)src;
+ bq = RB_FIND(&bouquets, &bqs, bq_link, _bq_cmp);
+ if (bq)
+ return bq;
+ if (create && name)
+ return bouquet_create(NULL, NULL, name, src);
+ return NULL;
+}
+
+/*
+ *
+ */
+void
+bouquet_add_service(bouquet_t *bq, service_t *s)
+{
+ lock_assert(&global_lock);
+
+ if (!idnode_set_exists(bq->bq_services, &s->s_id)) {
+ idnode_set_add(bq->bq_services, &s->s_id, NULL);
+ bq->bq_saveflag = 1;
+ }
+}
+
+/**
+ *
+ */
+void
+bouquet_save(bouquet_t *bq, int notify)
+{
+ htsmsg_t *c = htsmsg_create_map();
+ idnode_save(&bq->bq_id, c);
+ hts_settings_save(c, "bouquet/%s", idnode_uuid_as_str(&bq->bq_id));
+ if (bq->bq_shield)
+ htsmsg_add_bool(c, "shield", 1);
+ htsmsg_destroy(c);
+ bq->bq_saveflag = 0;
+ if (notify)
+ idnode_notify_simple(&bq->bq_id);
+}
+
+/* **************************************************************************
+ * Class definition
+ * **************************************************************************/
+
+static void
+bouquet_class_save(idnode_t *self)
+{
+ bouquet_save((bouquet_t *)self, 0);
+}
+
+static void
+bouquet_class_delete(idnode_t *self)
+{
+ bouquet_t *bq = (bouquet_t *)self;
+
+ if (!bq->bq_shield) {
+ hts_settings_remove("bouquet/%s", idnode_uuid_as_str(&bq->bq_id));
+ bouquet_destroy(bq);
+ } else {
+ idnode_set_free(bq->bq_services);
+ bq->bq_services = idnode_set_create();
+ bouquet_save(bq, 1);
+ }
+}
+
+static const char *
+bouquet_class_get_title (idnode_t *self)
+{
+ bouquet_t *bq = (bouquet_t *)self;
+
+ if (bq->bq_comment && bq->bq_comment[0] != '\0')
+ return bq->bq_comment;
+ return bq->bq_name ?: "";
+}
+
+static void
+bouquet_class_enabled_notify ( void *obj )
+{
+ bouquet_t *bq = obj;
+ service_t *s;
+ size_t z;
+
+ if (!bq->bq_enabled) {
+ for (z = 0; z < bq->bq_services->is_count; z++) {
+ s = (service_t *)bq->bq_services->is_array[z];
+ if (s->s_master_bouquet == bq)
+ s->s_master_bouquet = NULL;
+ }
+ }
+}
+
+static const void *
+bouquet_class_services_get ( void *obj )
+{
+ htsmsg_t *l = htsmsg_create_list();
+ bouquet_t *bq = obj;
+ size_t z;
+
+ /* Add all */
+ for (z = 0; z < bq->bq_services->is_count; z++)
+ htsmsg_add_str(l, NULL, idnode_uuid_as_str(bq->bq_services->is_array[z]));
+
+ return l;
+}
+
+static char *
+bouquet_class_services_rend ( void *obj )
+{
+ bouquet_t *bq = obj;
+ char buf[32];
+ snprintf(buf, sizeof(buf), "Services Count %zi", bq->bq_services->is_count);
+ return strdup(buf);
+}
+
+static int
+bouquet_class_services_set ( void *obj, const void *p )
+{
+ bouquet_t *bq = obj;
+
+ if (bq->bq_services_waiting)
+ htsmsg_destroy(bq->bq_services_waiting);
+ bq->bq_services_waiting = NULL;
+ if (bq->bq_in_load)
+ bq->bq_services_waiting = htsmsg_copy((htsmsg_t *)p);
+ return 0;
+}
+
+static const void *
+bouquet_class_services_count_get ( void *obj )
+{
+ static uint32_t u32;
+ bouquet_t *bq = obj;
+
+ u32 = bq->bq_services->is_count;
+ return &u32;
+}
+
+const idclass_t bouquet_class = {
+ .ic_class = "bouquet",
+ .ic_caption = "Bouquet",
+ .ic_event = "bouquet",
+ .ic_perm_def = ACCESS_ADMIN,
+ .ic_save = bouquet_class_save,
+ .ic_get_title = bouquet_class_get_title,
+ .ic_delete = bouquet_class_delete,
+ .ic_properties = (const property_t[]){
+ {
+ .type = PT_BOOL,
+ .id = "enabled",
+ .name = "Enabled",
+ .off = offsetof(bouquet_t, bq_enabled),
+ .notify = bouquet_class_enabled_notify,
+ },
+ {
+ .type = PT_STR,
+ .id = "name",
+ .name = "Name",
+ .off = offsetof(bouquet_t, bq_name),
+ },
+ {
+ .type = PT_STR,
+ .id = "source",
+ .name = "Source",
+ .off = offsetof(bouquet_t, bq_src),
+ .opts = PO_RDONLY,
+ },
+ {
+ .type = PT_STR,
+ .islist = 1,
+ .id = "services",
+ .name = "Services",
+ .get = bouquet_class_services_get,
+ .set = bouquet_class_services_set,
+ .rend = bouquet_class_services_rend,
+ .opts = PO_RDONLY | PO_HIDDEN,
+ },
+ {
+ .type = PT_U32,
+ .id = "services_count",
+ .name = "# Services",
+ .get = bouquet_class_services_count_get,
+ .opts = PO_RDONLY | PO_NOSAVE,
+ },
+ {
+ .type = PT_STR,
+ .id = "comment",
+ .name = "Comment",
+ .off = offsetof(bouquet_t, bq_comment),
+ },
+ {
+ .type = PT_U32,
+ .id = "lcn_off",
+ .name = "Channel Number Offset",
+ .off = offsetof(bouquet_t, bq_lcn_offset),
+ },
+ {}
+ }
+};
+
+/**
+ *
+ */
+void
+bouquet_init(void)
+{
+ htsmsg_t *c, *m;
+ htsmsg_field_t *f;
+
+ RB_INIT(&bouquets);
+
+ /* Load */
+ if ((c = hts_settings_load("bouquet")) != NULL) {
+ HTSMSG_FOREACH(f, c) {
+ if (!(m = htsmsg_field_get_map(f))) continue;
+ (void)bouquet_create(f->hmf_name, m, NULL, NULL);
+ }
+ htsmsg_destroy(c);
+ }
+}
+
+void
+bouquet_service_resolve(void)
+{
+ bouquet_t *bq;
+ htsmsg_field_t *f;
+ service_t *s;
+ const char *str;
+ int saveflag;
+
+ lock_assert(&global_lock);
+
+ RB_FOREACH(bq, &bouquets, bq_link) {
+ if (!bq->bq_services_waiting)
+ continue;
+ saveflag = bq->bq_saveflag;
+ HTSMSG_FOREACH(f, bq->bq_services_waiting) {
+ if ((str = htsmsg_field_get_str(f))) {
+ s = service_find_by_identifier(str);
+ if (s)
+ bouquet_add_service(bq, s);
+ }
+ }
+ htsmsg_destroy(bq->bq_services_waiting);
+ bq->bq_services_waiting = NULL;
+ bq->bq_saveflag = saveflag;
+ }
+}
+
+void
+bouquet_done(void)
+{
+ bouquet_t *bq;
+
+ pthread_mutex_lock(&global_lock);
+ while ((bq = RB_FIRST(&bouquets)) != NULL)
+ bouquet_destroy(bq);
+ pthread_mutex_unlock(&global_lock);
+}
diff --git a/src/bouquet.h b/src/bouquet.h
new file mode 100644
index 00000000..2e2537a5
--- /dev/null
+++ b/src/bouquet.h
@@ -0,0 +1,88 @@
+/*
+ * TV headend - Bouquets
+ * 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 BOUQUET_H_
+#define BOUQUET_H_
+
+#include "idnode.h"
+#include "htsmsg.h"
+#include "service.h"
+
+typedef struct bouquet {
+ idnode_t bq_id;
+ RB_ENTRY(bouquet) bq_link;
+
+ int bq_saveflag;
+ int bq_in_load;
+
+ int bq_shield;
+ int bq_enabled;
+ char *bq_name;
+ char *bq_src;
+ char *bq_comment;
+ idnode_set_t *bq_services;
+ htsmsg_t *bq_services_waiting;
+ uint32_t bq_lcn_offset;
+
+} bouquet_t;
+
+typedef RB_HEAD(,bouquet) bouquet_tree_t;
+
+extern bouquet_tree_t bouquets;
+
+extern const idclass_t bouquet_class;
+
+/**
+ *
+ */
+bouquet_t *
+bouquet_create(const char *uuid, htsmsg_t *conf,
+ const char *name, const char *src);
+
+/**
+ *
+ */
+void
+bouquet_destroy_by_service(service_t *t);
+
+/**
+ *
+ */
+bouquet_t *
+bouquet_find_by_source(const char *name, const char *src, int create);
+
+/**
+ *
+ */
+void
+bouquet_add_service(bouquet_t *bq, service_t *s);
+
+/**
+ *
+ */
+void
+bouquet_save(bouquet_t *bq, int notify);
+
+/**
+ *
+ */
+void bouquet_init(void);
+void bouquet_service_resolve(void);
+void bouquet_done(void);
+
+#endif /* BOUQUET_H_ */
diff --git a/src/idnode.c b/src/idnode.c
index e9d52c61..bf60af44 100644
--- a/src/idnode.c
+++ b/src/idnode.c
@@ -164,8 +164,8 @@ idnode_insert(idnode_t *in, const char *uuid, const idclass_t *class, int flags)
} while (c != NULL && --retries > 0);
if(c != NULL) {
- fprintf(stderr, "Id node collision%s\n",
- (flags & IDNODE_SHORT_UUID) ? " (short)" : "");
+ fprintf(stderr, "Id node collision (%s) %s\n",
+ uuid, (flags & IDNODE_SHORT_UUID) ? " (short)" : "");
abort();
}
tvhtrace("idnode", "insert node %s", idnode_uuid_as_str(in));
@@ -958,6 +958,21 @@ idnode_set_add
is->is_array[is->is_count++] = in;
}
+void
+idnode_set_remove
+ ( idnode_set_t *is, idnode_t *in )
+{
+ size_t z;
+
+ for (z = 0; z < is->is_count; z++)
+ if (is->is_array[z] == in) {
+ memmove(&is->is_array[z], &is->is_array[z+1],
+ (is->is_count - z - 1) * sizeof(idnode_t *));
+ is->is_count--;
+ break;
+ }
+}
+
int
idnode_set_exists
( idnode_set_t *is, idnode_t * in )
diff --git a/src/idnode.h b/src/idnode.h
index fbaae48f..544e6f91 100644
--- a/src/idnode.h
+++ b/src/idnode.h
@@ -200,11 +200,12 @@ int idnode_filter
#define idnode_set_create() calloc(1, sizeof(idnode_set_t))
void idnode_set_add
( idnode_set_t *is, idnode_t *in, idnode_filter_t *filt );
+void idnode_set_remove ( idnode_set_t *is, idnode_t *in );
int idnode_set_exists ( idnode_set_t *is, idnode_t *in );
-void idnode_set_sort ( idnode_set_t *is, idnode_sort_t *s );
+void idnode_set_sort ( idnode_set_t *is, idnode_sort_t *s );
void idnode_set_sort_by_title ( idnode_set_t *is );
htsmsg_t *idnode_set_as_htsmsg ( idnode_set_t *is );
-void idnode_set_free ( idnode_set_t *is );
+void idnode_set_free ( idnode_set_t *is );
#endif /* __TVH_IDNODE_H__ */
diff --git a/src/input/mpegts.c b/src/input/mpegts.c
index 7a105bb8..4b65b89b 100644
--- a/src/input/mpegts.c
+++ b/src/input/mpegts.c
@@ -17,6 +17,7 @@
*/
#include "input.h"
+#include "mpegts/fastscan.h"
void
mpegts_init ( int linuxdvb_mask, str_list_t *satip_client,
@@ -27,6 +28,9 @@ mpegts_init ( int linuxdvb_mask, str_list_t *satip_client,
idclass_register(&mpegts_mux_class);
idclass_register(&mpegts_service_class);
+ /* FastScan init */
+ dvb_fastscan_init();
+
/* Network scanner */
#if ENABLE_MPEGTS
mpegts_network_scan_init();
@@ -97,6 +101,7 @@ mpegts_done ( void )
#if ENABLE_TSFILE
tvhftrace("main", tsfile_done);
#endif
+ dvb_fastscan_done();
}
/******************************************************************************
diff --git a/src/input/mpegts.h b/src/input/mpegts.h
index 55f544d7..57a5c047 100644
--- a/src/input/mpegts.h
+++ b/src/input/mpegts.h
@@ -425,9 +425,9 @@ struct mpegts_service
* Fields defined by DVB standard EN 300 468
*/
- uint16_t s_dvb_service_id;
- uint16_t s_dvb_channel_num;
+ uint32_t s_dvb_channel_num;
uint16_t s_dvb_channel_minor;
+ uint16_t s_dvb_service_id;
char *s_dvb_svcname;
char *s_dvb_provider;
char *s_dvb_cridauth;
diff --git a/src/input/mpegts/dvb.h b/src/input/mpegts/dvb.h
index 2aee8a39..2f6a7337 100644
--- a/src/input/mpegts/dvb.h
+++ b/src/input/mpegts/dvb.h
@@ -59,6 +59,10 @@ struct mpegts_mux;
#define DVB_BAT_BASE 0x48
#define DVB_BAT_MASK 0xF8
+#define DVB_FASTSCAN_NIT_BASE 0xBC
+#define DVB_FASTSCAN_SDT_BASE 0xBD
+#define DVB_FASTSCAN_MASK 0xFF
+
#define DVB_VCT_T_BASE 0xC8
#define DVB_VCT_C_BASE 0xC9
#define DVB_VCT_MASK 0xFF
@@ -210,6 +214,8 @@ int dvb_nit_callback
(struct mpegts_table *mt, const uint8_t *ptr, int len, int tableid);
int dvb_bat_callback
(struct mpegts_table *mt, const uint8_t *ptr, int len, int tableid);
+int dvb_fs_sdt_callback
+ (struct mpegts_table *mt, const uint8_t *ptr, int len, int tableid);
int dvb_sdt_callback
(struct mpegts_table *mt, const uint8_t *ptr, int len, int tableid);
int dvb_tdt_callback
diff --git a/src/input/mpegts/dvb_psi.c b/src/input/mpegts/dvb_psi.c
index 67d35c92..8f8a7abd 100644
--- a/src/input/mpegts/dvb_psi.c
+++ b/src/input/mpegts/dvb_psi.c
@@ -24,6 +24,8 @@
#include "lang_codes.h"
#include "service.h"
#include "dvb_charset.h"
+#include "bouquet.h"
+#include "fastscan.h"
#include
#include
@@ -321,7 +323,7 @@ dvb_desc_service
static int
dvb_desc_service_list
- ( const char *dstr, const uint8_t *ptr, int len, mpegts_mux_t *mm )
+ ( const char *dstr, const uint8_t *ptr, int len, mpegts_mux_t *mm, bouquet_t *bq )
{
uint16_t stype, sid;
int i;
@@ -333,6 +335,8 @@ dvb_desc_service_list
if (mm) {
int save = 0;
s = mpegts_service_find(mm, sid, 0, 1, &save);
+ if (bq)
+ bouquet_add_service(bq, (service_t *)s);
if (save)
s->s_config_save((service_t*)s);
}
@@ -342,7 +346,7 @@ dvb_desc_service_list
static int
dvb_desc_local_channel
- ( const char *dstr, const uint8_t *ptr, int len, mpegts_mux_t *mm )
+ ( const char *dstr, const uint8_t *ptr, int len, mpegts_mux_t *mm, bouquet_t *bq )
{
int save = 0;
uint16_t sid, lcn;
@@ -354,6 +358,11 @@ dvb_desc_local_channel
if (lcn && mm) {
mpegts_service_t *s = mpegts_service_find(mm, sid, 0, 0, &save);
if (s) {
+ if (bq && bq->bq_lcn_offset &&
+ (!s->s_master_bouquet || s->s_master_bouquet == bq)) {
+ lcn += bq->bq_lcn_offset;
+ s->s_master_bouquet = bq;
+ }
if (s->s_dvb_channel_num != lcn) {
s->s_dvb_channel_num = lcn;
s->s_config_save((service_t*)s);
@@ -721,18 +730,20 @@ dvb_nit_callback
mpegts_network_t *mn = mm->mm_network;
char name[256], dauth[256];
mpegts_table_state_t *st = NULL;
+ bouquet_t *bq = NULL;
const char *charset;
/* Net/Bat ID */
nbid = (ptr[0] << 8) | ptr[1];
/* Begin */
- if (tableid != 0x40 && tableid != 0x41 && tableid != 0x4A) return -1;
+ if (tableid != 0x40 && tableid != 0x41 && tableid != 0x4A && tableid != 0xBC)
+ return -1;
r = dvb_table_begin(mt, ptr, len, tableid, nbid, 7, &st, §, &last, &ver);
if (r != 1) return r;
/* NIT */
- if (tableid != 0x4A) {
+ if (tableid != 0x4A && tableid != 0xBC /* fastscan */) {
/* Specific NID */
if (mn->mn_nid) {
@@ -764,8 +775,13 @@ dvb_nit_callback
}
}
+ /* Fastscan */
+ if (tableid == 0xBC) {
+ tvhdebug(mt->mt_name, "fastscan %04X (%d) [%s]", nbid, nbid, name);
+ bq = mt->mt_opaque;
+
/* BAT */
- if (tableid == 0x4A) {
+ } else if (tableid == 0x4A) {
tvhdebug(mt->mt_name, "bouquet %04X (%d) [%s]", nbid, nbid, name);
/* NIT */
@@ -780,7 +796,6 @@ dvb_nit_callback
DVB_LOOP_FOREACH(ptr, len, 0, lptr, llen, 6) {
tsid = (lptr[0] << 8) | lptr[1];
onid = (lptr[2] << 8) | lptr[3];
- tvhdebug(mt->mt_name, " onid %04X (%d) tsid %04X (%d)", onid, onid, tsid, tsid);
/* Find existing mux */
LIST_FOREACH(mux, &mn->mn_muxes, mm_network_link)
@@ -788,6 +803,33 @@ dvb_nit_callback
break;
charset = dvb_charset_find(mn, mux, NULL);
+#if ENABLE_MPEGTS_DVB
+ dauth[0] = 0;
+ if (mux && *name && tableid == 0x4A /* BAT */) {
+ if (idnode_is_instance(&mux->mm_id, &dvb_mux_dvbs_class)) {
+ dvb_mux_conf_t *mc = &((dvb_mux_t *)mux)->lm_tuning;
+ if (mc->u.dmc_fe_qpsk.orbital_dir) {
+ int pos = mc->u.dmc_fe_qpsk.orbital_pos;
+ if (mc->u.dmc_fe_qpsk.orbital_dir == 'W')
+ pos = -pos;
+ snprintf(dauth, sizeof(dauth), "dvb-bouquet://dvbs,%d/%s", pos, name);
+ }
+ } else if (idnode_is_instance(&mux->mm_id, &dvb_mux_dvbt_class)) {
+ snprintf(dauth, sizeof(dauth), "dvb-bouquet://dvbt/%s", name);
+ } else if (idnode_is_instance(&mux->mm_id, &dvb_mux_dvbc_class)) {
+ snprintf(dauth, sizeof(dauth), "dvb-bouquet://dvbc/%s", name);
+ }
+ }
+ if (dauth[0]) {
+ bouquet_t *bq2 = bouquet_find_by_source(name, dauth, 1);
+ if (bq2 != bq && bq && bq->bq_saveflag)
+ bouquet_save(bq, 1);
+ bq = bq2;
+ }
+#endif
+
+ tvhdebug(mt->mt_name, " onid %04X (%d) tsid %04X (%d) mux %p bq %p", onid, onid, tsid, tsid, mux, bq);
+
DVB_DESC_FOREACH(lptr, llen, 4, dlptr, dllen, dtag, dlen, dptr) {
tvhtrace(mt->mt_name, " dtag %02X dlen %d", dtag, dlen);
@@ -839,17 +881,20 @@ dvb_nit_callback
mpegts_mux_set_crid_authority(mux, dauth);
break;
case DVB_DESC_LOCAL_CHAN:
- if (dvb_desc_local_channel(mt->mt_name, dptr, dlen, mux))
+ if (dvb_desc_local_channel(mt->mt_name, dptr, dlen, mux, bq))
return -1;
break;
case DVB_DESC_SERVICE_LIST:
- if (dvb_desc_service_list(mt->mt_name, dptr, dlen, mux))
+ if (dvb_desc_service_list(mt->mt_name, dptr, dlen, mux, bq))
return -1;
break;
}
}
}
+ if (bq && bq->bq_saveflag)
+ bouquet_save(bq, 1);
+
/* End */
return dvb_table_end(mt, st, sect);
}
@@ -940,7 +985,7 @@ dvb_sdt_callback
int r;
s->s_dvb_servicetype = stype;
save = 1;
- tvhtrace("sdt", " type changed");
+ tvhtrace("sdt", " type changed (old %i)", s->s_dvb_servicetype);
/* Set tvh service type */
if ((r = dvb_servicetype_lookup(stype)) != -1)
@@ -1100,6 +1145,144 @@ dvb_bat_callback
return dvb_nit_callback(mt, ptr, len, tableid);
}
+#if ENABLE_MPEGTS_DVB
+/*
+ * DVB fastscan table processing
+ */
+int
+dvb_fs_sdt_callback
+ (mpegts_table_t *mt, const uint8_t *ptr, int len, int tableid)
+{
+ int r, sect, last, ver;
+ uint8_t dtag;
+ int llen, dlen;
+ const uint8_t *lptr, *dptr;
+ uint16_t nbid = 0, onid, tsid, service_id;
+ mpegts_mux_t *mm = mt->mt_mux, *mux;
+ mpegts_network_t *mn = mm->mm_network;
+ mpegts_table_state_t *st = NULL;
+ bouquet_t *bq = mt->mt_opaque;
+
+ /* Fastscan ID */
+ nbid = (ptr[0] << 8) | ptr[1];
+
+ /* Begin */
+ if (tableid != 0xBD)
+ return -1;
+ r = dvb_table_begin(mt, ptr, len, tableid, nbid, 7, &st, §, &last, &ver);
+ if (r != 1) return r;
+ if (len < 5) return -1;
+ ptr += 5;
+ len -= 5;
+
+ while (len > 0) {
+ const char *charset;
+ char sprov[256] = "", sname[256] = "";
+ mpegts_service_t *s;
+ int stype = 0, save = 0;
+
+ onid = (ptr[0] << 8) | ptr[1];
+ tsid = (ptr[2] << 8) | ptr[3];
+ service_id = (ptr[4] << 8) | ptr[5];
+ /* (ptr[6] << 8) | ptr[7] - video pid */
+ /* (ptr[7] << 8) | ptr[8] - audio pid */
+ /* (ptr[9] << 8) | ptr[10] - video ecm pid */
+ /* (ptr[10] << 8) | ptr[12] - audio ecm pid */
+ /* (ptr[14] << 8) | ptr[15] - pcr pid */
+
+ tvhdebug(mt->mt_name, " service %04X (%d) onid %04X (%d) tsid %04X (%d)",
+ service_id, service_id, onid, onid, tsid, tsid);
+
+ /* Initialise the loop */
+ DVB_LOOP_INIT(ptr, len, 16, lptr, llen);
+
+ /* Find service */
+ s = mpegts_service_find(mm, service_id, 0, 1, &save);
+ charset = dvb_charset_find(mn, mm, s);
+ if (bq && s)
+ bouquet_add_service(bq, (service_t *)s);
+
+ /* Descriptor loop */
+ DVB_DESC_EACH(lptr, llen, dtag, dlen, dptr) {
+ tvhtrace(mt->mt_name, " dtag %02X dlen %d", dtag, dlen);
+ switch (dtag) {
+ case DVB_DESC_SERVICE:
+ if (dvb_desc_service(dptr, dlen, &stype, sprov,
+ sizeof(sprov), sname, sizeof(sname), charset))
+ return -1;
+ break;
+ case DVB_DESC_LOCAL_CHAN:
+ /* Find existing mux */
+ LIST_FOREACH(mux, &mn->mn_muxes, mm_network_link)
+ if (mux->mm_onid == onid && mux->mm_tsid == tsid)
+ break;
+ if (dvb_desc_local_channel(mt->mt_name, dptr, dlen, mux, bq))
+ return -1;
+ break;
+ case DVB_DESC_SAT_DEL:
+ mux = dvb_desc_sat_del(mm, onid, tsid, dptr, dlen);
+ if (mux) {
+ mpegts_mux_set_onid(mux, onid);
+ mpegts_mux_set_tsid(mux, tsid, 0);
+ }
+ break;
+ }
+ }
+
+ tvhtrace(mt->mt_name, " type %d name [%s] provider [%s]",
+ stype, sname, sprov);
+
+ /* Update service type */
+ if (stype && !s->s_dvb_servicetype) {
+ int r;
+ s->s_dvb_servicetype = stype;
+ save = 1;
+ tvhtrace(mt->mt_name, " type changed");
+
+ /* Set tvh service type */
+ if ((r = dvb_servicetype_lookup(stype)) != -1)
+ s->s_servicetype = r;
+ }
+
+ /* Update name */
+ if (*sname && strcmp(s->s_dvb_svcname ?: "", sname)) {
+ if (!s->s_dvb_svcname) {
+ tvh_str_update(&s->s_dvb_svcname, sname);
+ save = 1;
+ tvhtrace(mt->mt_name, " name changed");
+ }
+ }
+
+ /* Update provider */
+ if (*sprov && strcmp(s->s_dvb_provider ?: "", sprov)) {
+ if (!s->s_dvb_provider) {
+ tvh_str_update(&s->s_dvb_provider, sprov);
+ save = 1;
+ tvhtrace(mt->mt_name, " provider changed");
+ }
+ }
+
+ if (save) {
+ /* Update nice name */
+ pthread_mutex_lock(&s->s_stream_mutex);
+ service_make_nicename((service_t*)s);
+ pthread_mutex_unlock(&s->s_stream_mutex);
+ tvhdebug(mt->mt_name, " nicename %s", s->s_nicename);
+ /* Save changes */
+ idnode_changed(&s->s_id);
+ service_refresh_channel((service_t*)s);
+ }
+ }
+
+ if (bq && bq->bq_saveflag)
+ bouquet_save(bq, 1);
+
+ /* End */
+ return dvb_table_end(mt, st, sect);
+
+}
+#endif
+
/**
* PMT update reason flags
*/
@@ -1589,6 +1772,18 @@ psi_tables_default ( mpegts_mux_t *mm )
NULL, "cat", MT_QUICKREQ | MT_CRC, DVB_CAT_PID);
}
+#if ENABLE_MPEGTS_DVB
+static void
+psi_tables_dvb_fastscan( void *aux, bouquet_t *bq, const char *name, int pid )
+{
+ tvhtrace("fastscan", "adding table %04X (%i) for '%s'", pid, pid, name);
+ mpegts_table_add(aux, DVB_FASTSCAN_NIT_BASE, DVB_FASTSCAN_MASK,
+ dvb_nit_callback, bq, "fs_nit", MT_CRC, pid);
+ mpegts_table_add(aux, DVB_FASTSCAN_SDT_BASE, DVB_FASTSCAN_MASK,
+ dvb_fs_sdt_callback, bq, "fs_sdt", MT_CRC, pid);
+}
+#endif
+
void
psi_tables_dvb ( mpegts_mux_t *mm )
{
@@ -1599,6 +1794,17 @@ psi_tables_dvb ( mpegts_mux_t *mm )
DVB_SDT_PID);
mpegts_table_add(mm, DVB_BAT_BASE, DVB_BAT_MASK, dvb_bat_callback,
NULL, "bat", MT_CRC, DVB_BAT_PID);
+#if ENABLE_MPEGTS_DVB
+ if (idnode_is_instance(&mm->mm_id, &dvb_mux_dvbs_class)) {
+ dvb_mux_conf_t *mc = &((dvb_mux_t *)mm)->lm_tuning;
+ if (mc->dmc_fe_type == DVB_TYPE_S) {
+ int pos = mc->u.dmc_fe_qpsk.orbital_pos;
+ if (mc->u.dmc_fe_qpsk.orbital_dir == 'W')
+ pos = -pos;
+ dvb_fastscan_each(mm, pos, mc->dmc_fe_freq, psi_tables_dvb_fastscan);
+ }
+ }
+#endif
}
void
diff --git a/src/input/mpegts/mpegts_network.c b/src/input/mpegts/mpegts_network.c
index b43d10f3..e32d008d 100644
--- a/src/input/mpegts/mpegts_network.c
+++ b/src/input/mpegts/mpegts_network.c
@@ -382,7 +382,7 @@ mpegts_network_set_network_name
{
char buf[256];
if (mn->mn_network_name) return 0;
- if (!name || !strcmp(name, mn->mn_network_name ?: ""))
+ if (!name || name[0] == '\0' || !strcmp(name, mn->mn_network_name ?: ""))
return 0;
tvh_str_update(&mn->mn_network_name, name);
mn->mn_display_name(mn, buf, sizeof(buf));
diff --git a/src/main.c b/src/main.c
index 6bbf105e..a193bab6 100644
--- a/src/main.c
+++ b/src/main.c
@@ -69,6 +69,7 @@
#include "libav.h"
#endif
#include "profile.h"
+#include "bouquet.h"
#ifdef PLATFORM_LINUX
#include
@@ -813,6 +814,8 @@ main(int argc, char **argv)
http_client_init(opt_user_agent);
esfilter_init();
+ bouquet_init();
+
service_init();
#if ENABLE_MPEGTS
@@ -821,6 +824,8 @@ main(int argc, char **argv)
channel_init();
+ bouquet_service_resolve();
+
subscription_init();
dvr_config_init();
@@ -917,6 +922,7 @@ main(int argc, char **argv)
tvhftrace("main", service_mapper_done);
tvhftrace("main", service_done);
tvhftrace("main", channel_done);
+ tvhftrace("main", bouquet_done);
tvhftrace("main", dvr_done);
tvhftrace("main", subscription_done);
tvhftrace("main", access_done);
diff --git a/src/service.c b/src/service.c
index 1edabe82..415748fc 100644
--- a/src/service.c
+++ b/src/service.c
@@ -46,6 +46,7 @@
#include "input.h"
#include "access.h"
#include "esfilter.h"
+#include "bouquet.h"
static void service_data_timeout(void *aux);
static void service_class_delete(struct idnode *self);
@@ -799,6 +800,8 @@ service_destroy(service_t *t, int delconf)
t->s_status = SERVICE_ZOMBIE;
+ bouquet_destroy_by_service(t);
+
TAILQ_INIT(&t->s_filt_components);
while((st = TAILQ_FIRST(&t->s_components)) != NULL)
service_stream_destroy(t, st);
diff --git a/src/service.h b/src/service.h
index 3097cb30..226a6251 100644
--- a/src/service.h
+++ b/src/service.h
@@ -443,6 +443,11 @@ typedef struct service {
int64_t s_current_pts;
+ /*
+ *
+ */
+ void *s_master_bouquet;
+
} service_t;
diff --git a/src/webui/static/app/cteditor.js b/src/webui/static/app/cteditor.js
index 79e2e96a..89560c34 100644
--- a/src/webui/static/app/cteditor.js
+++ b/src/webui/static/app/cteditor.js
@@ -6,10 +6,9 @@ tvheadend.cteditor = function(panel, index)
{
tvheadend.idnode_grid(panel, {
url: 'api/channeltag',
- comet: 'channeltag',
titleS: 'Channel Tag',
titleP: 'Channel Tags',
- iconCls: 'channelTags',
+ iconCls: 'channelTags',
tabIndex: index,
add: {
url: 'api/channeltag',
@@ -27,3 +26,31 @@ tvheadend.cteditor = function(panel, index)
return panel;
};
+
+/*
+ * Bouquet
+ */
+tvheadend.bouquet = function(panel, index)
+{
+ var list = 'enabled,name,source,services_count,comment,lcn_off';
+
+ tvheadend.idnode_grid(panel, {
+ url: 'api/bouquet',
+ titleS: 'Bouquet',
+ titleP: 'Bouquets',
+ iconCls: 'bouquets',
+ tabIndex: index,
+ list: list,
+ del: true,
+ edit: { params: { list: list } },
+ sort: {
+ field: 'name',
+ direction: 'ASC'
+ },
+ help: function() {
+ new tvheadend.help('Bouquets', 'config_bouquet.html');
+ },
+ });
+
+ return panel;
+};
diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js
index 2348fd5c..08834e8e 100644
--- a/src/webui/static/app/tvheadend.js
+++ b/src/webui/static/app/tvheadend.js
@@ -366,6 +366,7 @@ function accessUpdate(o) {
});
tvheadend.channel_tab(chepg);
tvheadend.cteditor(chepg);
+ tvheadend.bouquet(chepg);
tvheadend.epggrab(chepg);
cp.add(chepg);