Commit 80078cce authored by Ed "oddsock" Zaleski's avatar Ed "oddsock" Zaleski

* support for the Shoutcast DSP (yay!).

    You can now use the Shoutcast DSP as a source client.  The connection 
    protocol is a bit odd, and we had to handle it separately, and thus 
    we've added a new config option (<shoutcast-compat>) that is set at 
    the listener port level.
* support for NSV (and the nsvscsrc source client).
    After adding support for the connection protocol of the shoutcast DSP, 
    adding NSV was just a simple of a few special handling cases.
* removed all traces of the earlier attempt at the shoutcast DSP connection 
  protocol
* Due to the growing complexity of the config files, I've also created a 
  few alternate config files, namely one for a "shoutcast compat" setup
  as well as a "minimal" one for quick basic configurations.

svn path=/icecast/trunk/icecast/; revision=8191
parent 04dc8fb1
......@@ -2,11 +2,11 @@
AUTOMAKE_OPTIONS = foreign
EXTRA_DIST = icecast.xml.in
DISTCLEANFILES = icecast.xml.dist
EXTRA_DIST = icecast.xml.in icecast_minimal.xml.in icecast_shoutcast_compat.xml.in
DISTCLEANFILES = icecast.xml.dist icecast_minimal.xml.dist icecast_shoutcast_compat.xml.dist
docdir = $(datadir)/$(PACKAGE)/doc
doc_DATA = icecast.xml.dist
doc_DATA = icecast.xml.dist icecast_minimal.xml.dist icecast_shoutcast_compat.xml.dist
install-data-hook:
$(mkinstalldirs) $(DESTDIR)$(sysconfdir)
......@@ -20,6 +20,12 @@ edit = sed -e 's,@pkgdatadir\@,$(pkgdatadir),g' \
icecast.xml.dist: $(srcdir)/icecast.xml.in
$(edit) $(srcdir)/icecast.xml.in > icecast.xml.dist
icecast_minimal.xml.dist: $(srcdir)/icecast_minimal.xml.in
$(edit) $(srcdir)/icecast_minimal.xml.in > icecast_minimal.xml.dist
icecast_shoutcast_compat.xml.dist: $(srcdir)/icecast_shoutcast_compat.xml.in
$(edit) $(srcdir)/icecast_shoutcast_compat.xml.in > icecast_shoutcast_compat.xml.dist
debug:
$(MAKE) all CFLAGS="@DEBUG@"
......
......@@ -157,6 +157,11 @@ The URL which icecast2 uses to communicate with the Directory server. The value
&lt;port&gt;8000&lt;/port&gt;
&lt;bind-address&gt;127.0.0.1&lt;/bind-address&gt;
&lt;/listen-socket&gt;
&lt;listen-socket&gt;
&lt;port&gt;8001&lt;/port&gt;
&lt;bind-address&gt;127.0.0.1&lt;/bind-address&gt;
&lt;shoutcast-compat&gt;1&lt;/shoutcast-compat&gt;
&lt;/listen-socket&gt;
&lt;fileserve&gt;1&lt;/fileserve&gt;
</pre>
......@@ -170,6 +175,10 @@ The TCP port that will be used to accept client connections.
<div class="indentedbox">
And option IP address that can be used to bind to a specific network card. If not supplied, then &lt;hostname&gt; will be used.
</div>
<h4>shoutcast-compat</h4>
<div class="indentedbox">
This optional flag will indicate that this port will operate in 'shoutcast-compatibility' mode. Due to major differences in the source client connection protocol, if you wish to use any of the shoutcast DJ tools, you will need to configure at least one socket as shoutcast-compatible. Note that when in this mode, only source clients (and specifically shoutcast source clients) will be able to attach to this port. All listeners may connect to any of the ports defined without this flag. Also, for proper Shoutcast DSP compatibility, you must define a listen socket with a port one less than the one defined as 'shoutcast-compat'. This means if you define 8001 as shoutcast-compat, then you will need to define a listen port of 8000 and it must not also be defined as shoutcast-compat. See the example config file is the distribution for more info.
</div>
<h4>fileserve</h4>
<div class="indentedbox">
This flag turns on the icecast2 fileserver from which static files can be served. All files are served relative to the path specified in the &lt;paths&gt;&lt;webroot&gt; configuration setting.
......
......@@ -52,6 +52,7 @@
#define COMMAND_RAW_SHOW_LISTENERS 3
#define COMMAND_RAW_MOVE_CLIENTS 4
#define COMMAND_RAW_MANAGEAUTH 5
#define COMMAND_SHOUTCAST_METADATA_UPDATE 6
#define COMMAND_TRANSFORMED_FALLBACK 50
#define COMMAND_TRANSFORMED_SHOW_LISTENERS 53
......@@ -78,6 +79,7 @@
#define FALLBACK_RAW_REQUEST "fallbacks"
#define FALLBACK_TRANSFORMED_REQUEST "fallbacks.xsl"
#define SHOUTCAST_METADATA_REQUEST "admin.cgi"
#define METADATA_REQUEST "metadata"
#define LISTCLIENTS_RAW_REQUEST "listclients"
#define LISTCLIENTS_TRANSFORMED_REQUEST "listclients.xsl"
......@@ -112,6 +114,8 @@ int admin_get_command(char *command)
return COMMAND_TRANSFORMED_FALLBACK;
else if(!strcmp(command, METADATA_REQUEST))
return COMMAND_METADATA_UPDATE;
else if(!strcmp(command, SHOUTCAST_METADATA_REQUEST))
return COMMAND_SHOUTCAST_METADATA_UPDATE;
else if(!strcmp(command, LISTCLIENTS_RAW_REQUEST))
return COMMAND_RAW_SHOW_LISTENERS;
else if(!strcmp(command, LISTCLIENTS_TRANSFORMED_REQUEST))
......@@ -158,6 +162,7 @@ int admin_get_command(char *command)
static void command_fallback(client_t *client, source_t *source, int response);
static void command_metadata(client_t *client, source_t *source);
static void command_shoutcast_metadata(client_t *client, source_t *source);
static void command_show_listeners(client_t *client, source_t *source,
int response);
static void command_move_clients(client_t *client, source_t *source,
......@@ -269,13 +274,20 @@ void admin_handle_request(client_t *client, char *uri)
int command;
int noauth = 0;
if(strncmp("/admin/", uri, 7)) {
DEBUG1("Admin request (%s)", uri);
if (!((strcmp(uri, "/admin.cgi") == 0) ||
(strncmp("/admin/", uri, 7) == 0))) {
ERROR0("Internal error: admin request isn't");
client_send_401(client);
return;
}
command_string = uri + 7;
if (strcmp(uri, "/admin.cgi") == 0) {
command_string = uri + 1;
}
else {
command_string = uri + 7;
}
DEBUG1("Got command (%s)", command_string);
command = admin_get_command(command_string);
......@@ -289,6 +301,31 @@ void admin_handle_request(client_t *client, char *uri)
mount = httpp_get_query_param(client->parser, "mount");
if (command == COMMAND_SHOUTCAST_METADATA_UPDATE) {
source_t *source;
mount = "/";
noauth = 1;
avl_tree_rlock(global.source_tree);
source = source_find_mount_raw(mount);
if (source == NULL) {
WARN2("Admin command %s on non-existent source %s",
command_string, mount);
avl_tree_unlock(global.source_tree);
client_send_400(client, "Mount / does not exist");
return;
}
else {
if (source->shoutcast_compat == 0) {
ERROR0("Illegal call to change metadata, source not shoutcast compatible");
avl_tree_unlock (global.source_tree);
client_send_400 (client, "Illegal metadata call");
return;
}
}
avl_tree_unlock(global.source_tree);
}
if(mount != NULL) {
source_t *source;
......@@ -319,13 +356,15 @@ void admin_handle_request(client_t *client, char *uri)
}
else
{
if (source->running == 0)
{
INFO2("Received admin command %s on unavailable mount \"%s\"",
command_string, mount);
avl_tree_unlock (global.source_tree);
client_send_400 (client, "Source is not available");
return;
if (!source->shoutcast_compat) {
if (source->running == 0)
{
INFO2("Received admin command %s on unavailable mount \"%s\"",
command_string, mount);
avl_tree_unlock (global.source_tree);
client_send_400 (client, "Source is not available");
return;
}
}
INFO2("Received admin command %s on mount \"%s\"",
command_string, mount);
......@@ -403,6 +442,9 @@ static void admin_handle_mount_request(client_t *client, source_t *source,
case COMMAND_METADATA_UPDATE:
command_metadata(client, source);
break;
case COMMAND_SHOUTCAST_METADATA_UPDATE:
command_shoutcast_metadata(client, source);
break;
case COMMAND_RAW_SHOW_LISTENERS:
command_show_listeners(client, source, RAW);
break;
......@@ -784,7 +826,8 @@ static void command_metadata(client_t *client, source_t *source)
COMMAND_REQUIRE(client, "mode", action);
COMMAND_REQUIRE(client, "song", value);
if (source->format->type != FORMAT_TYPE_MP3)
if ((source->format->type != FORMAT_TYPE_MP3) &&
(source->format->type != FORMAT_TYPE_NSV))
{
client_send_400 (client, "Not mp3, cannot update metadata");
return;
......@@ -811,6 +854,64 @@ static void command_metadata(client_t *client, source_t *source)
html_success(client, "Metadata update successful");
}
static void command_shoutcast_metadata(client_t *client, source_t *source)
{
char *action;
char *value;
char *source_pass;
char *config_source_pass;
ice_config_t *config;
mp3_state *state;
DEBUG0("Got shoutcast metadata update request");
COMMAND_REQUIRE(client, "mode", action);
COMMAND_REQUIRE(client, "song", value);
COMMAND_REQUIRE(client, "pass", source_pass);
config = config_get_config();
config_source_pass = strdup(config->source_password);
config_release_config();
if ((source->format->type != FORMAT_TYPE_MP3) &&
(source->format->type != FORMAT_TYPE_NSV))
{
client_send_400 (client, "Not mp3 or NSV, cannot update metadata");
return;
}
if (strcmp (action, "updinfo") != 0)
{
client_send_400 (client, "No such action");
return;
}
if (strcmp(source_pass, config_source_pass) != 0)
{
ERROR0("Invalid source password specified, metadata not updated");
client_send_400 (client, "Invalid source password");
return;
}
if (config_source_pass) {
free(config_source_pass);
}
state = source->format->_state;
mp3_set_tag (source->format, "title", value);
DEBUG2("Metadata on mountpoint %s changed to \"%s\"",
source->mount, value);
stats_event(source->mount, "title", value);
/* If we get an update on the mountpoint, force a
yp touch */
yp_touch (source->mount);
html_success(client, "Metadata update successful");
}
static void command_stats(client_t *client, int response) {
xmlDocPtr doc;
......
......@@ -321,6 +321,7 @@ static void _set_defaults(ice_config_t *configuration)
configuration->port = 0;
configuration->listeners[0].port = 0;
configuration->listeners[0].bind_address = NULL;
configuration->listeners[0].shoutcast_compat = 0;
configuration->master_server = NULL;
configuration->master_server_port = 0;
configuration->master_update_interval = CONFIG_MASTER_UPDATE_INTERVAL;
......@@ -667,6 +668,11 @@ static void _parse_listen_socket(xmlDocPtr doc, xmlNodePtr node,
listener->port = atoi(tmp);
if(tmp) xmlFree(tmp);
}
else if (strcmp(node->name, "shoutcast-compat") == 0) {
tmp = (char *)xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
listener->shoutcast_compat = atoi(tmp);
if(tmp) xmlFree(tmp);
}
else if (strcmp(node->name, "bind-address") == 0) {
listener->bind_address = (char *)xmlNodeListGetString(doc,
node->xmlChildrenNode, 1);
......
......@@ -75,6 +75,7 @@ typedef struct _aliases {
typedef struct {
int port;
char *bind_address;
int shoutcast_compat;
} listener_t;
typedef struct ice_config_tag
......
......@@ -65,6 +65,23 @@
#define CATMODULE "connection"
/* Two different major types of source authentication.
Shoutcast style is used only by the Shoutcast DSP
and is a crazy version of HTTP. It looks like :
Source Client -> Connects to port + 1
Source Client -> sends encoder password (plaintext)\r\n
Icecast -> reads encoder password, if ok, sends OK2\r\n, else disconnects
Source Client -> reads OK2\r\n, then sends http-type request headers
that contain the stream details (icy-name, etc..)
Icecast -> reads headers, stores them
Source Client -> starts sending MP3 data
Source Client -> periodically updates metadata via admin.cgi call
Icecast auth style uses HTTP and Basic Authorization.
*/
#define SHOUTCAST_SOURCE_AUTH 1
#define ICECAST_SOURCE_AUTH 0
typedef struct con_queue_tag {
connection_t *con;
struct con_queue_tag *next;
......@@ -664,7 +681,7 @@ int connection_check_source_pass(http_parser_t *parser, char *mount)
static void _handle_source_request(connection_t *con,
http_parser_t *parser, char *uri)
http_parser_t *parser, char *uri, int auth_style)
{
client_t *client;
source_t *source;
......@@ -680,18 +697,23 @@ static void _handle_source_request(connection_t *con,
return;
}
if (!connection_check_source_pass(parser, uri)) {
/* We commonly get this if the source client is using the wrong
* protocol: attempt to diagnose this and return an error
*/
/* TODO: Do what the above comment says */
INFO1("Source (%s) attempted to login with invalid or missing password", uri);
client_send_401(client);
return;
if (auth_style == ICECAST_SOURCE_AUTH) {
if (!connection_check_source_pass(parser, uri)) {
/* We commonly get this if the source client is using the wrong
* protocol: attempt to diagnose this and return an error
*/
/* TODO: Do what the above comment says */
INFO1("Source (%s) attempted to login with invalid or missing password", uri);
client_send_401(client);
return;
}
}
source = source_reserve (uri);
if (source)
{
if (auth_style == SHOUTCAST_SOURCE_AUTH) {
source->shoutcast_compat = 1;
}
source->client = client;
source->parser = parser;
source->con = con;
......@@ -796,7 +818,8 @@ static void _handle_get_request(connection_t *con,
stats_event_inc(NULL, "client_connections");
/* Dispatch all admin requests */
if (strncmp(uri, "/admin/", 7) == 0) {
if ((strcmp(uri, "/admin.cgi") == 0) ||
(strncmp(uri, "/admin/", 7) == 0)) {
admin_handle_request(client, uri);
if (uri != passed_uri) free (uri);
return;
......@@ -965,6 +988,74 @@ static void _handle_get_request(connection_t *con,
if (uri != passed_uri) free (uri);
}
void _handle_shoutcast_compatible(connection_t *con, char *source_password) {
char shoutcast_password[256];
char shoutcast_source[256];
char *http_compliant;
int http_compliant_len = 0;
char header[4096];
http_parser_t *parser;
memset(shoutcast_password, 0, sizeof (shoutcast_password));
/* Step one of shoutcast auth protocol, read encoder password (1 line) */
if (util_read_header(con->sock, shoutcast_password,
sizeof (shoutcast_password),
READ_LINE) == 0) {
/* either we didn't get a complete line, or we timed out */
connection_close(con);
return;
}
/* Get rid of trailing \n */
shoutcast_password[strlen(shoutcast_password)-1] = '\000';
if (strcmp(shoutcast_password, source_password)) {
ERROR0("Invalid source password");
connection_close(con);
return;
}
/* Step two of shoutcast auth protocol, send OK2. For those
interested, OK2 means it supports metadata updates via admin.cgi,
and the string "OK" can also be sent, but will indicate to the
shoutcast source client to not send metadata updates.
I believe icecast 1.x used to send OK. */
sock_write(con->sock, "%s\r\n", "OK2");
memset(header, 0, sizeof (header));
/* Step three of shoutcast auth protocol, read HTTP-style
request headers and process them.*/
if (util_read_header(con->sock, header, sizeof (header),
READ_ENTIRE_HEADER) == 0) {
/* either we didn't get a complete header, or we timed out */
connection_close(con);
return;
}
/* Here we create a valid HTTP request based of the information
that was passed in via the non-HTTP style protocol above. This
means we can use some of our existing code to handle this case */
memset(shoutcast_source, 0, sizeof (shoutcast_source));
strcpy(shoutcast_source, "SOURCE / HTTP/1.0\r\n");
http_compliant_len = strlen(shoutcast_source) +
strlen(header) + 1;
http_compliant = (char *)calloc(1, http_compliant_len);
sprintf(http_compliant, "%s%s", shoutcast_source,
header);
parser = httpp_create_parser();
httpp_initialize(parser, NULL);
if (httpp_parse(parser, http_compliant,
strlen(http_compliant))) {
_handle_source_request(con, parser, "/", SHOUTCAST_SOURCE_AUTH);
free(http_compliant);
return;
}
else {
ERROR0("Invalid source request");
connection_close(con);
free(http_compliant);
httpp_destroy(parser);
return;
}
return;
}
static void *_handle_connection(void *arg)
{
char header[4096];
......@@ -972,6 +1063,10 @@ static void *_handle_connection(void *arg)
http_parser_t *parser;
char *rawuri, *uri;
client_t *client;
int i = 0;
int continue_flag = 0;
ice_config_t *config;
char *source_password;
while (global.running == ICE_RUNNING) {
......@@ -996,9 +1091,30 @@ static void *_handle_connection(void *arg)
sock_set_blocking(con->sock, SOCK_BLOCK);
continue_flag = 0;
/* Check for special shoutcast compatability processing */
for(i = 0; i < MAX_LISTEN_SOCKETS; i++) {
if(global.serversock[i] == con->serversock) {
config = config_get_config();
if (config->listeners[i].shoutcast_compat) {
source_password = strdup(config->source_password);
config_release_config();
_handle_shoutcast_compatible(con, source_password);
free(source_password);
continue_flag = 1;
break;
}
config_release_config();
}
}
if(continue_flag) {
continue;
}
/* fill header with the http header */
memset(header, 0, sizeof (header));
if (util_read_header(con->sock, header, sizeof (header)) == 0) {
if (util_read_header(con->sock, header, sizeof (header),
READ_ENTIRE_HEADER) == 0) {
/* either we didn't get a complete header, or we timed out */
connection_close(con);
continue;
......@@ -1027,7 +1143,7 @@ static void *_handle_connection(void *arg)
}
if (parser->req_type == httpp_req_source) {
_handle_source_request(con, parser, uri);
_handle_source_request(con, parser, uri, ICECAST_SOURCE_AUTH);
}
else if (parser->req_type == httpp_req_stats) {
_handle_stats_request(con, parser, uri);
......@@ -1044,22 +1160,6 @@ static void *_handle_connection(void *arg)
free(uri);
continue;
}
else if(httpp_parse_icy(parser, header, strlen(header))) {
/* TODO: Map incoming icy connections to /icy_0, etc. */
char mount[20];
unsigned i = 0;
strcpy(mount, "/");
avl_tree_rlock(global.source_tree);
while (source_find_mount (mount) != NULL) {
snprintf (mount, sizeof (mount), "/icy_%u", i++);
}
avl_tree_unlock(global.source_tree);
_handle_source_request(con, parser, mount);
continue;
}
else {
ERROR0("HTTP request parsing failed");
connection_close(con);
......
......@@ -40,6 +40,7 @@
#include "format_mp3.h"
#include "logging.h"
#include "stats.h"
#define CATMODULE "format"
#ifdef WIN32
......@@ -57,6 +58,8 @@ format_type_t format_get_type(char *contenttype)
return FORMAT_TYPE_MP3;
else if(strcmp(contenttype, "audio/x-mpeg") == 0)
return FORMAT_TYPE_MP3; /* Relay-compatibility for some servers */
else if(strcmp(contenttype, "video/nsv") == 0)
return FORMAT_TYPE_NSV;
else
return FORMAT_ERROR;
}
......@@ -70,6 +73,9 @@ char *format_get_mimetype(format_type_t type)
case FORMAT_TYPE_MP3:
return "audio/mpeg";
break;
case FORMAT_TYPE_NSV:
return "video/nsv";
break;
default:
return NULL;
}
......@@ -86,9 +92,16 @@ int format_get_plugin(format_type_t type, source_t *source)
case FORMAT_TYPE_MP3:
ret = format_mp3_get_plugin (source);
break;
case FORMAT_TYPE_NSV:
ret = format_mp3_get_plugin (source);
source->format->format_description = "NSV Video";
source->format->type = FORMAT_TYPE_NSV;
break;
default:
break;
}
stats_event (source->mount, "content-type",
format_get_mimetype(source->format->type));
return ret;
}
......
......@@ -28,6 +28,7 @@ typedef enum _format_type_tag
{
FORMAT_TYPE_VORBIS,
FORMAT_TYPE_MP3,
FORMAT_TYPE_NSV,
FORMAT_ERROR /* No format, source not processable */
} format_type_t;
......
......@@ -169,7 +169,7 @@ static void *start_relay_stream (void *arg)
"\r\n",
relay->mount, relay->mp3metadata?"Icy-MetaData: 1\r\n":"");
memset (header, 0, sizeof(header));
if (util_read_header (con->sock, header, 4096) == 0)
if (util_read_header (con->sock, header, 4096, READ_ENTIRE_HEADER) == 0)
{
WARN0("Header read failed");
break;
......
......@@ -239,6 +239,7 @@ void source_clear_source (source_t *source)
source->queue_size_limit = 0;
source->listeners = 0;
source->no_mount = 0;
source->shoutcast_compat = 0;
source->max_listeners = -1;
source->yp_public = 0;
util_dict_free (source->audio_info);
......@@ -484,15 +485,20 @@ static void source_init (source_t *source)
char *listenurl, *str;
int listen_url_size;
char *s;
char *extra = "";
if (source->format->type == FORMAT_TYPE_NSV) {
extra = "?file=stream.nsv";
}
/* 6 for max size of port */
listen_url_size = strlen("http://") + strlen(config->hostname) +
strlen(":") + 6 + strlen(source->mount) + 1;
strlen(":") + 6 + strlen(source->mount) + strlen(extra) + 1;
listenurl = malloc (listen_url_size);
memset (listenurl, '\000', listen_url_size);
snprintf (listenurl, listen_url_size, "http://%s:%d%s",
config->hostname, config->port, source->mount);
snprintf (listenurl, listen_url_size, "http://%s:%d%s%s",
config->hostname, config->port, source->mount, extra);
config_release_config();
do
......
......@@ -54,6 +54,7 @@ typedef struct source_tag
struct auth_tag *authenticator;
int fallback_override;
int no_mount;
int shoutcast_compat;
/* per source burst handling for connecting clients */
unsigned int burst_size; /* trigger level for burst on connect */
......
......@@ -84,7 +84,7 @@ int util_timed_wait_for_fd(int fd, int timeout)
#endif
}
int util_read_header(int sock, char *buff, unsigned long len)
int util_read_header(int sock, char *buff, unsigned long len, int entire)
{
int read_bytes, ret;
unsigned long pos;
......@@ -107,9 +107,18 @@ int util_read_header(int sock, char *buff, unsigned long len)
if ((read_bytes = recv(sock, &c, 1, 0))) {
if (c != '\r') buff[pos++] = c;
if ((pos > 1) && (buff[pos - 1] == '\n' && buff[pos - 2] == '\n')) {
ret = 1;
break;
if (entire) {
if ((pos > 1) && (buff[pos - 1] == '\n' &&
buff[pos - 2] == '\n')) {
ret = 1;
break;
}
}
else {
if ((pos > 1) && (buff[pos - 1] == '\n')) {
ret = 1;
break;
}
}
}
} else {
......
......@@ -16,8 +16,11 @@
#define XSLT_CONTENT 1
#define HTML_CONTENT 2
#define READ_ENTIRE_HEADER 1
#define READ_LINE 0
int util_timed_wait_for_fd(int fd, int timeout);
int util_read_header(int sock, char *buff, unsigned long len);
int util_read_header(int sock, char *buff, unsigned long len, int entire);
int util_check_valid_extension(char *uri);
char *util_get_extension(char *path);
char *util_get_path_from_uri(char *uri);
......
......@@ -479,12 +479,22 @@ static ypdata_t *create_yp_entry (source_t *source)
if (url == NULL)
break;
config = config_get_config();
ret = snprintf (url, len, "http://%s:%d%s", config->hostname, config->port, source->mount);
if (source->format->type == FORMAT_TYPE_NSV) {
ret = snprintf (url, len, "http://%s:%d%s?stream.nsv", config->hostname, config->port, source->mount);
}
else {
ret = snprintf (url, len, "http://%s:%d%s", config->hostname, config->port, source->mount);
}
if (ret >= (signed)len)
{
s = realloc (url, ++ret);
if (s) url = s;
snprintf (url, ret, "http://%s:%d%s", config->hostname, config->port, source->mount);
if (source->format->type == FORMAT_TYPE_NSV) {
snprintf (url, ret, "http://%s:%d%s?file=stream.nsv", config->hostname, config->port, source->mount);
}
else {
snprintf (url, ret, "http://%s:%d%s", config->hostname, config->port, source->mount);
}
}
config_release_config();
yp->listen_url = util_url_escape (url);
......
......@@ -54,7 +54,14 @@
<a href="auth.xsl">Click to Listen</a>
</xsl:when>
<xsl:otherwise>
<a href="{@mount}.m3u">Click to Listen</a>
<xsl:choose>
<xsl:when test="content-type='video/nsv'">
<a href="{@mount}%3Ffile%3Dstream.nsv.m3u">Click to Listen</a>
</xsl:when>
<xsl:otherwise>
<a href="{@mount}.m3u">Click to Listen</a>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</td></tr>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment