Add support for IPTV

This commit is contained in:
Andreas Öman 2009-08-15 21:45:34 +00:00
parent ac5ae9e9cd
commit cf49e03467
20 changed files with 993 additions and 266 deletions

View file

@ -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 \

2
debian/changelog vendored
View file

@ -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

View file

@ -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);

View file

@ -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++;
}
}

View file

@ -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;

View file

@ -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;

View file

@ -19,9 +19,10 @@
#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>
@ -31,271 +32,427 @@
#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"
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();
}

View file

@ -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_ */

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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 */

View file

@ -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, ...);

View file

@ -24,6 +24,9 @@
#include <string.h>
#include <stdarg.h>
#include <arpa/inet.h>
#include <libavutil/avstring.h>
#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);
}

View file

@ -185,6 +185,11 @@
background-image:url(../icons/arrow_join.png) !important;
}
.iptv {
background-image:url(../icons/world.png) !important;
}
.x-smallhdr {
float:left;

View file

@ -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 :
'<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;
}

View file

@ -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]
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B