diff --git a/src/dvr/dvr_rec.c b/src/dvr/dvr_rec.c index 9ba9e103..a46987fb 100755 --- a/src/dvr/dvr_rec.c +++ b/src/dvr/dvr_rec.c @@ -86,7 +86,10 @@ dvr_rec_subscribe(dvr_entry_t *de) } de->de_s = subscription_create_from_channel(de->de_channel, weight, - buf, st, flags); + buf, st, flags, + NULL, "DVR", + lang_str_get(de->de_title, + NULL)); pthread_create(&de->de_thread, NULL, dvr_thread, de); } diff --git a/src/htsp.c b/src/htsp.c index fc2bce35..99813b0a 100644 --- a/src/htsp.c +++ b/src/htsp.c @@ -1167,7 +1167,10 @@ htsp_method_subscribe(htsp_connection_t *htsp, htsmsg_t *in) hs->hs_s = subscription_create_from_channel(ch, weight, htsp->htsp_logname, - st, 0); + st, 0, + htsp->htsp_peername, + htsp->htsp_username, + htsp->htsp_clientname); return NULL; } diff --git a/src/main.c b/src/main.c index 770d71f1..9cca79fd 100644 --- a/src/main.c +++ b/src/main.c @@ -415,6 +415,8 @@ main(int argc, char **argv) channels_init(); + subscription_init(); + access_init(createdefault); tcp_server_init(); diff --git a/src/subscriptions.c b/src/subscriptions.c index 4249a382..0f816ba2 100644 --- a/src/subscriptions.c +++ b/src/subscriptions.c @@ -36,6 +36,9 @@ #include "streaming.h" #include "channels.h" #include "service.h" +#include "htsmsg.h" +#include "notify.h" +#include "atomic.h" struct th_subscription_list subscriptions; static gtimer_t subscription_reschedule_timer; @@ -129,12 +132,16 @@ subscription_unlink_service(th_subscription_t *s, int reason) } +/** + * + */ static void subscription_reschedule_cb(void *aux) { subscription_reschedule(); } + /** * */ @@ -212,9 +219,13 @@ subscription_unsubscribe(th_subscription_t *s) streaming_msg_free(s->ths_start_message); free(s->ths_title); + free(s->ths_hostname); + free(s->ths_username); + free(s->ths_client); free(s); subscription_reschedule(); + notify_reload("subscriptions"); } @@ -261,6 +272,14 @@ subscription_input(void *opauqe, streaming_message_t *sm) streaming_msg_free(sm); return; } + + if(sm->sm_type == SMT_PACKET) { + th_pkt_t *pkt = sm->sm_data; + if(pkt->pkt_err) + s->ths_total_err++; + s->ths_bytes += pkt->pkt_payload->pb_size; + } + streaming_target_deliver(s->ths_output, sm); } @@ -282,10 +301,12 @@ subscription_input_direct(void *opauqe, streaming_message_t *sm) */ static th_subscription_t * subscription_create(int weight, const char *name, streaming_target_t *st, - int flags, int direct) + int flags, int direct, const char *hostname, + const char *username, const char *client) { th_subscription_t *s = calloc(1, sizeof(th_subscription_t)); int reject = 0; + static int tally; if(flags & SUBSCRIPTION_RAW_MPEGTS) reject |= SMT_TO_MASK(SMT_PACKET); // Reject parsed frames @@ -298,11 +319,17 @@ subscription_create(int weight, const char *name, streaming_target_t *st, s->ths_weight = weight; s->ths_title = strdup(name); + s->ths_hostname = hostname ? strdup(hostname) : NULL; + s->ths_username = username ? strdup(username) : NULL; + s->ths_client = client ? strdup(client) : NULL; s->ths_total_err = 0; s->ths_output = st; s->ths_flags = flags; time(&s->ths_start); + + s->ths_id = ++tally; + LIST_INSERT_SORTED(&subscriptions, s, ths_global_link, subscription_sort); return s; @@ -315,9 +342,11 @@ subscription_create(int weight, const char *name, streaming_target_t *st, th_subscription_t * subscription_create_from_channel(channel_t *ch, unsigned int weight, const char *name, streaming_target_t *st, - int flags) + int flags, const char *hostname, + const char *username, const char *client) { - th_subscription_t *s = subscription_create(weight, name, st, flags, 0); + th_subscription_t *s = subscription_create(weight, name, st, flags, 0, + hostname, username, client); s->ths_channel = ch; LIST_INSERT_HEAD(&ch->ch_subscriptions, s, ths_channel_link); @@ -349,6 +378,7 @@ subscription_create_from_channel(channel_t *ch, unsigned int weight, service_source_info_free(&si); } + notify_reload("subscriptions"); return s; } @@ -360,7 +390,8 @@ th_subscription_t * subscription_create_from_service(service_t *t, const char *name, streaming_target_t *st, int flags) { - th_subscription_t *s = subscription_create(INT32_MAX, name, st, flags, 1); + th_subscription_t *s = subscription_create(INT32_MAX, name, st, flags, 1, + NULL, NULL, NULL); source_info_t si; int r; @@ -391,6 +422,7 @@ subscription_create_from_service(service_t *t, const char *name, service_source_info_free(&si); subscription_link_service(s, t); + notify_reload("subscriptions"); return s; } @@ -478,3 +510,91 @@ subscription_dummy_join(const char *id, int first) tvhlog(LOG_NOTICE, "subscription", "Dummy join %s ok", id); } + + + +/** + * + */ +htsmsg_t * +subscription_create_msg(th_subscription_t *s) +{ + htsmsg_t *m = htsmsg_create_map(); + + htsmsg_add_u32(m, "id", s->ths_id); + htsmsg_add_u32(m, "start", s->ths_start); + htsmsg_add_u32(m, "errors", s->ths_total_err); + + const char *state; + switch(s->ths_state) { + default: + state = "Idle"; + break; + + case SUBSCRIPTION_TESTING_SERVICE: + state = "Testing"; + break; + + case SUBSCRIPTION_GOT_SERVICE: + state = "Running"; + break; + + case SUBSCRIPTION_BAD_SERVICE: + state = "Bad"; + break; + } + + + htsmsg_add_str(m, "state", state); + + if (s->ths_hostname && s->ths_username && s->ths_client) { + htsmsg_add_str(m, "hostname", s->ths_hostname); + htsmsg_add_str(m, "username", s->ths_username); + htsmsg_add_str(m, "title", s->ths_client); + } else { + htsmsg_add_str(m, "title", s->ths_title); + } + + if(s->ths_channel != NULL) + htsmsg_add_str(m, "channel", s->ths_channel->ch_name); + + if(s->ths_service != NULL) + htsmsg_add_str(m, "service", s->ths_service->s_nicename); + + return m; +} + + +static gtimer_t every_sec; + +/** + * + */ +static void +every_sec_cb(void *aux) +{ + th_subscription_t *s; + gtimer_arm(&every_sec, every_sec_cb, NULL, 1); + + LIST_FOREACH(s, &subscriptions, ths_global_link) { + int errors = s->ths_total_err; + int bw = atomic_exchange(&s->ths_bytes, 0); + + htsmsg_t *m = subscription_create_msg(s); + htsmsg_delete_field(m, "errors"); + htsmsg_add_u32(m, "errors", errors); + htsmsg_add_u32(m, "bw", bw); + htsmsg_add_u32(m, "updateEntry", 1); + notify_by_msg("subscriptions", m); + } +} + + +/** + * + */ +void +subscription_init(void) +{ + gtimer_arm(&every_sec, every_sec_cb, NULL, 1); +} diff --git a/src/subscriptions.h b/src/subscriptions.h index 52738041..ff5d1d1d 100644 --- a/src/subscriptions.h +++ b/src/subscriptions.h @@ -19,9 +19,14 @@ #ifndef SUBSCRIPTIONS_H #define SUBSCRIPTIONS_H +extern struct th_subscription_list subscriptions; + #define SUBSCRIPTION_RAW_MPEGTS 0x1 typedef struct th_subscription { + + int ths_id; + LIST_ENTRY(th_subscription) ths_global_link; int ths_weight; @@ -46,6 +51,7 @@ typedef struct th_subscription { char *ths_title; /* display title */ time_t ths_start; /* time when subscription started */ int ths_total_err; /* total errors during entire subscription */ + int ths_bytes; // Reset every second to get aprox. bandwidth streaming_target_t ths_input; @@ -55,12 +61,19 @@ typedef struct th_subscription { streaming_message_t *ths_start_message; + char *ths_hostname; + char *ths_username; + char *ths_client; + + } th_subscription_t; /** * Prototypes */ +void subscription_init(void); + void subscription_unsubscribe(th_subscription_t *s); void subscription_set_weight(th_subscription_t *s, unsigned int weight); @@ -71,7 +84,10 @@ th_subscription_t *subscription_create_from_channel(struct channel *ch, unsigned int weight, const char *name, streaming_target_t *st, - int flags); + int flags, + const char *hostname, + const char *username, + const char *client); th_subscription_t *subscription_create_from_service(struct service *t, @@ -89,4 +105,7 @@ void subscription_dummy_join(const char *id, int first); int subscriptions_active(void); +struct htsmsg; +struct htsmsg *subscription_create_msg(th_subscription_t *s); + #endif /* SUBSCRIPTIONS_H */ diff --git a/src/webui/extjs.c b/src/webui/extjs.c index cf318fc5..7ede18ae 100644 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -46,6 +46,7 @@ #include "epggrab/private.h" #include "config2.h" #include "lang_codes.h" +#include "subscriptions.h" /** * @@ -138,6 +139,7 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque) extjs_load(hq, "static/app/dvr.js"); extjs_load(hq, "static/app/epggrab.js"); extjs_load(hq, "static/app/config.js"); + extjs_load(hq, "static/app/status.js"); /** * Finally, the app itself @@ -1386,6 +1388,41 @@ extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque) return 0; } + +/** + * + */ +static int +extjs_subscriptions(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + htsmsg_t *out, *array; + th_subscription_t *s; + + pthread_mutex_lock(&global_lock); + + if(http_access_verify(hc, ACCESS_ADMIN)) { + pthread_mutex_unlock(&global_lock); + return HTTP_STATUS_UNAUTHORIZED; + } + + out = htsmsg_create_map(); + array = htsmsg_create_list(); + + LIST_FOREACH(s, &subscriptions, ths_global_link) + htsmsg_add_msg(array, NULL, subscription_create_msg(s)); + + pthread_mutex_unlock(&global_lock); + + htsmsg_add_msg(out, "entries", array); + + htsmsg_json_serialize(out, hq, 0); + htsmsg_destroy(out); + http_output_content(hc, "text/x-json; charset=UTF-8"); + return 0; +} + + /** * */ @@ -1878,6 +1915,7 @@ extjs_start(void) http_path_add("/epgobject", NULL, extjs_epgobject, ACCESS_WEB_INTERFACE); http_path_add("/dvr", NULL, extjs_dvr, ACCESS_WEB_INTERFACE); http_path_add("/dvrlist", NULL, extjs_dvrlist, ACCESS_WEB_INTERFACE); + http_path_add("/subscriptions", NULL, extjs_subscriptions, ACCESS_WEB_INTERFACE); http_path_add("/ecglist", NULL, extjs_ecglist, ACCESS_WEB_INTERFACE); http_path_add("/config", NULL, extjs_config, ACCESS_WEB_INTERFACE); http_path_add("/languages", NULL, extjs_languages, ACCESS_WEB_INTERFACE); diff --git a/src/webui/static/app/status.js b/src/webui/static/app/status.js new file mode 100644 index 00000000..44f999d2 --- /dev/null +++ b/src/webui/static/app/status.js @@ -0,0 +1,134 @@ +/** + * + */ +tvheadend.status = function() { + + tvheadend.subsStore = new Ext.data.JsonStore({ + root : 'entries', + totalProperty : 'totalCount', + fields : [ { + name : 'id' + }, { + name : 'hostname' + }, { + name : 'username' + }, { + name : 'title' + }, { + name : 'channel' + }, { + name : 'service' + }, { + name : 'state' + }, { + name : 'errors' + }, { + name : 'bw' + }, { + name : 'start', + type : 'date', + dateFormat : 'U' /* unix time */ + } ], + url : 'subscriptions', + autoLoad : true, + id : 'id' + }); + + + + tvheadend.comet.on('subscriptions', function(m) { + + if (m.reload != null) tvheadend.subsStore.reload(); + + if (m.updateEntry != null) { + r = tvheadend.subsStore.getById(m.id) + if (typeof r === 'undefined') { + tvheadend.subsStore.reload(); + return; + } + + r.data.channel = m.channel; + r.data.service = m.service; + r.data.state = m.state; + r.data.errors = m.errors; + r.data.bw = m.bw + + tvheadend.subsStore.afterEdit(r); + tvheadend.subsStore.fireEvent('updated', tvheadend.subsStore, r, + Ext.data.Record.COMMIT); + } + }); + + function renderDate(value) { + var dt = new Date(value); + return dt.format('D j M H:i'); + } + + function renderBw(value) { + return parseInt(value / 125); + } + + var subsCm = new Ext.grid.ColumnModel([{ + width : 50, + id : 'hostname', + header : "Hostname", + dataIndex : 'hostname' + }, { + width : 50, + id : 'username', + header : "Username", + dataIndex : 'username' + }, { + width : 80, + id : 'title', + header : "Title", + dataIndex : 'title' + }, { + width : 50, + id : 'channel', + header : "Channel", + dataIndex : 'channel' + }, { + width : 200, + id : 'service', + header : "Service", + dataIndex : 'service', + }, { + width : 50, + id : 'start', + header : "Start", + dataIndex : 'start', + renderer : renderDate + }, { + width : 50, + id : 'state', + header : "State", + dataIndex : 'state' + }, { + width : 50, + id : 'errors', + header : "Errors", + dataIndex : 'errors' + }, { + width : 50, + id : 'bw', + header : "Bandwidth (kb/s)", + dataIndex : 'bw', + renderer: renderBw + } ]); + + var panel = new Ext.grid.GridPanel({ + loadMask : true, + stripeRows : true, + disableSelection : true, + title : 'Active subscriptions', + iconCls : 'eye', + store : tvheadend.subsStore, + cm : subsCm, + viewConfig : { + forceFit : true + } + }); + return panel; +} + diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index ab942852..3b44f6ca 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -244,6 +244,11 @@ function accessUpdate(o) { tvheadend.rootTabPanel.add(tvheadend.confpanel); } + if (o.admin == true && tvheadend.statuspanel == null) { + tvheadend.statuspanel = new tvheadend.status; + tvheadend.rootTabPanel.add(tvheadend.statuspanel); + } + if (tvheadend.aboutPanel == null) { tvheadend.aboutPanel = new Ext.Panel({ border : false, diff --git a/src/webui/webui.c b/src/webui/webui.c index 6c1041e7..c4f0b787 100644 --- a/src/webui/webui.c +++ b/src/webui/webui.c @@ -607,7 +607,8 @@ http_stream_channel(http_connection_t *hc, channel_t *ch) } pthread_mutex_lock(&global_lock); - s = subscription_create_from_channel(ch, priority, "HTTP", st, flags); + s = subscription_create_from_channel(ch, priority, "HTTP", st, flags, + NULL, NULL, NULL); pthread_mutex_unlock(&global_lock); if(s) {