From cf49e034672544bc0c6014e84fb50b87d8c3bd67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96man?= Date: Sat, 15 Aug 2009 21:45:34 +0000 Subject: [PATCH] Add support for IPTV --- Makefile | 1 + debian/changelog | 2 + src/channels.c | 7 +- src/dvb/dvb_adapter.c | 6 +- src/dvb/dvb_tables.c | 4 +- src/dvb/dvb_transport.c | 14 +- src/iptv_input.c | 551 +++++++++++++++++++----------- src/iptv_input.h | 9 +- src/main.c | 11 +- src/psi.c | 7 +- src/rawtsinput.c | 8 +- src/subscriptions.c | 28 +- src/transports.c | 109 ++++-- src/transports.h | 6 +- src/tvhead.h | 36 +- src/webui/extjs.c | 178 +++++++++- src/webui/static/app/ext.css | 5 + src/webui/static/app/iptv.js | 276 +++++++++++++++ src/webui/static/app/tvheadend.js | 1 + src/webui/static/icons/world.png | Bin 0 -> 923 bytes 20 files changed, 993 insertions(+), 266 deletions(-) create mode 100644 src/webui/static/app/iptv.js create mode 100644 src/webui/static/icons/world.png diff --git a/Makefile b/Makefile index b099119d..6b6a553f 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,7 @@ SRCS = src/main.c \ src/avg.c \ src/htsstr.c \ src/rawtsinput.c \ + src/iptv_input.c \ SRCS += src/dvr/dvr_db.c \ src/dvr/dvr_rec.c \ diff --git a/debian/changelog b/debian/changelog index 92fd9260..7737bec8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -8,6 +8,8 @@ hts-tvheadend (2.5) hts; urgency=low * 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 diff --git a/src/channels.c b/src/channels.c index c2153b3a..f8cfaaa0 100644 --- a/src/channels.c +++ b/src/channels.c @@ -353,11 +353,8 @@ channel_rename(channel_t *ch, const char *newname) 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); diff --git a/src/dvb/dvb_adapter.c b/src/dvb/dvb_adapter.c index d1ad49fa..61aabf74 100644 --- a/src/dvb/dvb_adapter.c +++ b/src/dvb/dvb_adapter.c @@ -381,7 +381,7 @@ dvb_adapter_clone(th_dvb_adapter_t *dst, th_dvb_adapter_t *src) 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); @@ -415,8 +415,8 @@ dvb_adapter_clone(th_dvb_adapter_t *dst, th_dvb_adapter_t *src) 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); @@ -527,7 +527,7 @@ dvb_adapter_build_msg(th_dvb_adapter_t *tda) // 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++; } } diff --git a/src/dvb/dvb_tables.c b/src/dvb/dvb_tables.c index fdd348e0..63f9fbe8 100644 --- a/src/dvb/dvb_tables.c +++ b/src/dvb/dvb_tables.c @@ -677,9 +677,7 @@ dvb_sdt_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, 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; diff --git a/src/dvb/dvb_transport.c b/src/dvb/dvb_transport.c index ca7720e5..be1856a3 100644 --- a/src/dvb/dvb_transport.c +++ b/src/dvb/dvb_transport.c @@ -66,7 +66,8 @@ dvb_transport_open_demuxers(th_dvb_adapter_t *tda, th_transport_t *t) 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; } @@ -80,7 +81,8 @@ dvb_transport_open_demuxers(th_dvb_adapter_t *tda, th_transport_t *t) 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; } @@ -258,7 +260,9 @@ dvb_transport_save(th_transport_t *t) 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, @@ -334,7 +338,7 @@ dvb_transport_find(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, 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; } @@ -359,12 +363,12 @@ dvb_transport_find(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, 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; diff --git a/src/iptv_input.c b/src/iptv_input.c index e757179f..221d8b06 100644 --- a/src/iptv_input.c +++ b/src/iptv_input.c @@ -19,9 +19,10 @@ #include #include -#include #include +#include #include +#include #include #include @@ -31,271 +32,427 @@ #include #include #include -#include -#include +#include #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" -struct th_transport_list iptv_probing_transports; -struct th_transport_list iptv_stale_transports; -static dtimer_t iptv_probe_timer; +static int iptv_thread_running; +static int iptv_epollfd; +static pthread_mutex_t iptv_recvmutex; -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); +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_fd_callback(int events, void *opaque, int fd) +iptv_got_pat(const uint8_t *ptr, int len, void *aux) { - th_transport_t *t = opaque; - uint8_t buf[2000]; - int r; - uint8_t *tsb = buf; + th_transport_t *t = aux; + uint16_t prognum, pmt; - r = read(fd, buf, sizeof(buf)); + len -= 8; + ptr += 8; - while(r >= 188) { + if(len < 4) + return; + + prognum = ptr[0] << 8 | ptr[1]; + pmt = (ptr[2] & 0x1f) << 8 | ptr[3]; + + t->tht_pmt_pid = pmt; +} + + +/** + * 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_ts_input(th_transport_t *t, uint8_t *tsb) +{ + 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) { + + 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); + + } 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; + } + + 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; } t->tht_iptv_fd = fd; - t->tht_runstatus = status; + t->tht_status = status; - syslog(LOG_ERR, "iptv: \"%s\" joined group", t->tht_name); - - 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) -{ - 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_refresh(th_transport_t *t) { - if(table[0] != 2 || t->tht_runstatus != TRANSPORT_PROBING) - return; - psi_parse_pmt(t, table + 3, table_len - 3, 0); - - iptv_probe_done(t, 0); } -/* +/** * */ - static void -iptv_parse_pat(struct th_transport *t, th_stream_t *st, - uint8_t *table, int table_len) +iptv_transport_stop(th_transport_t *t) { - if(table[0] != 0 || t->tht_runstatus != TRANSPORT_PROBING) - return; - - psi_parse_pat(t, table + 3, table_len - 3, iptv_parse_pmt); -} - -/* - * - */ - -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; - - t->tht_type = TRANSPORT_IPTV; - t->tht_start_feed = iptv_start_feed; - t->tht_stop_feed = iptv_stop_feed; - - if((s = config_get_str_sub(head, "group-address", NULL)) == NULL) - return -1; - t->tht_iptv_group_addr.s_addr = inet_addr(s); - - t->tht_iptv_ifindex = 0; - - 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; - - snprintf(ifname, sizeof(ifname), "%s", - inet_ntoa(t->tht_iptv_interface_addr)); - - if((s = config_get_str_sub(head, "interface", NULL)) != NULL) { - - 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); - - 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); - - 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"); - - snprintf(buf, sizeof(buf), "iptv_%s_%d", - inet_ntoa(t->tht_iptv_group_addr), t->tht_iptv_port); - t->tht_identifier = strdup(buf); - - t->tht_chname = strdup(channel_name); - - LIST_INSERT_HEAD(&iptv_probing_transports, t, tht_active_link); - startupcounter++; - - if(!dtimer_isarmed(&iptv_probe_timer)) { - iptv_probe_transport(t); - dtimer_arm(&iptv_probe_timer, iptv_probe_callback, t, 5); - } - - return 0; -} - -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); -} - - -static void -iptv_probe_done(th_transport_t *t, int timeout) -{ - int pidcnt = 0; - th_stream_t *st; - - startupcounter--; - - dtimer_disarm(&iptv_probe_timer); - - LIST_FOREACH(st, &t->tht_streams, st_link) - pidcnt++; - + t->tht_status = TRANSPORT_IDLE; + pthread_mutex_lock(&iptv_recvmutex); LIST_REMOVE(t, tht_active_link); + pthread_mutex_unlock(&iptv_recvmutex); - syslog(LOG_INFO, "iptv: Transport %s probed, %d pids found%s", - t->tht_name, pidcnt, timeout ? ", but probe timeouted" : ""); + assert(t->tht_iptv_fd >= 0); - iptv_stop_feed(t); + close(t->tht_iptv_fd); // Automatically removes fd from epoll set - if(!timeout) - transport_map_channel(t, NULL); - else - LIST_INSERT_HEAD(&iptv_stale_transports, t, tht_active_link); - - t = LIST_FIRST(&iptv_probing_transports); - if(t == NULL) - return; - - iptv_probe_transport(t); - dtimer_arm(&iptv_probe_timer, iptv_probe_callback, t, 5); + t->tht_iptv_fd = -1; } - +/** + * + */ static void -iptv_probe_callback(void *aux, int64_t now) +iptv_transport_save(th_transport_t *t) { - th_transport_t *t = aux; - iptv_probe_done(t, 1); + htsmsg_t *m = htsmsg_create_map(); + char abuf[INET_ADDRSTRLEN]; + + lock_assert(&global_lock); + + htsmsg_add_u32(m, "pmt", t->tht_pmt_pid); + + if(t->tht_iptv_port) + htsmsg_add_u32(m, "port", t->tht_iptv_port); + + if(t->tht_iptv_iface) + htsmsg_add_str(m, "interface", t->tht_iptv_iface); + + 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(t->tht_ch != NULL) { + htsmsg_add_str(m, "channelname", t->tht_ch->ch_name); + 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, "iptvtransports/%s", + t->tht_identifier); + + htsmsg_destroy(m); +} + + +/** + * + */ +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; + + return 100; +} + + +/** + * Generate a descriptive name for the source + */ +static htsmsg_t * +iptv_transport_sourceinfo(th_transport_t *t) +{ + htsmsg_t *m = htsmsg_create_map(); + + 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; +} + + +/** + * + */ +th_transport_t * +iptv_transport_find(const char *id, int create) +{ + static int tally; + th_transport_t *t; + char buf[20]; + + 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; + } + + 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); + } + + 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_transport_load(void) +{ + htsmsg_t *l, *c; + htsmsg_field_t *f; + uint32_t pmt; + const char *s; + unsigned int u32; + th_transport_t *t; + + lock_assert(&global_lock); + + if((l = hts_settings_load("iptvtransports")) == NULL) + return; + + HTSMSG_FOREACH(f, l) { + if((c = htsmsg_get_map_by_field(f)) == NULL) + continue; + + if(htsmsg_get_u32(c, "pmt", &pmt)) + continue; + + t = iptv_transport_find(f->hmf_name, 1); + t->tht_pmt_pid = pmt; + + tvh_str_update(&t->tht_iptv_iface, htsmsg_get_str(c, "interface")); + + 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; + + 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); +} + + +/** + * + */ +void +iptv_input_init(void) +{ + pthread_mutex_init(&iptv_recvmutex, NULL); + iptv_transport_load(); } diff --git a/src/iptv_input.h b/src/iptv_input.h index b9918516..d5020b1e 100644 --- a/src/iptv_input.h +++ b/src/iptv_input.h @@ -19,11 +19,10 @@ #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_ */ diff --git a/src/main.c b/src/main.c index c62298b2..e8ba6f6d 100644 --- a/src/main.c +++ b/src/main.c @@ -52,6 +52,8 @@ #include "htsp.h" #include "rawtsinput.h" #include "avahi.h" +#include "iptv_input.h" +#include "transports.h" #include "parachute.h" #include "settings.h" @@ -327,6 +329,8 @@ main(int argc, char **argv) xmltv_init(); /* Must be initialized before channels */ + transport_init(); + channels_init(); access_init(createdefault); @@ -335,6 +339,8 @@ main(int argc, char **argv) dvb_init(); + iptv_input_init(); + http_server_init(); webui_init(contentpath); @@ -507,13 +513,14 @@ tvh_str_set(char **strp, const char *src) /** * */ -void +int tvh_str_update(char **strp, const char *src) { if(src == NULL) - return; + return 0; free(*strp); *strp = strdup(src); + return 1; } diff --git a/src/psi.c b/src/psi.c index 99d24993..32aa9c0b 100644 --- a/src/psi.c +++ b/src/psi.c @@ -276,12 +276,15 @@ psi_parse_pmt(th_transport_t *t, const uint8_t *ptr, int len, int chksvcid, 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) { @@ -442,9 +445,9 @@ psi_parse_pmt(th_transport_t *t, const uint8_t *ptr, int len, int chksvcid, } 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; } diff --git a/src/rawtsinput.c b/src/rawtsinput.c index acc06b75..2c1a4c09 100644 --- a/src/rawtsinput.c +++ b/src/rawtsinput.c @@ -103,7 +103,7 @@ rawts_transport_add(rawts_t *rt, uint16_t sid, int pmt_pid) 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; } @@ -118,7 +118,7 @@ rawts_transport_add(rawts_t *rt, uint16_t sid, int pmt_pid) 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; @@ -126,7 +126,7 @@ rawts_transport_add(rawts_t *rt, uint16_t sid, int pmt_pid) 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); @@ -245,7 +245,7 @@ process_ts_packet(rawts_t *rt, uint8_t *tsb) return; } - LIST_FOREACH(t, &rt->rt_transports, tht_mux_link) + LIST_FOREACH(t, &rt->rt_transports, tht_group_link) ts_recv_packet1(t, tsb); } diff --git a/src/subscriptions.c b/src/subscriptions.c index 8a2161d3..feaa9a18 100644 --- a/src/subscriptions.c +++ b/src/subscriptions.c @@ -78,18 +78,20 @@ subscription_link_transport(th_subscription_t *s, th_transport_t *t) // 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); } @@ -108,10 +110,12 @@ subscription_unlink_transport(th_subscription_t *s) // 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); diff --git a/src/transports.c b/src/transports.c index 0f518953..df47a982 100644 --- a/src/transports.c +++ b/src/transports.c @@ -434,14 +434,12 @@ transport_destroy(th_transport_t *t) 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) @@ -458,6 +456,9 @@ transport_destroy(th_transport_t *t) free(st); } + free(t->tht_pat_section); + free(t->tht_pmt_section); + transport_unref(t); } @@ -536,6 +537,7 @@ transport_stream_create(th_transport_t *t, int pid, 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); @@ -593,12 +595,8 @@ transport_map_channel(th_transport_t *t, channel_t *ch, int save) 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); } @@ -675,19 +673,24 @@ transport_set_feed_status(th_transport_t *t, transport_feed_status_t newstatus) * (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); + } } @@ -771,8 +774,76 @@ transport_set_enable(th_transport_t *t, int enabled) return; t->tht_enabled = enabled; - - pthread_mutex_lock(&t->tht_stream_mutex); - t->tht_config_change(t); // Save config - pthread_mutex_unlock(&t->tht_stream_mutex); + t->tht_config_save(t); +} + + +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); } diff --git a/src/transports.h b/src/transports.h index e9d07312..d9787a83 100644 --- a/src/transports.h +++ b/src/transports.h @@ -23,6 +23,8 @@ #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); @@ -78,8 +80,10 @@ htsmsg_t *transport_build_stream_start_msg(th_transport_t *t); 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 */ diff --git a/src/tvhead.h b/src/tvhead.h index 34924456..22114db9 100644 --- a/src/tvhead.h +++ b/src/tvhead.h @@ -356,8 +356,6 @@ typedef enum { */ typedef struct th_transport { - const char *tht_name; - LIST_ENTRY(th_transport) tht_hash_link; enum { @@ -436,7 +434,7 @@ typedef struct th_transport { int tht_enabled; - LIST_ENTRY(th_transport) tht_mux_link; + LIST_ENTRY(th_transport) tht_group_link; LIST_ENTRY(th_transport) tht_active_link; @@ -449,7 +447,7 @@ typedef struct th_transport { 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); @@ -511,6 +509,18 @@ typedef struct th_transport { 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 @@ -518,6 +528,22 @@ typedef struct th_transport { */ 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 @@ -614,7 +640,7 @@ static inline unsigned int tvh_strhash(const char *s, unsigned int mod) #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, ...); diff --git a/src/webui/extjs.c b/src/webui/extjs.c index ac8dffe7..e1c50f1c 100644 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -24,6 +24,9 @@ #include #include +#include +#include + #include "htsmsg.h" #include "htsmsg_json.h" @@ -43,6 +46,7 @@ #include "serviceprobe.h" #include "xmltv.h" #include "epg.h" +#include "iptv_input.h" extern const char *htsversion; extern const char *htsversion_full; @@ -123,6 +127,7 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque) 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"); @@ -1007,7 +1012,7 @@ extjs_dvbadapter(http_connection_t *hc, const char *remain, void *opaque) "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); } @@ -1140,6 +1145,24 @@ extjs_dvbmuxes(http_connection_t *hc, const char *remain, void *opaque) } +/** + * + */ +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); + } +} + + /** * */ @@ -1213,7 +1236,7 @@ extjs_dvbservices(http_connection_t *hc, const char *remain, void *opaque) 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++; } } @@ -1221,7 +1244,7 @@ extjs_dvbservices(http_connection_t *hc, const char *remain, void *opaque) 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; } } @@ -1514,8 +1537,153 @@ extjs_mergechannel(http_connection_t *hc, const char *remain, void *opaque) 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; } @@ -1563,4 +1731,8 @@ extjs_start(void) http_path_add("/dvb/addmux", NULL, extjs_dvb_addmux, ACCESS_ADMIN); + + http_path_add("/iptv/services", + NULL, extjs_iptvservices, ACCESS_ADMIN); + } diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index 2151cc42..6813760f 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -185,6 +185,11 @@ background-image:url(../icons/arrow_join.png) !important; } +.iptv { + background-image:url(../icons/world.png) !important; +} + + .x-smallhdr { float:left; diff --git a/src/webui/static/app/iptv.js b/src/webui/static/app/iptv.js new file mode 100644 index 00000000..7796cef5 --- /dev/null +++ b/src/webui/static/app/iptv.js @@ -0,0 +1,276 @@ +/** + * 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 : + 'Unmapped'; + }, + 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 : + 'Unset'; + }, + editor: new fm.TextField({allowBlank: false}) + }, + { + header: "Group", + dataIndex: 'group', + width: 100, + renderer: function(value, metadata, record, row, col, store) { + return value ? value : + 'Unset'; + }, + 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; +} diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index 3a16abcc..497f740a 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -52,6 +52,7 @@ function accessUpdate(o) { new tvheadend.cteditor, new tvheadend.dvrsettings, new tvheadend.dvb, + new tvheadend.iptv, new tvheadend.acleditor, new tvheadend.cwceditor] }); diff --git a/src/webui/static/icons/world.png b/src/webui/static/icons/world.png new file mode 100644 index 0000000000000000000000000000000000000000..68f21d30116710e48a8bf462cb32441e51fad5f6 GIT binary patch literal 923 zcmV;M17!S(P)A9)b<7tX~vT z$e)FfZ+`X4_uKyq#wJHC;J3lH{lhQkUc~Wid;*pnjhM12xe-bPByd^xuQ9zgeM^Mm z*tc)|P}LtTnHXr@Gkmmbkg^O2bqyhO>LP|qjIwW2@Di+4EuKm~&tOO2!N3o{128Hl z9v%fgerM0C#)7P|PMvxr*!Gf?eGA8f{OT6fS`9l>LQCg)p=~c$Zr|AT_0+_?F*JJk zlapOT2Q(wWx-LMq(TxXxLn+U;!LV)MhNp~ommdh+fo8T*&g-yQbbG&ze&=>tC(Ar=&^1xlA;Jc(6 zcCi_xs8k}-S&#ONOHm%e@#nGC7F++8C~r29Or!_{(QGQEG)+O^J1BCPmgM4JAzC8I z`jS9bO>|}Jq_#$IRzp0d34>)&3L%7MN)eTv!0B!^nn}f4z2*vFE@jv3dn zG>H)u>FR7_d2JcsjvfZ$vkP~xik@T^(_N)nx=tqJV+tQjQ`owJ83bf`zX6Ear*=Mhzn5QUuXE|v zR33Qyi8G!0{H2r##d#6R6YmYbZz4NTssT;cXiGb6lxO+k@{ba@2D~*hKDY6N;Bkh> xhhCRLejsJkAIT{5sICHcfU`5>bKmUb{{y)0nR3PMMxX!y002ovPDHLkV1nl+t-}BS literal 0 HcmV?d00001