
This allows file:// paths to be specified for channel icons even if image cache support is disabled. The image cache functionality is compile time optional (for those without curl support) and also run-time configurable for those that don't want it. All images, including EPG ones should be cached.
963 lines
23 KiB
C
963 lines
23 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 <sys/sendfile.h>
|
|
|
|
#include "tvheadend.h"
|
|
#include "access.h"
|
|
#include "http.h"
|
|
#include "webui.h"
|
|
#include "dvr/dvr.h"
|
|
#include "filebundle.h"
|
|
#include "psi.h"
|
|
#include "plumbing/tsfix.h"
|
|
#include "plumbing/globalheaders.h"
|
|
#include "epg.h"
|
|
#include "muxer.h"
|
|
#include "dvb/dvb.h"
|
|
#include "dvb/dvb_support.h"
|
|
#include "imagecache.h"
|
|
|
|
/**
|
|
*
|
|
*/
|
|
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;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 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");
|
|
} else {
|
|
http_redirect(hc, "extjs.html");
|
|
}
|
|
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);
|
|
free(tmp);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
streaming_message_t *sm;
|
|
int run = 1;
|
|
int started = 0;
|
|
muxer_t *mux = NULL;
|
|
int timeouts = 0;
|
|
struct timespec ts;
|
|
struct timeval tp;
|
|
int err = 0;
|
|
socklen_t errlen = sizeof(err);
|
|
|
|
mux = muxer_create(mc);
|
|
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) {
|
|
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 >= 20) {
|
|
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) {
|
|
muxer_write_pkt(mux, sm->sm_type, sm->sm_data);
|
|
sm->sm_data = NULL;
|
|
}
|
|
break;
|
|
|
|
case SMT_START:
|
|
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_SIGNAL_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) {
|
|
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;
|
|
|
|
hq = &hc->hc_reply;
|
|
host = http_arg_get(&hc->hc_args, "Host");
|
|
|
|
snprintf(buf, sizeof(buf), "/stream/channelid/%d", channel->ch_id);
|
|
|
|
htsbuf_qprintf(hq, "#EXTM3U\n");
|
|
htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", channel->ch_name);
|
|
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 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;
|
|
|
|
hq = &hc->hc_reply;
|
|
host = http_arg_get(&hc->hc_args, "Host");
|
|
|
|
htsbuf_qprintf(hq, "#EXTM3U\n");
|
|
LIST_FOREACH(ctm, &tag->ct_ctms, ctm_tag_link) {
|
|
snprintf(buf, sizeof(buf), "/stream/channelid/%d", ctm->ctm_channel->ch_id);
|
|
htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", ctm->ctm_channel->ch_name);
|
|
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 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;
|
|
|
|
hq = &hc->hc_reply;
|
|
host = http_arg_get(&hc->hc_args, "Host");
|
|
|
|
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", ct->ct_identifier);
|
|
htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", ct->ct_name);
|
|
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 flat playlist with all channels
|
|
*/
|
|
static int
|
|
http_channel_list_playlist(http_connection_t *hc)
|
|
{
|
|
htsbuf_queue_t *hq;
|
|
char buf[255];
|
|
channel_t *ch;
|
|
const char *host;
|
|
|
|
hq = &hc->hc_reply;
|
|
host = http_arg_get(&hc->hc_args, "Host");
|
|
|
|
htsbuf_qprintf(hq, "#EXTM3U\n");
|
|
RB_FOREACH(ch, &channel_name_tree, ch_name_link) {
|
|
snprintf(buf, sizeof(buf), "/stream/channelid/%d", ch->ch_id);
|
|
|
|
htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", ch->ch_name);
|
|
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 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;
|
|
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;
|
|
|
|
durration = de->de_stop - de->de_start;
|
|
durration += (de->de_stop_extra + de->de_start_extra)*60;
|
|
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);
|
|
htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%d,BANDWIDTH=%d\n", de->de_id, bandwidth);
|
|
htsbuf_qprintf(hq, "#EXT-X-PROGRAM-DATE-TIME:%s\n", buf);
|
|
|
|
snprintf(buf, sizeof(buf), "/dvrfile/%d", de->de_id);
|
|
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;
|
|
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 = de->de_stop - de->de_start;
|
|
durration += (de->de_stop_extra + de->de_start_extra)*60;
|
|
|
|
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);
|
|
htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%d,BANDWIDTH=%d\n", de->de_id, bandwidth);
|
|
htsbuf_qprintf(hq, "#EXT-X-PROGRAM-DATE-TIME:%s\n", buf);
|
|
|
|
snprintf(buf, sizeof(buf), "/dvrfile/%d", de->de_id);
|
|
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");
|
|
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_identifier(atoi(components[1]));
|
|
else if(nc == 2 && !strcmp(components[0], "channel"))
|
|
ch = channel_find_by_name(components[1], 0, 0);
|
|
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)
|
|
{
|
|
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;
|
|
const char *str;
|
|
size_t qsize;
|
|
const char *name;
|
|
|
|
mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux"));
|
|
if(mc == MC_UNKNOWN) {
|
|
cfg = dvr_config_find_by_name_default("");
|
|
mc = cfg->dvr_mc;
|
|
}
|
|
|
|
if ((str = http_arg_get(&hc->hc_req_args, "qsize")))
|
|
qsize = atoll(str);
|
|
else
|
|
qsize = 1500000;
|
|
|
|
if(mc == MC_PASS) {
|
|
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;
|
|
flags = 0;
|
|
}
|
|
|
|
s = subscription_create_from_service(service, "HTTP", st, flags);
|
|
if(s) {
|
|
name = strdupa(service->s_ch ?
|
|
service->s_ch->ch_name : service->s_nicename);
|
|
pthread_mutex_unlock(&global_lock);
|
|
http_stream_run(hc, &sq, name, mc);
|
|
pthread_mutex_lock(&global_lock);
|
|
subscription_unsubscribe(s);
|
|
}
|
|
|
|
if(gh)
|
|
globalheaders_destroy(gh);
|
|
|
|
if(tsfix)
|
|
tsfix_destroy(tsfix);
|
|
|
|
streaming_queue_deinit(&sq);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Subscribes to a service and starts the streaming loop
|
|
*/
|
|
#if ENABLE_LINUXDVB
|
|
static int
|
|
http_stream_tdmi(http_connection_t *hc, th_dvb_mux_instance_t *tdmi)
|
|
{
|
|
th_subscription_t *s;
|
|
streaming_queue_t sq;
|
|
const char *name;
|
|
streaming_queue_init(&sq, SMT_PACKET);
|
|
|
|
s = dvb_subscription_create_from_tdmi(tdmi, "HTTP", &sq.sq_st);
|
|
name = strdupa(tdmi->tdmi_identifier);
|
|
pthread_mutex_unlock(&global_lock);
|
|
http_stream_run(hc, &sq, name, MC_PASS);
|
|
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)
|
|
{
|
|
streaming_queue_t sq;
|
|
th_subscription_t *s;
|
|
streaming_target_t *gh;
|
|
streaming_target_t *tsfix;
|
|
streaming_target_t *st;
|
|
dvr_config_t *cfg;
|
|
int priority = 100;
|
|
int flags;
|
|
muxer_container_type_t mc;
|
|
char *str;
|
|
size_t qsize;
|
|
const char *name;
|
|
|
|
mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux"));
|
|
if(mc == MC_UNKNOWN) {
|
|
cfg = dvr_config_find_by_name_default("");
|
|
mc = cfg->dvr_mc;
|
|
}
|
|
|
|
if ((str = http_arg_get(&hc->hc_req_args, "qsize")))
|
|
qsize = atoll(str);
|
|
else
|
|
qsize = 1500000;
|
|
|
|
if(mc == MC_PASS) {
|
|
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;
|
|
flags = 0;
|
|
}
|
|
|
|
s = subscription_create_from_channel(ch, priority, "HTTP", st, flags,
|
|
inet_ntoa(hc->hc_peer->sin_addr),
|
|
hc->hc_username,
|
|
http_arg_get(&hc->hc_args, "User-Agent"));
|
|
|
|
if(s) {
|
|
name = strdupa(ch->ch_name);
|
|
pthread_mutex_unlock(&global_lock);
|
|
http_stream_run(hc, &sq, name, mc);
|
|
pthread_mutex_lock(&global_lock);
|
|
subscription_unsubscribe(s);
|
|
}
|
|
|
|
if(gh)
|
|
globalheaders_destroy(gh);
|
|
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_LINUXDVB
|
|
th_dvb_mux_instance_t *tdmi = NULL;
|
|
#endif
|
|
|
|
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]);
|
|
|
|
scopedgloballock();
|
|
|
|
if(!strcmp(components[0], "channelid")) {
|
|
ch = channel_find_by_identifier(atoi(components[1]));
|
|
} else if(!strcmp(components[0], "channel")) {
|
|
ch = channel_find_by_name(components[1], 0, 0);
|
|
} else if(!strcmp(components[0], "service")) {
|
|
service = service_find_by_identifier(components[1]);
|
|
#if ENABLE_LINUXDVB
|
|
} else if(!strcmp(components[0], "mux")) {
|
|
tdmi = dvb_mux_find_by_identifier(components[1]);
|
|
#endif
|
|
}
|
|
|
|
if(ch != NULL) {
|
|
return http_stream_channel(hc, ch);
|
|
} else if(service != NULL) {
|
|
return http_stream_service(hc, service);
|
|
#if ENABLE_LINUXDVB
|
|
} else if(tdmi != NULL) {
|
|
return http_stream_tdmi(hc, tdmi);
|
|
#endif
|
|
} else {
|
|
http_error(hc, HTTP_STATUS_BAD_REQUEST);
|
|
return HTTP_STATUS_BAD_REQUEST;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, *postfix, *range;
|
|
dvr_entry_t *de;
|
|
char *fname;
|
|
char range_buf[255];
|
|
char disposition[256];
|
|
off_t content_len, file_start, file_end, chunk;
|
|
ssize_t r;
|
|
|
|
if(remain == NULL)
|
|
return 404;
|
|
|
|
pthread_mutex_lock(&global_lock);
|
|
|
|
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);
|
|
postfix = muxer_container_suffix(de->de_mc, 1);
|
|
|
|
pthread_mutex_unlock(&global_lock);
|
|
|
|
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=%"PRId64"-%"PRId64"", &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 %"PRId64"-%"PRId64"/%"PRId64"",
|
|
file_start, file_end, st.st_size);
|
|
|
|
if(file_start > 0)
|
|
lseek(fd, file_start, SEEK_SET);
|
|
|
|
if(de->de_title != NULL) {
|
|
snprintf(disposition, sizeof(disposition),
|
|
"attachment; filename=%s.%s", lang_str_get(de->de_title, NULL), postfix);
|
|
i = 20;
|
|
while(disposition[i]) {
|
|
if(disposition[i] == ' ')
|
|
disposition[i] = '_';
|
|
i++;
|
|
}
|
|
|
|
} else {
|
|
disposition[0] = 0;
|
|
}
|
|
|
|
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);
|
|
r = sendfile(hc->hc_fd, fd, NULL, chunk);
|
|
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;
|
|
|
|
if ((fd = imagecache_open(id)) < 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, strdup(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");
|
|
return 0;
|
|
}
|
|
|
|
int page_statedump(http_connection_t *hc, const char *remain, void *opaque);
|
|
|
|
/**
|
|
* WEB user interface
|
|
*/
|
|
void
|
|
webui_init(void)
|
|
{
|
|
http_path_add("", NULL, page_root2, ACCESS_WEB_INTERFACE);
|
|
http_path_add("/", NULL, page_root, ACCESS_WEB_INTERFACE);
|
|
|
|
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_ANONYMOUS);
|
|
|
|
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();
|
|
|
|
}
|