src/avg.c \
src/htsstr.c \
src/rawtsinput.c \
+ src/iptv_input.c \
SRCS += src/dvr/dvr_db.c \
src/dvr/dvr_rec.c \
* The HTSP service is now announced via AVAHI (mDNS service discovery)
+ * Support for IPTV has been added.
+
hts-tvheadend (2.4) hts; urgency=low
* Due to a bug, the polarisation of DVB-S muxes was not correctly
RB_REMOVE(&channel_name_tree, ch, ch_name_link);
channel_set_name(ch, newname);
- LIST_FOREACH(t, &ch->ch_transports, tht_ch_link) {
- pthread_mutex_lock(&t->tht_stream_mutex);
- t->tht_config_change(t);
- pthread_mutex_unlock(&t->tht_stream_mutex);
- }
+ LIST_FOREACH(t, &ch->ch_transports, tht_ch_link)
+ t->tht_config_save(t);
channel_save(ch);
htsp_channel_update(ch);
assert(tdmi_dst != NULL);
- LIST_FOREACH(t_src, &tdmi_src->tdmi_transports, tht_mux_link) {
+ LIST_FOREACH(t_src, &tdmi_src->tdmi_transports, tht_group_link) {
t_dst = dvb_transport_find(tdmi_dst,
t_src->tht_dvb_service_id,
t_src->tht_pmt_pid, NULL);
st_dst->st_caid = st_src->st_caid;
}
- t_dst->tht_config_change(t_dst); // Save config
pthread_mutex_unlock(&t_src->tht_stream_mutex);
+ t_dst->tht_config_save(t_dst); // Save config
}
dvb_mux_save(tdmi_dst);
// XXX: bad bad bad slow slow slow
LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) {
nummux++;
- LIST_FOREACH(t, &tdmi->tdmi_transports, tht_mux_link) {
+ LIST_FOREACH(t, &tdmi->tdmi_transports, tht_group_link) {
numsvc++;
}
}
free((void *)t->tht_svcname);
t->tht_svcname = strdup(chname);
- pthread_mutex_lock(&t->tht_stream_mutex);
- t->tht_config_change(t);
- pthread_mutex_unlock(&t->tht_stream_mutex);
+ t->tht_config_save(t);
}
}
break;
st->st_demuxer_fd = -1;
tvhlog(LOG_ERR, "dvb",
"\"%s\" unable to open demuxer \"%s\" for pid %d -- %s",
- t->tht_name, tda->tda_demux_path, st->st_pid, strerror(errno));
+ t->tht_identifier, tda->tda_demux_path,
+ st->st_pid, strerror(errno));
continue;
}
if(ioctl(fd, DMX_SET_PES_FILTER, &dmx_param)) {
tvhlog(LOG_ERR, "dvb",
"\"%s\" unable to configure demuxer \"%s\" for pid %d -- %s",
- t->tht_name, tda->tda_demux_path, st->st_pid, strerror(errno));
+ t->tht_identifier, tda->tda_demux_path,
+ st->st_pid, strerror(errno));
close(fd);
fd = -1;
}
htsmsg_add_u32(m, "mapped", 1);
}
+ pthread_mutex_lock(&t->tht_stream_mutex);
psi_save_transport_settings(m, t);
+ pthread_mutex_unlock(&t->tht_stream_mutex);
hts_settings_save(m, "dvbtransports/%s/%s",
t->tht_dvb_mux_instance->tdmi_identifier,
lock_assert(&global_lock);
- LIST_FOREACH(t, &tdmi->tdmi_transports, tht_mux_link) {
+ LIST_FOREACH(t, &tdmi->tdmi_transports, tht_group_link) {
if(t->tht_dvb_service_id == sid)
return t;
}
t->tht_start_feed = dvb_transport_start;
t->tht_refresh_feed = dvb_transport_refresh;
t->tht_stop_feed = dvb_transport_stop;
- t->tht_config_change = dvb_transport_save;
+ t->tht_config_save = dvb_transport_save;
t->tht_sourceinfo = dvb_transport_sourceinfo;
t->tht_dvb_mux_instance = tdmi;
t->tht_quality_index = dvb_transport_quality;
- LIST_INSERT_HEAD(&tdmi->tdmi_transports, t, tht_mux_link);
+ LIST_INSERT_HEAD(&tdmi->tdmi_transports, t, tht_group_link);
dvb_adapter_notify(tdmi->tdmi_adapter);
return t;
#include <pthread.h>
#include <sys/types.h>
-#include <sys/stat.h>
#include <sys/ioctl.h>
+#include <sys/epoll.h>
#include <fcntl.h>
+#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>
#include <linux/netdevice.h>
-#include <syslog.h>
-#include <libhts/htscfg.h>
+#include <libavutil/avstring.h>
#include "tvhead.h"
-#include "iptv_input.h"
+#include "htsmsg.h"
#include "channels.h"
#include "transports.h"
-#include "dispatch.h"
-#include "psi.h"
+#include "iptv_input.h"
#include "tsdemux.h"
+#include "psi.h"
+#include "settings.h"
+
+static int iptv_thread_running;
+static int iptv_epollfd;
+static pthread_mutex_t iptv_recvmutex;
+
+struct th_transport_list iptv_all_transports; /* All IPTV transports */
+static struct th_transport_list iptv_active_transports; /* Currently enabled */
+
+/**
+ * PAT parser. We only parse a single program. CRC has already been verified
+ */
+static void
+iptv_got_pat(const uint8_t *ptr, int len, void *aux)
+{
+ th_transport_t *t = aux;
+ uint16_t prognum, pmt;
+
+ len -= 8;
+ ptr += 8;
+
+ if(len < 4)
+ return;
+
+ prognum = ptr[0] << 8 | ptr[1];
+ pmt = (ptr[2] & 0x1f) << 8 | ptr[3];
+
+ t->tht_pmt_pid = pmt;
+}
-struct th_transport_list iptv_probing_transports;
-struct th_transport_list iptv_stale_transports;
-static dtimer_t iptv_probe_timer;
-static void iptv_probe_transport(th_transport_t *t);
-static void iptv_probe_callback(void *aux, int64_t now);
-static void iptv_probe_done(th_transport_t *t, int timeout);
+/**
+ * PMT parser. CRC has already been verified
+ */
+static void
+iptv_got_pmt(const uint8_t *ptr, int len, void *aux)
+{
+ th_transport_t *t = aux;
+
+ if(len < 3 || ptr[0] != 2)
+ return;
+
+ pthread_mutex_lock(&t->tht_stream_mutex);
+ psi_parse_pmt(t, ptr + 3, len - 3, 0, 1);
+ pthread_mutex_unlock(&t->tht_stream_mutex);
+}
+
+/**
+ * Handle a single TS packet for the given IPTV transport
+ */
static void
-iptv_fd_callback(int events, void *opaque, int fd)
+iptv_ts_input(th_transport_t *t, uint8_t *tsb)
{
- th_transport_t *t = opaque;
- uint8_t buf[2000];
- int r;
- uint8_t *tsb = buf;
+ uint16_t pid = ((tsb[1] & 0x1f) << 8) | tsb[2];
+
+ if(pid == 0) {
+
+ if(t->tht_pat_section == NULL)
+ t->tht_pat_section = calloc(1, sizeof(psi_section_t));
+ psi_rawts_table_parser(t->tht_pat_section, tsb, iptv_got_pat, t);
+
+ } else if(pid == t->tht_pmt_pid) {
- r = read(fd, buf, sizeof(buf));
+ if(t->tht_pmt_section == NULL)
+ t->tht_pmt_section = calloc(1, sizeof(psi_section_t));
+ psi_rawts_table_parser(t->tht_pmt_section, tsb, iptv_got_pmt, t);
- while(r >= 188) {
+ } else {
ts_recv_packet1(t, tsb);
- r -= 188;
- tsb += 188;
+ }
+}
+
+
+/**
+ * Main epoll() based input thread for IPTV
+ */
+static void *
+iptv_thread(void *aux)
+{
+ int nfds, fd, r, j;
+ uint8_t tsb[2048];
+ th_transport_t *t;
+ struct epoll_event ev;
+
+ while(1) {
+ nfds = epoll_wait(iptv_epollfd, &ev, 1, -1);
+ if(nfds == -1) {
+ tvhlog(LOG_ERR, "IPTV", "epoll() error -- %s, sleeping 1 second",
+ strerror(errno));
+ sleep(1);
+ continue;
+ }
+
+ if(nfds < 1)
+ continue;
+
+ fd = ev.data.fd;
+ r = read(fd, tsb, 2048);
+
+ // Add RTP support here
+
+ if((r % 188) != 0)
+ continue; // We expect multiples of a TS packet
+
+ pthread_mutex_lock(&iptv_recvmutex);
+
+ LIST_FOREACH(t, &iptv_active_transports, tht_active_link) {
+ if(t->tht_iptv_fd != fd)
+ continue;
+
+ for(j = 0; j < r; j += 188)
+ iptv_ts_input(t, tsb + j);
+ }
+ pthread_mutex_unlock(&iptv_recvmutex);
}
}
+
+/**
+ *
+ */
static int
-iptv_start_feed(th_transport_t *t, unsigned int weight, int status, int force)
+iptv_transport_start(th_transport_t *t, unsigned int weight, int status,
+ int force_start)
{
+ pthread_t tid;
int fd;
struct ip_mreqn m;
struct sockaddr_in sin;
+ struct ifreq ifr;
+ struct epoll_event ev;
+
+ assert(t->tht_iptv_fd == -1);
+
+ if(iptv_thread_running == 0) {
+ iptv_thread_running = 1;
+ iptv_epollfd = epoll_create(10);
+ pthread_create(&tid, NULL, iptv_thread, NULL);
+ }
+
+ /* Now, open the real socket for UDP */
fd = socket(AF_INET, SOCK_DGRAM, 0);
- if(fd == -1)
+ if(fd == -1) {
+ tvhlog(LOG_ERR, "IPTV", "\"%s\" cannot open socket", t->tht_identifier);
return -1;
+ }
+
+ /* First, resolve interface name */
+ memset(&ifr, 0, sizeof(ifr));
+ av_strlcpy(ifr.ifr_name, t->tht_iptv_iface, IFNAMSIZ);
+ ifr.ifr_name[IFNAMSIZ - 1] = 0;
+ if(ioctl(fd, SIOCGIFINDEX, &ifr)) {
+ tvhlog(LOG_ERR, "IPTV", "\"%s\" cannot find interface %s",
+ t->tht_identifier, t->tht_iptv_iface);
+ close(fd);
+ return -1;
+ }
+ /* Bind to multicast group */
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(t->tht_iptv_port);
- sin.sin_addr.s_addr = t->tht_iptv_group_addr.s_addr;
+ sin.sin_addr.s_addr = t->tht_iptv_group.s_addr;
if(bind(fd, (struct sockaddr *)&sin, sizeof(sin)) == -1) {
- syslog(LOG_ERR, "iptv: \"%s\" cannot bind %s:%d -- %s",
- t->tht_name, inet_ntoa(sin.sin_addr), t->tht_iptv_port,
+ tvhlog(LOG_ERR, "IPTV", "\"%s\" cannot bind %s:%d -- %s",
+ t->tht_identifier, inet_ntoa(sin.sin_addr), t->tht_iptv_port,
strerror(errno));
close(fd);
return -1;
}
+ /* Join group */
memset(&m, 0, sizeof(m));
- m.imr_multiaddr.s_addr = t->tht_iptv_group_addr.s_addr;
- m.imr_address.s_addr = t->tht_iptv_interface_addr.s_addr;
- m.imr_ifindex = t->tht_iptv_ifindex;
+ m.imr_multiaddr.s_addr = t->tht_iptv_group.s_addr;
+ m.imr_address.s_addr = 0;
+ m.imr_ifindex = ifr.ifr_ifindex;
if(setsockopt(fd, SOL_IP, IP_ADD_MEMBERSHIP, &m,
sizeof(struct ip_mreqn)) == -1) {
- syslog(LOG_ERR, "iptv: \"%s\" cannot join %s -- %s",
- t->tht_name, inet_ntoa(m.imr_multiaddr),
- strerror(errno));
+ tvhlog(LOG_ERR, "IPTV", "\"%s\" cannot join %s -- %s",
+ t->tht_identifier, inet_ntoa(m.imr_multiaddr), strerror(errno));
close(fd);
return -1;
}
- t->tht_iptv_fd = fd;
- t->tht_runstatus = status;
+ memset(&ev, 0, sizeof(ev));
+ ev.events = EPOLLIN;
+ ev.data.fd = fd;
+ if(epoll_ctl(iptv_epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
+ tvhlog(LOG_ERR, "IPTV", "\"%s\" cannot add to epoll set -- %s",
+ t->tht_identifier, strerror(errno));
+ close(fd);
+ return -1;
+ }
- syslog(LOG_ERR, "iptv: \"%s\" joined group", t->tht_name);
+ t->tht_iptv_fd = fd;
+ t->tht_status = status;
- t->tht_iptv_dispatch_handle = dispatch_addfd(fd, iptv_fd_callback, t,
- DISPATCH_READ);
+ pthread_mutex_lock(&iptv_recvmutex);
+ LIST_INSERT_HEAD(&iptv_active_transports, t, tht_active_link);
+ pthread_mutex_unlock(&iptv_recvmutex);
return 0;
}
+
+/**
+ *
+ */
static void
-iptv_stop_feed(th_transport_t *t)
+iptv_transport_refresh(th_transport_t *t)
{
- if(t->tht_runstatus == TRANSPORT_IDLE)
- return;
-
- t->tht_runstatus = TRANSPORT_IDLE;
- dispatch_delfd(t->tht_iptv_dispatch_handle);
- close(t->tht_iptv_fd);
- syslog(LOG_ERR, "iptv: \"%s\" left group", t->tht_name);
}
-/*
+/**
*
*/
-
static void
-iptv_parse_pmt(struct th_transport *t, th_stream_t *st,
- uint8_t *table, int table_len)
+iptv_transport_stop(th_transport_t *t)
{
- if(table[0] != 2 || t->tht_runstatus != TRANSPORT_PROBING)
- return;
+ t->tht_status = TRANSPORT_IDLE;
+ pthread_mutex_lock(&iptv_recvmutex);
+ LIST_REMOVE(t, tht_active_link);
+ pthread_mutex_unlock(&iptv_recvmutex);
+
+ assert(t->tht_iptv_fd >= 0);
- psi_parse_pmt(t, table + 3, table_len - 3, 0);
+ close(t->tht_iptv_fd); // Automatically removes fd from epoll set
- iptv_probe_done(t, 0);
+ t->tht_iptv_fd = -1;
}
-/*
+/**
*
*/
-
static void
-iptv_parse_pat(struct th_transport *t, th_stream_t *st,
- uint8_t *table, int table_len)
+iptv_transport_save(th_transport_t *t)
{
- if(table[0] != 0 || t->tht_runstatus != TRANSPORT_PROBING)
- return;
+ htsmsg_t *m = htsmsg_create_map();
+ char abuf[INET_ADDRSTRLEN];
- psi_parse_pat(t, table + 3, table_len - 3, iptv_parse_pmt);
-}
+ lock_assert(&global_lock);
-/*
- *
- */
-
-int
-iptv_configure_transport(th_transport_t *t, const char *iptv_type,
- struct config_head *head, const char *channel_name)
-{
- const char *s;
- int fd;
- char buf[100];
- char ifname[100];
- struct ifreq ifr;
- th_stream_t *st;
-
- if(!strcasecmp(iptv_type, "rawudp"))
- t->tht_iptv_mode = IPTV_MODE_RAWUDP;
- else
- return -1;
+ htsmsg_add_u32(m, "pmt", t->tht_pmt_pid);
- t->tht_type = TRANSPORT_IPTV;
- t->tht_start_feed = iptv_start_feed;
- t->tht_stop_feed = iptv_stop_feed;
+ if(t->tht_iptv_port)
+ htsmsg_add_u32(m, "port", t->tht_iptv_port);
- if((s = config_get_str_sub(head, "group-address", NULL)) == NULL)
- return -1;
- t->tht_iptv_group_addr.s_addr = inet_addr(s);
+ if(t->tht_iptv_iface)
+ htsmsg_add_str(m, "interface", t->tht_iptv_iface);
- t->tht_iptv_ifindex = 0;
+ if(t->tht_iptv_group.s_addr) {
+ inet_ntop(AF_INET, &t->tht_iptv_group, abuf, sizeof(abuf));
+ htsmsg_add_str(m, "group", abuf);
+ }
- if((s = config_get_str_sub(head, "interface-address", NULL)) != NULL)
- t->tht_iptv_interface_addr.s_addr = inet_addr(s);
- else
- t->tht_iptv_interface_addr.s_addr = INADDR_ANY;
+ if(t->tht_ch != NULL) {
+ htsmsg_add_str(m, "channelname", t->tht_ch->ch_name);
+ htsmsg_add_u32(m, "mapped", 1);
+ }
- snprintf(ifname, sizeof(ifname), "%s",
- inet_ntoa(t->tht_iptv_interface_addr));
+ pthread_mutex_lock(&t->tht_stream_mutex);
+ psi_save_transport_settings(m, t);
+ pthread_mutex_unlock(&t->tht_stream_mutex);
+
+ hts_settings_save(m, "iptvtransports/%s",
+ t->tht_identifier);
- if((s = config_get_str_sub(head, "interface", NULL)) != NULL) {
+ htsmsg_destroy(m);
+}
- memset(&ifr, 0, sizeof(ifr));
- strncpy(ifr.ifr_name, s, IFNAMSIZ - 1);
- ifr.ifr_name[IFNAMSIZ - 1] = 0;
-
- fd = socket(PF_INET,SOCK_STREAM,0);
- if(fd != -1) {
- if(ioctl(fd, SIOCGIFINDEX, &ifr) == 0) {
- t->tht_iptv_ifindex = ifr.ifr_ifindex;
- snprintf(ifname, sizeof(ifname), "%s", s);
- }
- close(fd);
- }
- }
- if((s = config_get_str_sub(head, "port", NULL)) == NULL)
- return -1;
- t->tht_iptv_port = atoi(s);
+/**
+ *
+ */
+static int
+iptv_transport_quality(th_transport_t *t)
+{
+ if(t->tht_iptv_iface == NULL ||
+ t->tht_iptv_group.s_addr == 0 ||
+ t->tht_iptv_port == 0)
+ return 0;
- snprintf(buf, sizeof(buf), "IPTV: %s (%s:%s:%d)", channel_name,
- ifname, inet_ntoa(t->tht_iptv_group_addr), t->tht_iptv_port);
- t->tht_name = strdup(buf);
+ return 100;
+}
- st = transport_add_stream(t, 0, HTSTV_PAT);
- st->st_got_section = iptv_parse_pat;
- st->st_section_docrc = 1;
- s = config_get_str_sub(head, "provider", NULL);
- if(s != NULL)
- t->tht_provider = strdup(s);
- else
- t->tht_provider = strdup("IPTV");
+/**
+ * Generate a descriptive name for the source
+ */
+static htsmsg_t *
+iptv_transport_sourceinfo(th_transport_t *t)
+{
+ htsmsg_t *m = htsmsg_create_map();
- snprintf(buf, sizeof(buf), "iptv_%s_%d",
- inet_ntoa(t->tht_iptv_group_addr), t->tht_iptv_port);
- t->tht_identifier = strdup(buf);
+ if(t->tht_iptv_iface != NULL)
+ htsmsg_add_str(m, "adapter", t->tht_iptv_iface);
+ htsmsg_add_str(m, "mux", inet_ntoa(t->tht_iptv_group));
+ return m;
+}
- t->tht_chname = strdup(channel_name);
- LIST_INSERT_HEAD(&iptv_probing_transports, t, tht_active_link);
- startupcounter++;
+/**
+ *
+ */
+th_transport_t *
+iptv_transport_find(const char *id, int create)
+{
+ static int tally;
+ th_transport_t *t;
+ char buf[20];
- if(!dtimer_isarmed(&iptv_probe_timer)) {
- iptv_probe_transport(t);
- dtimer_arm(&iptv_probe_timer, iptv_probe_callback, t, 5);
+ if(id != NULL) {
+
+ if(strncmp(id, "iptv_", 5))
+ return NULL;
+
+ LIST_FOREACH(t, &iptv_all_transports, tht_group_link)
+ if(!strcmp(t->tht_identifier, id))
+ return t;
}
- return 0;
-}
+ if(create == 0)
+ return NULL;
+
+ if(id == NULL) {
+ tally++;
+ snprintf(buf, sizeof(buf), "iptv_%d", tally);
+ id = buf;
+ } else {
+ tally = MAX(atoi(id + 5), tally);
+ }
-static void
-iptv_probe_transport(th_transport_t *t)
-{
- syslog(LOG_INFO, "iptv: Probing transport %s", t->tht_name);
- iptv_start_feed(t, 1, TRANSPORT_PROBING, 1);
+ t = transport_create(id, TRANSPORT_IPTV, THT_MPEG_TS);
+
+ t->tht_start_feed = iptv_transport_start;
+ t->tht_refresh_feed = iptv_transport_refresh;
+ t->tht_stop_feed = iptv_transport_stop;
+ t->tht_config_save = iptv_transport_save;
+ t->tht_sourceinfo = iptv_transport_sourceinfo;
+ t->tht_quality_index = iptv_transport_quality;
+ t->tht_iptv_fd = -1;
+
+ LIST_INSERT_HEAD(&iptv_all_transports, t, tht_group_link);
+
+ return t;
}
+/**
+ * Load config for the given mux
+ */
static void
-iptv_probe_done(th_transport_t *t, int timeout)
+iptv_transport_load(void)
{
- int pidcnt = 0;
- th_stream_t *st;
-
- startupcounter--;
+ htsmsg_t *l, *c;
+ htsmsg_field_t *f;
+ uint32_t pmt;
+ const char *s;
+ unsigned int u32;
+ th_transport_t *t;
- dtimer_disarm(&iptv_probe_timer);
+ lock_assert(&global_lock);
- LIST_FOREACH(st, &t->tht_streams, st_link)
- pidcnt++;
+ if((l = hts_settings_load("iptvtransports")) == NULL)
+ return;
- LIST_REMOVE(t, tht_active_link);
+ HTSMSG_FOREACH(f, l) {
+ if((c = htsmsg_get_map_by_field(f)) == NULL)
+ continue;
- syslog(LOG_INFO, "iptv: Transport %s probed, %d pids found%s",
- t->tht_name, pidcnt, timeout ? ", but probe timeouted" : "");
-
- iptv_stop_feed(t);
+ if(htsmsg_get_u32(c, "pmt", &pmt))
+ continue;
+
+ t = iptv_transport_find(f->hmf_name, 1);
+ t->tht_pmt_pid = pmt;
- if(!timeout)
- transport_map_channel(t, NULL);
- else
- LIST_INSERT_HEAD(&iptv_stale_transports, t, tht_active_link);
+ tvh_str_update(&t->tht_iptv_iface, htsmsg_get_str(c, "interface"));
- t = LIST_FIRST(&iptv_probing_transports);
- if(t == NULL)
- return;
+ if((s = htsmsg_get_str(c, "group")) != NULL)
+ inet_pton(AF_INET, s, &t->tht_iptv_group.s_addr);
+
+ if(!htsmsg_get_u32(c, "port", &u32))
+ t->tht_iptv_port = u32;
- iptv_probe_transport(t);
- dtimer_arm(&iptv_probe_timer, iptv_probe_callback, t, 5);
+ pthread_mutex_lock(&t->tht_stream_mutex);
+ psi_load_transport_settings(c, t);
+ pthread_mutex_unlock(&t->tht_stream_mutex);
+
+ s = htsmsg_get_str(c, "channelname");
+ if(htsmsg_get_u32(c, "mapped", &u32))
+ u32 = 0;
+
+ if(s && u32)
+ transport_map_channel(t, channel_find_by_name(s, 1), 0);
+ }
+ htsmsg_destroy(l);
}
-
-static void
-iptv_probe_callback(void *aux, int64_t now)
+/**
+ *
+ */
+void
+iptv_input_init(void)
{
- th_transport_t *t = aux;
- iptv_probe_done(t, 1);
+ pthread_mutex_init(&iptv_recvmutex, NULL);
+ iptv_transport_load();
}
#ifndef IPTV_INPUT_H_
#define IPTV_INPUT_H_
-int iptv_configure_transport(th_transport_t *t, const char *muxname,
- struct config_head *head,
- const char *channel_name);
+void iptv_input_init(void);
-extern struct th_transport_list iptv_probing_transports;
-extern struct th_transport_list iptv_stale_transports;
+th_transport_t *iptv_transport_find(const char *id, int create);
+
+extern struct th_transport_list iptv_all_transports;
#endif /* IPTV_INPUT_H_ */
#include "htsp.h"
#include "rawtsinput.h"
#include "avahi.h"
+#include "iptv_input.h"
+#include "transports.h"
#include "parachute.h"
#include "settings.h"
xmltv_init(); /* Must be initialized before channels */
+ transport_init();
+
channels_init();
access_init(createdefault);
dvb_init();
+ iptv_input_init();
+
http_server_init();
webui_init(contentpath);
/**
*
*/
-void
+int
tvh_str_update(char **strp, const char *src)
{
if(src == NULL)
- return;
+ return 0;
free(*strp);
*strp = strdup(src);
+ return 1;
}
char lang[4];
int frameduration;
int update = 0;
+ int had_components;
if(len < 9)
return -1;
lock_assert(&t->tht_stream_mutex);
+ had_components = !!LIST_FIRST(&t->tht_components);
+
sid = ptr[0] << 8 | ptr[1];
if((ptr[2] & 1) == 0) {
}
if(update) {
- t->tht_config_change(t);
+ transport_request_save(t);
if(t->tht_status == TRANSPORT_RUNNING)
- transport_restart(t);
+ transport_restart(t, had_components);
}
return 0;
}
char tmp[200];
- LIST_FOREACH(t, &rt->rt_transports, tht_mux_link) {
+ LIST_FOREACH(t, &rt->rt_transports, tht_group_link) {
if(t->tht_dvb_service_id == sid)
return t;
}
t->tht_start_feed = rawts_transport_start;
t->tht_stop_feed = rawts_transport_stop;
- t->tht_config_change = rawts_transport_save;
+ t->tht_config_save = rawts_transport_save;
t->tht_sourceinfo = rawts_transport_sourceinfo;
t->tht_quality_index = rawts_transport_quality;
tvhlog(LOG_NOTICE, "rawts", "Added service %d (pmt: %d)", sid, pmt_pid);
- LIST_INSERT_HEAD(&rt->rt_transports, t, tht_mux_link);
+ LIST_INSERT_HEAD(&rt->rt_transports, t, tht_group_link);
ch = channel_find_by_name(tmp, 1);
return;
}
- LIST_FOREACH(t, &rt->rt_transports, tht_mux_link)
+ LIST_FOREACH(t, &rt->rt_transports, tht_group_link)
ts_recv_packet1(t, tsb);
}
// Link to transport output
streaming_target_connect(&t->tht_streaming_pad, &s->ths_input);
- // Send a START message to the subscription client
- sm = streaming_msg_create_msg(SMT_START,
- transport_build_stream_start_msg(t));
+ if(LIST_FIRST(&t->tht_components) != NULL) {
- streaming_target_deliver(s->ths_output, sm);
+ // Send a START message to the subscription client
+ sm = streaming_msg_create_msg(SMT_START,
+ transport_build_stream_start_msg(t));
- // Send a TRANSPORT_STATUS message to the subscription client
- if(t->tht_feed_status != TRANSPORT_FEED_UNKNOWN) {
- sm = streaming_msg_create_code(SMT_TRANSPORT_STATUS, t->tht_feed_status);
streaming_target_deliver(s->ths_output, sm);
- }
+ // Send a TRANSPORT_STATUS message to the subscription client
+ if(t->tht_feed_status != TRANSPORT_FEED_UNKNOWN) {
+ sm = streaming_msg_create_code(SMT_TRANSPORT_STATUS, t->tht_feed_status);
+ streaming_target_deliver(s->ths_output, sm);
+ }
+ }
pthread_mutex_unlock(&t->tht_stream_mutex);
}
// Unlink from transport output
streaming_target_disconnect(&t->tht_streaming_pad, &s->ths_input);
- // Send a STOP message to the subscription client
- sm = streaming_msg_create_msg(SMT_STOP, htsmsg_create_map());
- streaming_target_deliver(s->ths_output, sm);
-
+ if(LIST_FIRST(&t->tht_components) != NULL) {
+ // Send a STOP message to the subscription client
+ sm = streaming_msg_create_msg(SMT_STOP, htsmsg_create_map());
+ streaming_target_deliver(s->ths_output, sm);
+ }
+
pthread_mutex_unlock(&t->tht_stream_mutex);
LIST_REMOVE(s, ths_transport_link);
subscription_unlink_transport(s);
}
- free((void *)t->tht_name);
-
if(t->tht_ch != NULL) {
t->tht_ch = NULL;
LIST_REMOVE(t, tht_ch_link);
}
- LIST_REMOVE(t, tht_mux_link);
+ LIST_REMOVE(t, tht_group_link);
LIST_REMOVE(t, tht_hash_link);
if(t->tht_status != TRANSPORT_IDLE)
free(st);
}
+ free(t->tht_pat_section);
+ free(t->tht_pmt_section);
+
transport_unref(t);
}
st->st_pid = pid;
st->st_demuxer_fd = -1;
+ st->st_tb = (AVRational){1, 90000};
TAILQ_INIT(&st->st_ptsq);
TAILQ_INIT(&st->st_durationq);
LIST_INSERT_HEAD(&ch->ch_transports, t, tht_ch_link);
}
- if(!save)
- return;
-
- pthread_mutex_lock(&t->tht_stream_mutex);
- t->tht_config_change(t); // Save config
- pthread_mutex_unlock(&t->tht_stream_mutex);
+ if(save)
+ t->tht_config_save(t);
}
* (i.e. an AC3 stream disappears, etc)
*/
void
-transport_restart(th_transport_t *t)
+transport_restart(th_transport_t *t, int had_components)
{
streaming_message_t *sm;
lock_assert(&t->tht_stream_mutex);
- sm = streaming_msg_create_msg(SMT_STOP, htsmsg_create_map());
- streaming_pad_deliver(&t->tht_streaming_pad, sm);
+ if(had_components) {
+ sm = streaming_msg_create_msg(SMT_STOP, htsmsg_create_map());
+ streaming_pad_deliver(&t->tht_streaming_pad, sm);
+ }
t->tht_refresh_feed(t);
- sm = streaming_msg_create_msg(SMT_START,
- transport_build_stream_start_msg(t));
- streaming_pad_deliver(&t->tht_streaming_pad, sm);
+ if(LIST_FIRST(&t->tht_components) != NULL) {
+
+ sm = streaming_msg_create_msg(SMT_START,
+ transport_build_stream_start_msg(t));
+ streaming_pad_deliver(&t->tht_streaming_pad, sm);
+ }
}
return;
t->tht_enabled = enabled;
+ t->tht_config_save(t);
+}
- pthread_mutex_lock(&t->tht_stream_mutex);
- t->tht_config_change(t); // Save config
- pthread_mutex_unlock(&t->tht_stream_mutex);
+
+static pthread_mutex_t pending_save_mutex;
+static pthread_cond_t pending_save_cond;
+static struct th_transport_queue pending_save_queue;
+
+/**
+ *
+ */
+void
+transport_request_save(th_transport_t *t)
+{
+ pthread_mutex_lock(&pending_save_mutex);
+
+ if(!t->tht_ps_onqueue) {
+ t->tht_ps_onqueue = 1;
+ TAILQ_INSERT_TAIL(&pending_save_queue, t, tht_ps_link);
+ transport_ref(t);
+ pthread_cond_signal(&pending_save_cond);
+ }
+
+ pthread_mutex_unlock(&pending_save_mutex);
+}
+
+
+/**
+ *
+ */
+static void *
+transport_saver(void *aux)
+{
+ th_transport_t *t;
+
+ pthread_mutex_lock(&pending_save_mutex);
+
+ while(1) {
+
+ if((t = TAILQ_FIRST(&pending_save_queue)) == NULL) {
+ pthread_cond_wait(&pending_save_cond, &pending_save_mutex);
+ continue;
+ }
+
+ TAILQ_REMOVE(&pending_save_queue, t, tht_ps_link);
+ t->tht_ps_onqueue = 0;
+
+ pthread_mutex_unlock(&pending_save_mutex);
+ pthread_mutex_lock(&global_lock);
+
+ if(t->tht_status != TRANSPORT_ZOMBIE)
+ t->tht_config_save(t);
+
+ transport_unref(t);
+
+ pthread_mutex_unlock(&global_lock);
+ pthread_mutex_lock(&pending_save_mutex);
+ }
+}
+
+
+/**
+ *
+ */
+void
+transport_init(void)
+{
+ pthread_t tid;
+ TAILQ_INIT(&pending_save_queue);
+ pthread_mutex_init(&pending_save_mutex, NULL);
+ pthread_cond_init(&pending_save_cond, NULL);
+ pthread_create(&tid, NULL, transport_saver, NULL);
}
#include "htsmsg.h"
#include "subscriptions.h"
+void transport_init(void);
+
unsigned int transport_compute_weight(struct th_transport_list *head);
int transport_start(th_transport_t *t, unsigned int weight, int force_start);
void transport_set_enable(th_transport_t *t, int enabled);
-void transport_restart(th_transport_t *t);
+void transport_restart(th_transport_t *t, int had_components);
void transport_stream_destroy(th_transport_t *t, th_stream_t *st);
+void transport_request_save(th_transport_t *t);
+
#endif /* TRANSPORTS_H */
*/
typedef struct th_transport {
- const char *tht_name;
-
LIST_ENTRY(th_transport) tht_hash_link;
enum {
int tht_enabled;
- LIST_ENTRY(th_transport) tht_mux_link;
+ LIST_ENTRY(th_transport) tht_group_link;
LIST_ENTRY(th_transport) tht_active_link;
void (*tht_stop_feed)(struct th_transport *t);
- void (*tht_config_change)(struct th_transport *t);
+ void (*tht_config_save)(struct th_transport *t);
struct htsmsg *(*tht_sourceinfo)(struct th_transport *t);
int tht_sp_onqueue;
TAILQ_ENTRY(th_transport) tht_sp_link;
+ /**
+ * Pending save.
+ *
+ * transport_request_save() will enqueue the transport here.
+ * We need to do this if we don't hold the global lock.
+ * This happens when we update PMT from within the TS stream itself.
+ * Then we hold the stream mutex, and thus, can not obtain the global lock
+ * as it would cause lock inversion.
+ */
+ int tht_ps_onqueue;
+ TAILQ_ENTRY(th_transport) tht_ps_link;
+
/**
* Timer which is armed at transport start. Once it fires
* it will check if any packets has been parsed. If not the status
*/
gtimer_t tht_receive_timer;
+ /**
+ * IPTV members
+ */
+ char *tht_iptv_iface;
+ struct in_addr tht_iptv_group;
+ uint16_t tht_iptv_port;
+ int tht_iptv_fd;
+
+ /**
+ * For per-transport PAT/PMT parsers, allocated on demand
+ * Free'd by transport_destroy
+ */
+ struct psi_section *tht_pat_section;
+ struct psi_section *tht_pmt_section;
+
+
/*********************************************************
*
* Streaming part of transport
#define MAX(a,b) ((a) > (b) ? (a) : (b))
void tvh_str_set(char **strp, const char *src);
-void tvh_str_update(char **strp, const char *src);
+int tvh_str_update(char **strp, const char *src);
void tvhlog(int severity, const char *subsys, const char *fmt, ...);
#include <string.h>
#include <stdarg.h>
+#include <arpa/inet.h>
+#include <libavutil/avstring.h>
+
#include "htsmsg.h"
#include "htsmsg_json.h"
#include "serviceprobe.h"
#include "xmltv.h"
#include "epg.h"
+#include "iptv_input.h"
extern const char *htsversion;
extern const char *htsversion_full;
extjs_load(hq, "static/app/acleditor.js");
extjs_load(hq, "static/app/cwceditor.js");
extjs_load(hq, "static/app/dvb.js");
+ extjs_load(hq, "static/app/iptv.js");
extjs_load(hq, "static/app/chconf.js");
extjs_load(hq, "static/app/epg.js");
extjs_load(hq, "static/app/dvr.js");
"Service probe started on \"%s\"", tda->tda_displayname);
LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) {
- LIST_FOREACH(t, &tdmi->tdmi_transports, tht_mux_link) {
+ LIST_FOREACH(t, &tdmi->tdmi_transports, tht_group_link) {
if(t->tht_enabled)
serviceprobe_enqueue(t);
}
}
+/**
+ *
+ */
+static void
+transport_delete(htsmsg_t *in)
+{
+ htsmsg_field_t *f;
+ th_transport_t *t;
+ const char *id;
+
+ TAILQ_FOREACH(f, &in->hm_fields, hmf_link) {
+ if((id = htsmsg_field_get_string(f)) != NULL &&
+ (t = transport_find_by_identifier(id)) != NULL)
+ transport_destroy(t);
+ }
+}
+
+
/**
*
*/
array = htsmsg_create_list();
LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) {
- LIST_FOREACH(t, &tdmi->tdmi_transports, tht_mux_link) {
+ LIST_FOREACH(t, &tdmi->tdmi_transports, tht_group_link) {
count++;
}
}
tvec = alloca(sizeof(th_transport_t *) * count);
LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) {
- LIST_FOREACH(t, &tdmi->tdmi_transports, tht_mux_link) {
+ LIST_FOREACH(t, &tdmi->tdmi_transports, tht_group_link) {
tvec[i++] = t;
}
}
htsmsg_destroy(out);
http_output_content(hc, "text/x-json; charset=UTF-8");
return 0;
+}
+
+
+
+/**
+ *
+ */
+static void
+transport_update_iptv(htsmsg_t *in)
+{
+ htsmsg_field_t *f;
+ htsmsg_t *c;
+ th_transport_t *t;
+ uint32_t u32;
+ const char *id, *s;
+ int save;
+
+ TAILQ_FOREACH(f, &in->hm_fields, hmf_link) {
+ if((c = htsmsg_get_map_by_field(f)) == NULL ||
+ (id = htsmsg_get_str(c, "id")) == NULL)
+ continue;
+
+ if((t = transport_find_by_identifier(id)) == NULL)
+ continue;
+
+ save = 0;
+
+ if(!htsmsg_get_u32(c, "port", &u32)) {
+ t->tht_iptv_port = u32;
+ save = 1;
+ }
+ if((s = htsmsg_get_str(c, "group")) != NULL) {
+ inet_pton(AF_INET, s, &t->tht_iptv_group.s_addr);
+ save = 1;
+ }
+
+ save |= tvh_str_update(&t->tht_iptv_iface, htsmsg_get_str(c, "interface"));
+ if(save)
+ t->tht_config_save(t); // Save config
+ }
+}
+
+
+
+/**
+ *
+ */
+static htsmsg_t *
+build_record_iptv(th_transport_t *t)
+{
+ htsmsg_t *r = htsmsg_create_map();
+ char abuf[INET_ADDRSTRLEN];
+
+ htsmsg_add_str(r, "id", t->tht_identifier);
+
+ htsmsg_add_str(r, "channelname", t->tht_ch ? t->tht_ch->ch_name : "");
+ htsmsg_add_str(r, "interface", t->tht_iptv_iface ?: "");
+
+ inet_ntop(AF_INET, &t->tht_iptv_group, abuf, sizeof(abuf));
+ htsmsg_add_str(r, "group", t->tht_iptv_group.s_addr ? abuf : "");
+
+ htsmsg_add_u32(r, "port", t->tht_iptv_port);
+ htsmsg_add_u32(r, "enabled", t->tht_enabled);
+ return r;
+}
+
+/**
+ *
+ */
+static int
+iptv_transportcmp(const void *A, const void *B)
+{
+ th_transport_t *a = *(th_transport_t **)A;
+ th_transport_t *b = *(th_transport_t **)B;
+
+ return memcmp(&a->tht_iptv_group, &b->tht_iptv_group, 4);
+}
+
+/**
+ *
+ */
+static int
+extjs_iptvservices(http_connection_t *hc, const char *remain, void *opaque)
+{
+ htsbuf_queue_t *hq = &hc->hc_reply;
+ htsmsg_t *out, *in, *array;
+ const char *op = http_arg_get(&hc->hc_req_args, "op");
+ const char *entries = http_arg_get(&hc->hc_req_args, "entries");
+ th_transport_t *t, **tvec;
+ int count = 0, i = 0;
+
+ pthread_mutex_lock(&global_lock);
+
+ in = entries != NULL ? htsmsg_json_deserialize(entries) : NULL;
+
+ if(!strcmp(op, "get")) {
+
+ LIST_FOREACH(t, &iptv_all_transports, tht_group_link)
+ count++;
+ tvec = alloca(sizeof(th_transport_t *) * count);
+ LIST_FOREACH(t, &iptv_all_transports, tht_group_link)
+ tvec[i++] = t;
+
+ out = htsmsg_create_map();
+ array = htsmsg_create_list();
+
+ qsort(tvec, count, sizeof(th_transport_t *), iptv_transportcmp);
+
+ for(i = 0; i < count; i++)
+ htsmsg_add_msg(array, NULL, build_record_iptv(tvec[i]));
+
+ htsmsg_add_msg(out, "entries", array);
+
+ } else if(!strcmp(op, "update")) {
+ if(in != NULL) {
+ transport_update(in); // Generic transport parameters
+ transport_update_iptv(in); // IPTV speicifc
+ }
+
+ out = htsmsg_create_map();
+
+ } else if(!strcmp(op, "create")) {
+
+ out = build_record_iptv(iptv_transport_find(NULL, 1));
+
+ } else if(!strcmp(op, "delete")) {
+ if(in != NULL)
+ transport_delete(in);
+
+ out = htsmsg_create_map();
+
+ } else {
+ pthread_mutex_unlock(&global_lock);
+ htsmsg_destroy(in);
+ return HTTP_STATUS_BAD_REQUEST;
+ }
+
+ htsmsg_destroy(in);
+
+ pthread_mutex_unlock(&global_lock);
+
+ htsmsg_json_serialize(out, hq, 0);
+ htsmsg_destroy(out);
+ http_output_content(hc, "text/x-json; charset=UTF-8");
+ return 0;
}
http_path_add("/dvb/addmux",
NULL, extjs_dvb_addmux, ACCESS_ADMIN);
+
+ http_path_add("/iptv/services",
+ NULL, extjs_iptvservices, ACCESS_ADMIN);
+
}
background-image:url(../icons/arrow_join.png) !important;
}
+.iptv {
+ background-image:url(../icons/world.png) !important;
+}
+
+
.x-smallhdr {
float:left;
--- /dev/null
+/**
+ * IPTV service grid
+ */
+tvheadend.iptv = function(adapterId) {
+
+ var fm = Ext.form;
+
+ var enabledColumn = new Ext.grid.CheckColumn({
+ header: "Enabled",
+ dataIndex: 'enabled',
+ width: 45
+ });
+
+ var actions = new Ext.ux.grid.RowActions({
+ header:'',
+ dataIndex: 'actions',
+ width: 45,
+ actions: [
+ {
+ iconCls:'info',
+ qtip:'Detailed information about service',
+ cb: function(grid, record, action, row, col) {
+ Ext.Ajax.request({
+ url: "dvb/servicedetails/" + record.id,
+ success:function(response, options) {
+ r = Ext.util.JSON.decode(response.responseText);
+ tvheadend.showTransportDetails(r);
+ }
+ })
+ }
+ }
+ ]
+ });
+
+ var cm = new Ext.grid.ColumnModel([
+ enabledColumn,
+ {
+ header: "Channel name",
+ dataIndex: 'channelname',
+ width: 150,
+ renderer: function(value, metadata, record, row, col, store) {
+ return value ? value :
+ '<span class="tvh-grid-unset">Unmapped</span>';
+ },
+ editor: new fm.ComboBox({
+ store: tvheadend.channels,
+ allowBlank: true,
+ typeAhead: true,
+ minChars: 2,
+ lazyRender: true,
+ triggerAction: 'all',
+ mode: 'local',
+ displayField:'name'
+ })
+ },
+ {
+ header: "Interface",
+ dataIndex: 'interface',
+ width: 100,
+ renderer: function(value, metadata, record, row, col, store) {
+ return value ? value :
+ '<span class="tvh-grid-unset">Unset</span>';
+ },
+ editor: new fm.TextField({allowBlank: false})
+ },
+ {
+ header: "Group",
+ dataIndex: 'group',
+ width: 100,
+ renderer: function(value, metadata, record, row, col, store) {
+ return value ? value :
+ '<span class="tvh-grid-unset">Unset</span>';
+ },
+ editor: new fm.TextField({allowBlank: false})
+ },
+ {
+ header: "UDP Port",
+ dataIndex: 'port',
+ width: 60,
+ editor: new fm.NumberField({
+ minValue: 1,
+ maxValue: 65535
+ })
+ },
+ {
+ header: "Service ID",
+ dataIndex: 'sid',
+ width: 50,
+ hidden: true
+ },
+ {
+ header: "PMT PID",
+ dataIndex: 'pmt',
+ width: 50,
+ hidden: true
+ },
+ {
+ header: "PCR PID",
+ dataIndex: 'pcr',
+ width: 50,
+ hidden: true
+ }, actions
+ ]);
+
+ cm.defaultSortable = true;
+
+ var rec = Ext.data.Record.create([
+ 'id', 'enabled', 'channelname', 'interface', 'group', 'port',
+ 'sid', 'pmt', 'pcr'
+ ]);
+
+ var store = new Ext.data.JsonStore({
+ root: 'entries',
+ fields: rec,
+ url: "iptv/services",
+ autoLoad: true,
+ id: 'id',
+ baseParams: {op: "get"},
+ listeners: {
+ 'update': function(s, r, o) {
+ d = s.getModifiedRecords().length == 0
+ saveBtn.setDisabled(d);
+ rejectBtn.setDisabled(d);
+ }
+ }
+ });
+
+/*
+ var storeReloader = new Ext.util.DelayedTask(function() {
+ store.reload()
+ });
+
+ tvheadend.comet.on('dvbService', function(m) {
+ storeReloader.delay(500);
+ });
+*/
+
+ function addRecord() {
+ Ext.Ajax.request({
+ url: "iptv/services",
+ params: {
+ op:"create"
+ },
+ failure:function(response,options){
+ Ext.MessageBox.alert('Server Error',
+ 'Unable to generate new record');
+ },
+ success:function(response,options){
+ var responseData = Ext.util.JSON.decode(response.responseText);
+ var p = new rec(responseData, responseData.id);
+ grid.stopEditing();
+ store.insert(0, p);
+ grid.startEditing(0, 0);
+ }
+ })
+ };
+
+
+ function delSelected() {
+ var selectedKeys = grid.selModel.selections.keys;
+ if(selectedKeys.length > 0) {
+ Ext.MessageBox.confirm('Message',
+ 'Do you really want to delete selection?',
+ deleteRecord);
+ } else {
+ Ext.MessageBox.alert('Message',
+ 'Please select at least one item to delete');
+ }
+ };
+
+ function deleteRecord(btn) {
+ if(btn=='yes') {
+ var selectedKeys = grid.selModel.selections.keys;
+
+ Ext.Ajax.request({
+ url: "iptv/services",
+ params: {
+ op:"delete",
+ entries:Ext.encode(selectedKeys)
+ },
+ failure:function(response,options) {
+ Ext.MessageBox.alert('Server Error','Unable to delete');
+ },
+ success:function(response,options) {
+ store.reload();
+ }
+ })
+ }
+ }
+
+ function saveChanges() {
+ var mr = store.getModifiedRecords();
+ var out = new Array();
+ for (var x = 0; x < mr.length; x++) {
+ v = mr[x].getChanges();
+ out[x] = v;
+ out[x].id = mr[x].id;
+ }
+
+ Ext.Ajax.request({
+ url: "iptv/services",
+ params: {
+ op:"update",
+ entries:Ext.encode(out)
+ },
+ success:function(response,options) {
+ store.commitChanges();
+ },
+ failure:function(response,options) {
+ Ext.MessageBox.alert('Message',response.statusText);
+ }
+ });
+ }
+
+ var delButton = new Ext.Toolbar.Button({
+ tooltip: 'Delete one or more selected rows',
+ iconCls:'remove',
+ text: 'Delete selected services',
+ handler: delSelected,
+ disabled: true
+ });
+
+ var saveBtn = new Ext.Toolbar.Button({
+ tooltip: 'Save any changes made (Changed cells have red borders).',
+ iconCls:'save',
+ text: "Save changes",
+ handler: saveChanges,
+ disabled: true
+ });
+
+ var rejectBtn = new Ext.Toolbar.Button({
+ tooltip: 'Revert any changes made (Changed cells have red borders).',
+ iconCls:'undo',
+ text: "Revert changes",
+ handler: function() {
+ store.rejectChanges();
+ },
+ disabled: true
+ });
+
+ var selModel = new Ext.grid.RowSelectionModel({
+ singleSelect:false
+ });
+
+ var grid = new Ext.grid.EditorGridPanel({
+ stripeRows: true,
+ title: 'IPTV',
+ iconCls: 'iptv',
+ plugins: [enabledColumn, actions],
+ store: store,
+ clicksToEdit: 2,
+ cm: cm,
+ viewConfig: {forceFit:true},
+ selModel: selModel,
+ tbar: [{
+ tooltip: 'Create a new entry on the server. '+
+ 'The new entry is initially disabled so it must be enabled '+
+ 'before it start taking effect.',
+ iconCls:'add',
+ text: 'Add service',
+ handler: addRecord
+ }, '-', delButton, '-', saveBtn, rejectBtn]
+ });
+
+ store.on('update', function(s, r, o) {
+ d = s.getModifiedRecords().length == 0
+ saveBtn.setDisabled(d);
+ rejectBtn.setDisabled(d);
+ });
+
+ selModel.on('selectionchange', function(self) {
+ delButton.setDisabled(self.getCount() == 0);
+ });
+
+ return grid;
+}
new tvheadend.cteditor,
new tvheadend.dvrsettings,
new tvheadend.dvb,
+ new tvheadend.iptv,
new tvheadend.acleditor,
new tvheadend.cwceditor]
});