tvheadend/src/webui/webui.c

1372 lines
34 KiB
C

/*
* tvheadend, WEBUI / HTML user interface
* Copyright (C) 2008 Andreas Öman
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define _GNU_SOURCE /* for splice() */
#include <fcntl.h>
#include <pthread.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include "tvheadend.h"
#include "access.h"
#include "http.h"
#include "webui.h"
#include "dvr/dvr.h"
#include "filebundle.h"
#include "streaming.h"
#include "plumbing/tsfix.h"
#include "plumbing/globalheaders.h"
#include "plumbing/transcoding.h"
#include "epg.h"
#include "muxer.h"
#include "imagecache.h"
#include "tcp.h"
#include "config.h"
#include "atomic.h"
#if defined(PLATFORM_LINUX)
#include <sys/sendfile.h>
#elif defined(PLATFORM_FREEBSD)
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#endif
#if ENABLE_ANDROID
#include <sys/socket.h>
#endif
static int webui_xspf;
/**
*
*/
static int
is_client_simple(http_connection_t *hc)
{
char *c;
if((c = http_arg_get(&hc->hc_args, "UA-OS")) != NULL) {
if(strstr(c, "Windows CE") || strstr(c, "Pocket PC"))
return 1;
}
if((c = http_arg_get(&hc->hc_args, "x-wap-profile")) != NULL) {
return 1;
}
return 0;
}
#if ENABLE_LIBAV
static int
http_get_transcoder_properties(struct http_arg_list *args,
transcoder_props_t *props)
{
int transcode;
const char *s;
memset(props, 0, sizeof(transcoder_props_t));
if ((s = http_arg_get(args, "transcode")))
transcode = atoi(s);
else
transcode = 0;
if ((s = http_arg_get(args, "resolution")))
props->tp_resolution = atoi(s);
else
props->tp_resolution = 384;
if ((s = http_arg_get(args, "channels")))
props->tp_channels = atoi(s);
else
props->tp_channels = 0; //same as source
if ((s = http_arg_get(args, "bandwidth")))
props->tp_bandwidth = atoi(s);
else
props->tp_bandwidth = 0; //same as source
if ((s = http_arg_get(args, "language")))
strncpy(props->tp_language, s, 3);
else
strncpy(props->tp_language, config_get_language() ?: "", 3);
if ((s = http_arg_get(args, "vcodec")))
props->tp_vcodec = streaming_component_txt2type(s);
else
props->tp_vcodec = SCT_UNKNOWN;
if ((s = http_arg_get(args, "acodec")))
props->tp_acodec = streaming_component_txt2type(s);
else
props->tp_acodec = SCT_UNKNOWN;
if ((s = http_arg_get(args, "scodec")))
props->tp_scodec = streaming_component_txt2type(s);
else
props->tp_scodec = SCT_UNKNOWN;
return transcode && transcoding_enabled;
}
#endif
/**
* Root page, we direct the client to different pages depending
* on if it is a full blown browser or just some mobile app
*/
static int
page_root(http_connection_t *hc, const char *remain, void *opaque)
{
if(is_client_simple(hc)) {
http_redirect(hc, "simple.html", &hc->hc_req_args);
} else {
http_redirect(hc, "extjs.html", &hc->hc_req_args);
}
return 0;
}
static int
page_root2(http_connection_t *hc, const char *remain, void *opaque)
{
if (!tvheadend_webroot) return 1;
char *tmp = malloc(strlen(tvheadend_webroot) + 2);
sprintf(tmp, "%s/", tvheadend_webroot);
http_redirect(hc, tmp, &hc->hc_req_args);
free(tmp);
return 0;
}
static int
page_login(http_connection_t *hc, const char *remain, void *opaque)
{
if (hc->hc_access != NULL &&
hc->hc_access->aa_username != NULL &&
hc->hc_access->aa_username != '\0') {
http_redirect(hc, "/", &hc->hc_req_args);
return 0;
} else {
return HTTP_STATUS_UNAUTHORIZED;
}
}
static int
page_logout(http_connection_t *hc, const char *remain, void *opaque)
{
if (hc->hc_access == NULL ||
hc->hc_access->aa_username == NULL ||
hc->hc_access->aa_username == '\0') {
redirect:
http_redirect(hc, "/", &hc->hc_req_args);
return 0;
} else {
const char *s = http_arg_get(&hc->hc_args, "Cookie");
if (s) {
while (*s && *s != ';')
s++;
if (*s) s++;
while (*s && *s <= ' ') s++;
if (!strncmp(s, "logout=1", 8)) {
hc->hc_logout_cookie = 2;
goto redirect;
}
hc->hc_logout_cookie = 1;
}
return HTTP_STATUS_UNAUTHORIZED;
}
}
/**
* Static download of a file from the filesystem
*/
int
page_static_file(http_connection_t *hc, const char *remain, void *opaque)
{
int ret = 0;
const char *base = opaque;
char path[500];
ssize_t size;
const char *content = NULL, *postfix;
char buf[4096];
const char *gzip;
if(remain == NULL)
return 404;
if(strstr(remain, ".."))
return HTTP_STATUS_BAD_REQUEST;
snprintf(path, sizeof(path), "%s/%s", base, remain);
postfix = strrchr(remain, '.');
if(postfix != NULL) {
postfix++;
if(!strcmp(postfix, "js"))
content = "text/javascript; charset=UTF-8";
else if(!strcmp(postfix, "css"))
content = "text/css; charset=UTF-8";
}
// TODO: handle compression
fb_file *fp = fb_open(path, 0, 1);
if (!fp) {
tvhlog(LOG_ERR, "webui", "failed to open %s", path);
return 500;
}
size = fb_size(fp);
gzip = fb_gzipped(fp) ? "gzip" : NULL;
http_send_header(hc, 200, content, size, gzip, NULL, 10, 0, NULL);
while (!fb_eof(fp)) {
ssize_t c = fb_read(fp, buf, sizeof(buf));
if (c < 0) {
ret = 500;
break;
}
if (tvh_write(hc->hc_fd, buf, c)) {
ret = 500;
break;
}
}
fb_close(fp);
return ret;
}
/**
* HTTP stream loop
*/
static void
http_stream_run(http_connection_t *hc, streaming_queue_t *sq,
const char *name, muxer_container_type_t mc,
th_subscription_t *s, muxer_config_t *mcfg)
{
streaming_message_t *sm;
int run = 1;
int started = 0;
muxer_t *mux = NULL;
int timeouts = 0, grace = 20;
struct timespec ts;
struct timeval tp;
int err = 0;
socklen_t errlen = sizeof(err);
mux = muxer_create(mc, mcfg);
if(muxer_open_stream(mux, hc->hc_fd))
run = 0;
/* reduce timeout on write() for streaming */
tp.tv_sec = 5;
tp.tv_usec = 0;
setsockopt(hc->hc_fd, SOL_SOCKET, SO_SNDTIMEO, &tp, sizeof(tp));
while(run && tvheadend_running) {
pthread_mutex_lock(&sq->sq_mutex);
sm = TAILQ_FIRST(&sq->sq_queue);
if(sm == NULL) {
gettimeofday(&tp, NULL);
ts.tv_sec = tp.tv_sec + 1;
ts.tv_nsec = tp.tv_usec * 1000;
if(pthread_cond_timedwait(&sq->sq_cond, &sq->sq_mutex, &ts) == ETIMEDOUT) {
timeouts++;
//Check socket status
getsockopt(hc->hc_fd, SOL_SOCKET, SO_ERROR, (char *)&err, &errlen);
if (err) {
tvhlog(LOG_DEBUG, "webui", "Stop streaming %s, client hung up", hc->hc_url_orig);
run = 0;
} else if(timeouts >= grace) {
tvhlog(LOG_WARNING, "webui", "Stop streaming %s, timeout waiting for packets", hc->hc_url_orig);
run = 0;
}
}
pthread_mutex_unlock(&sq->sq_mutex);
continue;
}
timeouts = 0; //Reset timeout counter
TAILQ_REMOVE(&sq->sq_queue, sm, sm_link);
pthread_mutex_unlock(&sq->sq_mutex);
switch(sm->sm_type) {
case SMT_MPEGTS:
case SMT_PACKET:
if(started) {
pktbuf_t *pb;;
if (sm->sm_type == SMT_PACKET)
pb = ((th_pkt_t*)sm->sm_data)->pkt_payload;
else
pb = sm->sm_data;
atomic_add(&s->ths_bytes_out, pktbuf_len(pb));
muxer_write_pkt(mux, sm->sm_type, sm->sm_data);
sm->sm_data = NULL;
}
break;
case SMT_GRACE:
grace = sm->sm_code < 5 ? 5 : grace;
break;
case SMT_START:
grace = 10;
if(!started) {
tvhlog(LOG_DEBUG, "webui", "Start streaming %s", hc->hc_url_orig);
http_output_content(hc, muxer_mime(mux, sm->sm_data));
if(muxer_init(mux, sm->sm_data, name) < 0)
run = 0;
started = 1;
} else if(muxer_reconfigure(mux, sm->sm_data) < 0) {
tvhlog(LOG_WARNING, "webui", "Unable to reconfigure stream %s", hc->hc_url_orig);
}
break;
case SMT_STOP:
if(sm->sm_code != SM_CODE_SOURCE_RECONFIGURED) {
tvhlog(LOG_WARNING, "webui", "Stop streaming %s, %s", hc->hc_url_orig,
streaming_code2txt(sm->sm_code));
run = 0;
}
break;
case SMT_SERVICE_STATUS:
if(getsockopt(hc->hc_fd, SOL_SOCKET, SO_ERROR, &err, &errlen)) {
tvhlog(LOG_DEBUG, "webui", "Stop streaming %s, client hung up",
hc->hc_url_orig);
run = 0;
}
break;
case SMT_SKIP:
case SMT_SPEED:
case SMT_SIGNAL_STATUS:
case SMT_TIMESHIFT_STATUS:
break;
case SMT_NOSTART:
tvhlog(LOG_WARNING, "webui", "Couldn't start streaming %s, %s",
hc->hc_url_orig, streaming_code2txt(sm->sm_code));
run = 0;
break;
case SMT_EXIT:
tvhlog(LOG_WARNING, "webui", "Stop streaming %s, %s", hc->hc_url_orig,
streaming_code2txt(sm->sm_code));
run = 0;
break;
}
streaming_msg_free(sm);
if(mux->m_errors) {
if (!mux->m_eos)
tvhlog(LOG_WARNING, "webui", "Stop streaming %s, muxer reported errors", hc->hc_url_orig);
run = 0;
}
}
if(started)
muxer_close(mux);
muxer_destroy(mux);
}
/**
* Output a playlist containing a single channel
*/
static int
http_channel_playlist(http_connection_t *hc, channel_t *channel)
{
htsbuf_queue_t *hq;
char buf[255];
const char *host;
muxer_container_type_t mc;
if (http_access_verify_channel(hc, ACCESS_STREAMING, channel, 1))
return HTTP_STATUS_UNAUTHORIZED;
mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux"));
if(mc == MC_UNKNOWN)
mc = dvr_config_find_by_name_default(NULL)->dvr_mc;
hq = &hc->hc_reply;
host = http_arg_get(&hc->hc_args, "Host");
snprintf(buf, sizeof(buf), "/stream/channelid/%d", channel_get_id(channel));
htsbuf_qprintf(hq, "#EXTM3U\n");
htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", channel_get_name(channel));
htsbuf_qprintf(hq, "http://%s%s?ticket=%s", host, buf,
access_ticket_create(buf));
#if ENABLE_LIBAV
transcoder_props_t props;
if(http_get_transcoder_properties(&hc->hc_req_args, &props)) {
htsbuf_qprintf(hq, "&transcode=1");
if(props.tp_resolution)
htsbuf_qprintf(hq, "&resolution=%d", props.tp_resolution);
if(props.tp_channels)
htsbuf_qprintf(hq, "&channels=%d", props.tp_channels);
if(props.tp_bandwidth)
htsbuf_qprintf(hq, "&bandwidth=%d", props.tp_bandwidth);
if(props.tp_language[0])
htsbuf_qprintf(hq, "&language=%s", props.tp_language);
if(props.tp_vcodec)
htsbuf_qprintf(hq, "&vcodec=%s", streaming_component_type2txt(props.tp_vcodec));
if(props.tp_acodec)
htsbuf_qprintf(hq, "&acodec=%s", streaming_component_type2txt(props.tp_acodec));
if(props.tp_scodec)
htsbuf_qprintf(hq, "&scodec=%s", streaming_component_type2txt(props.tp_scodec));
}
#endif
htsbuf_qprintf(hq, "&mux=%s\n", muxer_container_type2txt(mc));
http_output_content(hc, "audio/x-mpegurl");
return 0;
}
/**
* Output a playlist containing all channels with a specific tag
*/
static int
http_tag_playlist(http_connection_t *hc, channel_tag_t *tag)
{
htsbuf_queue_t *hq;
char buf[255];
channel_tag_mapping_t *ctm;
const char *host;
muxer_container_type_t mc;
hq = &hc->hc_reply;
host = http_arg_get(&hc->hc_args, "Host");
mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux"));
if(mc == MC_UNKNOWN)
mc = dvr_config_find_by_name_default(NULL)->dvr_mc;
htsbuf_qprintf(hq, "#EXTM3U\n");
LIST_FOREACH(ctm, &tag->ct_ctms, ctm_tag_link) {
if (http_access_verify_channel(hc, ACCESS_STREAMING, ctm->ctm_channel, 0))
continue;
snprintf(buf, sizeof(buf), "/stream/channelid/%d", channel_get_id(ctm->ctm_channel));
htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", channel_get_name(ctm->ctm_channel));
htsbuf_qprintf(hq, "http://%s%s?ticket=%s", host, buf,
access_ticket_create(buf));
htsbuf_qprintf(hq, "&mux=%s\n", muxer_container_type2txt(mc));
}
http_output_content(hc, "audio/x-mpegurl");
return 0;
}
/**
* Output a playlist pointing to tag-specific playlists
*/
static int
http_tag_list_playlist(http_connection_t *hc)
{
htsbuf_queue_t *hq;
char buf[255];
channel_tag_t *ct;
const char *host;
muxer_container_type_t mc;
hq = &hc->hc_reply;
host = http_arg_get(&hc->hc_args, "Host");
mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux"));
if(mc == MC_UNKNOWN)
mc = dvr_config_find_by_name_default(NULL)->dvr_mc;
htsbuf_qprintf(hq, "#EXTM3U\n");
TAILQ_FOREACH(ct, &channel_tags, ct_link) {
if(!ct->ct_enabled || ct->ct_internal)
continue;
snprintf(buf, sizeof(buf), "/playlist/tagid/%d", idnode_get_short_uuid(&ct->ct_id));
htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", ct->ct_name);
htsbuf_qprintf(hq, "http://%s%s?ticket=%s", host, buf,
access_ticket_create(buf));
htsbuf_qprintf(hq, "&mux=%s\n", muxer_container_type2txt(mc));
}
http_output_content(hc, "audio/x-mpegurl");
return 0;
}
/**
* Output a flat playlist with all channels
*/
static int
http_channel_list_playlist_cmp(const void *a, const void *b)
{
channel_t *c1 = *(channel_t **)a, *c2 = *(channel_t **)b;
int r = channel_get_number(c1) - channel_get_number(c2);
if (r == 0)
r = strcasecmp(channel_get_name(c1), channel_get_name(c2));
return r;
}
static int
http_channel_list_playlist(http_connection_t *hc)
{
htsbuf_queue_t *hq;
char buf[255];
channel_t *ch;
channel_t **chlist;
const char *host;
int idx = 0, count = 0;
muxer_container_type_t mc;
hq = &hc->hc_reply;
host = http_arg_get(&hc->hc_args, "Host");
mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux"));
if(mc == MC_UNKNOWN)
mc = dvr_config_find_by_name_default(NULL)->dvr_mc;
CHANNEL_FOREACH(ch)
count++;
chlist = malloc(count * sizeof(channel_t *));
CHANNEL_FOREACH(ch)
chlist[idx++] = ch;
assert(idx == count);
qsort(chlist, count, sizeof(channel_t *), http_channel_list_playlist_cmp);
htsbuf_qprintf(hq, "#EXTM3U\n");
for (idx = 0; idx < count; idx++) {
ch = chlist[idx];
if (http_access_verify_channel(hc, ACCESS_STREAMING, ch, 0))
continue;
snprintf(buf, sizeof(buf), "/stream/channelid/%d", channel_get_id(ch));
htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", channel_get_name(ch));
htsbuf_qprintf(hq, "http://%s%s?ticket=%s", host, buf,
access_ticket_create(buf));
htsbuf_qprintf(hq, "&mux=%s\n", muxer_container_type2txt(mc));
}
free(chlist);
http_output_content(hc, "audio/x-mpegurl");
return 0;
}
/**
* Output a playlist of all recordings.
*/
static int
http_dvr_list_playlist(http_connection_t *hc)
{
htsbuf_queue_t *hq;
char buf[255];
dvr_entry_t *de;
const char *host, *uuid;
off_t fsize;
time_t durration;
struct tm tm;
int bandwidth;
hq = &hc->hc_reply;
host = http_arg_get(&hc->hc_args, "Host");
htsbuf_qprintf(hq, "#EXTM3U\n");
LIST_FOREACH(de, &dvrentries, de_global_link) {
fsize = dvr_get_filesize(de);
if(!fsize)
continue;
if (de->de_channel &&
http_access_verify_channel(hc, ACCESS_RECORDER, de->de_channel, 0))
continue;
durration = dvr_entry_get_stop_time(de) - dvr_entry_get_start_time(de);
bandwidth = ((8*fsize) / (durration*1024.0));
strftime(buf, sizeof(buf), "%FT%T%z", localtime_r(&(de->de_start), &tm));
htsbuf_qprintf(hq, "#EXTINF:%"PRItime_t",%s\n", durration, lang_str_get(de->de_title, NULL));
htsbuf_qprintf(hq, "#EXT-X-TARGETDURATION:%"PRItime_t"\n", durration);
uuid = idnode_uuid_as_str(&de->de_id);
htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%s,BANDWIDTH=%d\n", uuid, bandwidth);
htsbuf_qprintf(hq, "#EXT-X-PROGRAM-DATE-TIME:%s\n", buf);
snprintf(buf, sizeof(buf), "/dvrfile/%s", uuid);
htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf,
access_ticket_create(buf));
}
http_output_content(hc, "audio/x-mpegurl");
return 0;
}
/**
* Output a playlist with a http stream for a dvr entry (.m3u format)
*/
static int
http_dvr_playlist(http_connection_t *hc, dvr_entry_t *de)
{
htsbuf_queue_t *hq = &hc->hc_reply;
char buf[255];
const char *ticket_id = NULL, *uuid;
time_t durration = 0;
off_t fsize = 0;
int bandwidth = 0;
struct tm tm;
const char *host = http_arg_get(&hc->hc_args, "Host");
durration = dvr_entry_get_stop_time(de) - dvr_entry_get_start_time(de);
fsize = dvr_get_filesize(de);
if(fsize) {
bandwidth = ((8*fsize) / (durration*1024.0));
strftime(buf, sizeof(buf), "%FT%T%z", localtime_r(&(de->de_start), &tm));
htsbuf_qprintf(hq, "#EXTM3U\n");
htsbuf_qprintf(hq, "#EXTINF:%"PRItime_t",%s\n", durration, lang_str_get(de->de_title, NULL));
htsbuf_qprintf(hq, "#EXT-X-TARGETDURATION:%"PRItime_t"\n", durration);
uuid = idnode_uuid_as_str(&de->de_id);
htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%s,BANDWIDTH=%d\n", uuid, bandwidth);
htsbuf_qprintf(hq, "#EXT-X-PROGRAM-DATE-TIME:%s\n", buf);
snprintf(buf, sizeof(buf), "/dvrfile/%s", uuid);
ticket_id = access_ticket_create(buf);
htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf, ticket_id);
http_output_content(hc, "application/x-mpegURL");
} else {
http_error(hc, HTTP_STATUS_NOT_FOUND);
return HTTP_STATUS_NOT_FOUND;
}
return 0;
}
/**
* Handle requests for playlists.
*/
static int
page_http_playlist(http_connection_t *hc, const char *remain, void *opaque)
{
char *components[2];
int nc, r;
channel_t *ch = NULL;
dvr_entry_t *de = NULL;
channel_tag_t *tag = NULL;
if(!remain) {
http_redirect(hc, "/playlist/channels", &hc->hc_req_args);
return HTTP_STATUS_FOUND;
}
nc = http_tokenize((char *)remain, components, 2, '/');
if(!nc) {
http_error(hc, HTTP_STATUS_BAD_REQUEST);
return HTTP_STATUS_BAD_REQUEST;
}
if(nc == 2)
http_deescape(components[1]);
pthread_mutex_lock(&global_lock);
if(nc == 2 && !strcmp(components[0], "channelid"))
ch = channel_find_by_id(atoi(components[1]));
else if(nc == 2 && !strcmp(components[0], "channelnumber"))
ch = channel_find_by_number(atoi(components[1]));
else if(nc == 2 && !strcmp(components[0], "channelname"))
ch = channel_find_by_name(components[1]);
else if(nc == 2 && !strcmp(components[0], "channel"))
ch = channel_find(components[1]);
else if(nc == 2 && !strcmp(components[0], "dvrid"))
de = dvr_entry_find_by_id(atoi(components[1]));
else if(nc == 2 && !strcmp(components[0], "tagid"))
tag = channel_tag_find_by_identifier(atoi(components[1]));
else if(nc == 2 && !strcmp(components[0], "tag"))
tag = channel_tag_find_by_name(components[1], 0);
if(ch)
r = http_channel_playlist(hc, ch);
else if(tag)
r = http_tag_playlist(hc, tag);
else if(de)
r = http_dvr_playlist(hc, de);
else if(!strcmp(components[0], "tags"))
r = http_tag_list_playlist(hc);
else if(!strcmp(components[0], "channels"))
r = http_channel_list_playlist(hc);
else if(!strcmp(components[0], "channels.m3u"))
r = http_channel_list_playlist(hc);
else if(!strcmp(components[0], "recordings"))
r = http_dvr_list_playlist(hc);
else {
http_error(hc, HTTP_STATUS_BAD_REQUEST);
r = HTTP_STATUS_BAD_REQUEST;
}
pthread_mutex_unlock(&global_lock);
return r;
}
/**
* Subscribes to a service and starts the streaming loop
*/
static int
http_stream_service(http_connection_t *hc, service_t *service, int weight)
{
streaming_queue_t sq;
th_subscription_t *s;
streaming_target_t *gh;
streaming_target_t *tsfix;
streaming_target_t *st;
dvr_config_t *cfg;
muxer_container_type_t mc;
int flags = SUBSCRIPTION_STREAMING;
const char *str;
size_t qsize;
const char *name;
char addrbuf[50];
if(http_access_verify(hc, ACCESS_ADVANCED_STREAMING))
return HTTP_STATUS_UNAUTHORIZED;
cfg = dvr_config_find_by_name_default(NULL);
/* Build muxer config - this takes the defaults from the default dvr config, which is a hack */
mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux"));
if(mc == MC_UNKNOWN) {
mc = cfg->dvr_mc;
}
if ((str = http_arg_get(&hc->hc_req_args, "qsize")))
qsize = atoll(str);
else
qsize = 1500000;
if(mc == MC_PASS || mc == MC_RAW) {
streaming_queue_init2(&sq, SMT_PACKET, qsize);
gh = NULL;
tsfix = NULL;
st = &sq.sq_st;
flags |= SUBSCRIPTION_RAW_MPEGTS;
} else {
streaming_queue_init2(&sq, 0, qsize);
gh = globalheaders_create(&sq.sq_st);
tsfix = tsfix_create(gh);
st = tsfix;
}
tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrbuf, 50);
s = subscription_create_from_service(service, weight ?: 100, "HTTP", st, flags,
addrbuf,
hc->hc_username,
http_arg_get(&hc->hc_args, "User-Agent"));
if(s) {
name = tvh_strdupa(service->s_nicename);
pthread_mutex_unlock(&global_lock);
http_stream_run(hc, &sq, name, mc, s, &cfg->dvr_muxcnf);
pthread_mutex_lock(&global_lock);
subscription_unsubscribe(s);
}
if(gh)
globalheaders_destroy(gh);
if(tsfix)
tsfix_destroy(tsfix);
streaming_queue_deinit(&sq);
return 0;
}
/**
* Subscribe to a mux for grabbing a raw dump
*
* TODO: can't currently force this to be on a particular input
*/
#if ENABLE_MPEGTS
#include "input.h"
static int
http_stream_mux(http_connection_t *hc, mpegts_mux_t *mm, int weight)
{
th_subscription_t *s;
streaming_queue_t sq;
const char *name;
char addrbuf[50];
muxer_config_t muxcfg = { 0 };
if(http_access_verify(hc, ACCESS_ADVANCED_STREAMING))
return HTTP_STATUS_UNAUTHORIZED;
streaming_queue_init(&sq, SMT_PACKET);
tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrbuf, 50);
s = subscription_create_from_mux(mm, weight ?: 10, "HTTP", &sq.sq_st,
SUBSCRIPTION_RAW_MPEGTS |
SUBSCRIPTION_FULLMUX |
SUBSCRIPTION_STREAMING,
addrbuf, hc->hc_username,
http_arg_get(&hc->hc_args, "User-Agent"), NULL);
if (!s)
return HTTP_STATUS_BAD_REQUEST;
name = tvh_strdupa(s->ths_title);
pthread_mutex_unlock(&global_lock);
http_stream_run(hc, &sq, name, MC_RAW, s, &muxcfg);
pthread_mutex_lock(&global_lock);
subscription_unsubscribe(s);
streaming_queue_deinit(&sq);
return 0;
}
#endif
/**
* Subscribes to a channel and starts the streaming loop
*/
static int
http_stream_channel(http_connection_t *hc, channel_t *ch, int weight)
{
streaming_queue_t sq;
th_subscription_t *s;
streaming_target_t *gh;
streaming_target_t *tsfix;
streaming_target_t *st;
#if ENABLE_LIBAV
streaming_target_t *tr = NULL;
#endif
dvr_config_t *cfg;
int flags = SUBSCRIPTION_STREAMING;
muxer_container_type_t mc;
char *str;
size_t qsize;
const char *name;
char addrbuf[50];
if (http_access_verify_channel(hc, ACCESS_STREAMING, ch, 1))
return HTTP_STATUS_UNAUTHORIZED;
cfg = dvr_config_find_by_name_default(NULL);
/* Build muxer config - this takes the defaults from the default dvr config, which is a hack */
mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux"));
if(mc == MC_UNKNOWN) {
mc = cfg->dvr_mc;
}
if ((str = http_arg_get(&hc->hc_req_args, "qsize")))
qsize = atoll(str);
else
qsize = 1500000;
if(mc == MC_PASS || mc == MC_RAW) {
streaming_queue_init2(&sq, SMT_PACKET, qsize);
gh = NULL;
tsfix = NULL;
st = &sq.sq_st;
flags |= SUBSCRIPTION_RAW_MPEGTS;
} else {
streaming_queue_init2(&sq, 0, qsize);
gh = globalheaders_create(&sq.sq_st);
#if ENABLE_LIBAV
transcoder_props_t props;
if(http_get_transcoder_properties(&hc->hc_req_args, &props)) {
tr = transcoder_create(gh);
transcoder_set_properties(tr, &props);
tsfix = tsfix_create(tr);
} else
#endif
tsfix = tsfix_create(gh);
st = tsfix;
}
tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrbuf, 50);
s = subscription_create_from_channel(ch, weight ?: 100, "HTTP", st, flags,
addrbuf,
hc->hc_username,
http_arg_get(&hc->hc_args, "User-Agent"));
if(s) {
name = tvh_strdupa(channel_get_name(ch));
pthread_mutex_unlock(&global_lock);
http_stream_run(hc, &sq, name, mc, s, &cfg->dvr_muxcnf);
pthread_mutex_lock(&global_lock);
subscription_unsubscribe(s);
}
if(gh)
globalheaders_destroy(gh);
#if ENABLE_LIBAV
if(tr)
transcoder_destroy(tr);
#endif
if(tsfix)
tsfix_destroy(tsfix);
streaming_queue_deinit(&sq);
return 0;
}
/**
* Handle the http request. http://tvheadend/stream/channelid/<chid>
* http://tvheadend/stream/channel/<chname>
* http://tvheadend/stream/service/<servicename>
* http://tvheadend/stream/mux/<muxid>
*/
static int
http_stream(http_connection_t *hc, const char *remain, void *opaque)
{
char *components[2];
channel_t *ch = NULL;
service_t *service = NULL;
#if ENABLE_MPEGTS
mpegts_mux_t *mm = NULL;
#endif
const char *str;
int weight = 0;
hc->hc_keep_alive = 0;
if(remain == NULL) {
http_error(hc, HTTP_STATUS_BAD_REQUEST);
return HTTP_STATUS_BAD_REQUEST;
}
if(http_tokenize((char *)remain, components, 2, '/') != 2) {
http_error(hc, HTTP_STATUS_BAD_REQUEST);
return HTTP_STATUS_BAD_REQUEST;
}
http_deescape(components[1]);
if ((str = http_arg_get(&hc->hc_req_args, "weight")))
weight = atoi(str);
scopedgloballock();
if(!strcmp(components[0], "channelid")) {
ch = channel_find_by_id(atoi(components[1]));
} else if(!strcmp(components[0], "channelnumber")) {
ch = channel_find_by_number(atoi(components[1]));
} else if(!strcmp(components[0], "channelname")) {
ch = channel_find_by_name(components[1]);
} else if(!strcmp(components[0], "channel")) {
ch = channel_find(components[1]);
} else if(!strcmp(components[0], "service")) {
service = service_find_by_identifier(components[1]);
#if ENABLE_MPEGTS
} else if(!strcmp(components[0], "mux")) {
// TODO: do we want to be able to force starting a particular instance
mm = mpegts_mux_find(components[1]);
#endif
}
if(ch != NULL) {
return http_stream_channel(hc, ch, weight);
} else if(service != NULL) {
return http_stream_service(hc, service, weight);
#if ENABLE_MPEGTS
} else if(mm != NULL) {
return http_stream_mux(hc, mm, weight);
#endif
} else {
http_error(hc, HTTP_STATUS_BAD_REQUEST);
return HTTP_STATUS_BAD_REQUEST;
}
}
/**
* Generate a xspf playlist
* http://en.wikipedia.org/wiki/XML_Shareable_Playlist_Format
*/
static int
page_xspf(http_connection_t *hc, const char *remain, void *opaque)
{
size_t maxlen;
char *buf;
const char *host = http_arg_get(&hc->hc_args, "Host");
const char *title;
size_t len;
if ((title = http_arg_get(&hc->hc_req_args, "title")) == NULL)
title = "TVHeadend Stream";
maxlen = strlen(remain) + strlen(title) + 256;
buf = alloca(maxlen);
snprintf(buf, maxlen, "\
<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n\
<playlist version=\"1\" xmlns=\"http://xspf.org/ns/0/\">\r\n\
<trackList>\r\n\
<track>\r\n\
<title>%s</title>\r\n\
<location>http://%s/%s</location>\r\n\
</track>\r\n\
</trackList>\r\n\
</playlist>\r\n", title, host, remain);
len = strlen(buf);
http_send_header(hc, 200, "application/xspf+xml", len, 0, NULL, 10, 0, NULL);
tvh_write(hc->hc_fd, buf, len);
return 0;
}
/**
* Generate an M3U playlist
* http://en.wikipedia.org/wiki/M3U
*/
static int
page_m3u(http_connection_t *hc, const char *remain, void *opaque)
{
size_t maxlen;
char *buf;
const char *host = http_arg_get(&hc->hc_args, "Host");
const char *title;
size_t len;
if ((title = http_arg_get(&hc->hc_req_args, "title")) == NULL)
title = "TVHeadend Stream";
maxlen = strlen(remain) + strlen(title) + 256;
buf = alloca(maxlen);
snprintf(buf, maxlen, "\
#EXTM3U\r\n\
#EXTINF:-1,%s\r\n\
http://%s/%s\r\n", title, host, remain);
len = strlen(buf);
http_send_header(hc, 200, "audio/x-mpegurl", len, 0, NULL, 10, 0, NULL);
tvh_write(hc->hc_fd, buf, len);
return 0;
}
static char *
page_play_path_modify(http_connection_t *hc, const char *path, int *cut)
{
/*
* For curl, wget and TVHeadend do not send the playlist, stream directly
*/
const char *agent = http_arg_get(&hc->hc_args, "User-Agent");
if (strncasecmp(agent, "curl/", 5) == 0 ||
strncasecmp(agent, "wget/", 5) == 0)
return strdup(path + 5);
if (strncasecmp(agent, "TVHeadend/", 10) == 0)
return strdup(path + 10);
return NULL;
}
static int
page_play(http_connection_t *hc, const char *remain, void *opaque)
{
char *playlist;
if(remain == NULL)
return 404;
playlist = http_arg_get(&hc->hc_req_args, "playlist");
if (playlist) {
if (strcmp(playlist, "xspf") == 0)
return page_xspf(hc, remain, opaque);
if (strcmp(playlist, "m3u") == 0)
return page_m3u(hc, remain, opaque);
}
if (webui_xspf)
return page_xspf(hc, remain, opaque);
return page_m3u(hc, remain, opaque);
}
/**
* Download a recorded file
*/
static int
page_dvrfile(http_connection_t *hc, const char *remain, void *opaque)
{
int fd, i;
struct stat st;
const char *content = NULL, *range;
dvr_entry_t *de;
char *fname;
char *basename;
char range_buf[255];
char disposition[256];
off_t content_len, chunk;
intmax_t file_start, file_end;
#if defined(PLATFORM_LINUX)
ssize_t r;
#elif defined(PLATFORM_FREEBSD) || defined(PLATFORM_DARWIN)
off_t r;
#endif
if(remain == NULL)
return 404;
pthread_mutex_lock(&global_lock);
de = dvr_entry_find_by_uuid(remain);
if (de == NULL)
de = dvr_entry_find_by_id(atoi(remain));
if(de == NULL || de->de_filename == NULL) {
pthread_mutex_unlock(&global_lock);
return 404;
}
fname = strdup(de->de_filename);
content = muxer_container_type2mime(de->de_mc, 1);
pthread_mutex_unlock(&global_lock);
basename = strrchr(fname, '/');
if (basename) {
basename++; /* Skip '/' */
snprintf(disposition, sizeof(disposition), "attachment; filename=\"%s\"", basename);
// Ensure there are no " characters in the filename.
i = strlen(disposition)-2;
while (i > 21) {
if (disposition[i] == '"') { disposition[i] = '_'; }
i--;
}
} else {
disposition[0] = 0;
}
fd = tvh_open(fname, O_RDONLY, 0);
free(fname);
if(fd < 0)
return 404;
if(fstat(fd, &st) < 0) {
close(fd);
return 404;
}
file_start = 0;
file_end = st.st_size-1;
range = http_arg_get(&hc->hc_args, "Range");
if(range != NULL)
sscanf(range, "bytes=%jd-%jd", &file_start, &file_end);
//Sanity checks
if(file_start < 0 || file_start >= st.st_size) {
close(fd);
return 200;
}
if(file_end < 0 || file_end >= st.st_size) {
close(fd);
return 200;
}
if(file_start > file_end) {
close(fd);
return 200;
}
content_len = file_end - file_start+1;
sprintf(range_buf, "bytes %jd-%jd/%zd",
file_start, file_end, (size_t)st.st_size);
if(file_start > 0)
lseek(fd, file_start, SEEK_SET);
http_send_header(hc, range ? HTTP_STATUS_PARTIAL_CONTENT : HTTP_STATUS_OK,
content, content_len, NULL, NULL, 10,
range ? range_buf : NULL,
disposition[0] ? disposition : NULL);
if(!hc->hc_no_output) {
while(content_len > 0) {
chunk = MIN(1024 * 1024 * 1024, content_len);
#if defined(PLATFORM_LINUX)
r = sendfile(hc->hc_fd, fd, NULL, chunk);
#elif defined(PLATFORM_FREEBSD)
sendfile(fd, hc->hc_fd, 0, chunk, NULL, &r, 0);
#elif defined(PLATFORM_DARWIN)
r = chunk;
sendfile(fd, hc->hc_fd, 0, NULL, &r, 0);
#endif
if(r == -1) {
close(fd);
return -1;
}
content_len -= r;
}
}
close(fd);
return 0;
}
/**
* Fetch image cache image
*/
/**
* Static download of a file from the filesystem
*/
static int
page_imagecache(http_connection_t *hc, const char *remain, void *opaque)
{
uint32_t id;
int fd;
char buf[8192];
struct stat st;
ssize_t c;
if(remain == NULL)
return 404;
if(sscanf(remain, "%d", &id) != 1)
return HTTP_STATUS_BAD_REQUEST;
/* Fetch details */
pthread_mutex_lock(&global_lock);
fd = imagecache_open(id);
pthread_mutex_unlock(&global_lock);
/* Check result */
if (fd < 0)
return 404;
if (fstat(fd, &st)) {
close(fd);
return 404;
}
http_send_header(hc, 200, NULL, st.st_size, 0, NULL, 10, 0, NULL);
while (1) {
c = read(fd, buf, sizeof(buf));
if (c <= 0)
break;
if (tvh_write(hc->hc_fd, buf, c))
break;
}
close(fd);
return 0;
}
/**
*
*/
static void
webui_static_content(const char *http_path, const char *source)
{
http_path_add(http_path, (void *)source, page_static_file,
ACCESS_WEB_INTERFACE);
}
/**
*
*/
static int
favicon(http_connection_t *hc, const char *remain, void *opaque)
{
http_redirect(hc, "static/htslogo.png", NULL);
return 0;
}
int page_statedump(http_connection_t *hc, const char *remain, void *opaque);
/**
* WEB user interface
*/
void
webui_init(int xspf)
{
webui_xspf = xspf;
if (tvheadend_webui_debug)
tvhlog(LOG_INFO, "webui", "Running web interface in debug mode");
http_path_add("", NULL, page_root2, ACCESS_WEB_INTERFACE);
http_path_add("/", NULL, page_root, ACCESS_WEB_INTERFACE);
http_path_add("/login", NULL, page_login, ACCESS_WEB_INTERFACE);
http_path_add("/logout", NULL, page_logout, ACCESS_WEB_INTERFACE);
http_path_add_modify("/play", NULL, page_play, ACCESS_WEB_INTERFACE, page_play_path_modify);
http_path_add("/dvrfile", NULL, page_dvrfile, ACCESS_WEB_INTERFACE);
http_path_add("/favicon.ico", NULL, favicon, ACCESS_WEB_INTERFACE);
http_path_add("/playlist", NULL, page_http_playlist, ACCESS_WEB_INTERFACE);
http_path_add("/state", NULL, page_statedump, ACCESS_ADMIN);
http_path_add("/stream", NULL, http_stream, ACCESS_STREAMING);
http_path_add("/imagecache", NULL, page_imagecache, ACCESS_WEB_INTERFACE);
webui_static_content("/static", "src/webui/static");
webui_static_content("/docs", "docs/html");
webui_static_content("/docresources", "docs/docresources");
simpleui_start();
extjs_start();
comet_init();
webui_api_init();
}
void
webui_done(void)
{
comet_done();
}