302 lines
9.1 KiB
C
302 lines
9.1 KiB
C
/*
|
|
* HTTP Smooth Streaming based on DASH demuxer
|
|
* Copyright (c) 2017 samsamsam@o2.pl based on HLS demux
|
|
* Copyright (c) 2017 Steven Liu
|
|
*
|
|
* This file is part of FFmpeg.
|
|
*
|
|
* FFmpeg is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* FFmpeg 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with FFmpeg; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
#include <libxml/parser.h>
|
|
#include "libavutil/intreadwrite.h"
|
|
#include "libavutil/opt.h"
|
|
#include "libavutil/time.h"
|
|
#include "libavutil/parseutils.h"
|
|
#include "internal.h"
|
|
#include "avio_internal.h"
|
|
#include "dash.h"
|
|
|
|
#define INITIAL_BUFFER_SIZE 32768
|
|
#define MAX_MANIFEST_SIZE 50 * 1024
|
|
#define DEFAULT_MANIFEST_SIZE 8 * 1024
|
|
|
|
typedef struct SmoothStreamingContext {
|
|
const AVClass *class;
|
|
char *base_url;
|
|
int is_live;
|
|
AVIOInterruptCB *interrupt_callback;
|
|
char *allowed_extensions;
|
|
AVDictionary *avio_opts;
|
|
int max_url_size;
|
|
char *cenc_decryption_key;
|
|
|
|
/* Flags for init section*/
|
|
int is_init_section_common_video;
|
|
int is_init_section_common_audio;
|
|
|
|
} SmoothStreamingContext;
|
|
|
|
static int ishttp(char *url)
|
|
{
|
|
const char *proto_name = avio_find_protocol_name(url);
|
|
return av_strstart(proto_name, "http", NULL);
|
|
}
|
|
|
|
static int aligned(int val)
|
|
{
|
|
return ((val + 0x3F) >> 6) << 6;
|
|
}
|
|
|
|
static int open_url(AVFormatContext *s, AVIOContext **pb, const char *url,
|
|
AVDictionary **opts, AVDictionary *opts2, int *is_http)
|
|
{
|
|
SmoothStreamingContext *c = s->priv_data;
|
|
AVDictionary *tmp = NULL;
|
|
const char *proto_name = NULL;
|
|
int ret;
|
|
|
|
if (av_strstart(url, "crypto", NULL)) {
|
|
if (url[6] == '+' || url[6] == ':')
|
|
proto_name = avio_find_protocol_name(url + 7);
|
|
}
|
|
|
|
if (!proto_name)
|
|
proto_name = avio_find_protocol_name(url);
|
|
|
|
if (!proto_name)
|
|
return AVERROR_INVALIDDATA;
|
|
|
|
// only http(s) & file are allowed
|
|
if (av_strstart(proto_name, "file", NULL)) {
|
|
if (strcmp(c->allowed_extensions, "ALL") && !av_match_ext(url, c->allowed_extensions)) {
|
|
av_log(s, AV_LOG_ERROR,
|
|
"Filename extension of \'%s\' is not a common multimedia extension, blocked for security reasons.\n"
|
|
"If you wish to override this adjust allowed_extensions, you can set it to \'ALL\' to allow all\n",
|
|
url);
|
|
return AVERROR_INVALIDDATA;
|
|
}
|
|
} else if (av_strstart(proto_name, "http", NULL)) {
|
|
;
|
|
} else
|
|
return AVERROR_INVALIDDATA;
|
|
|
|
if (!strncmp(proto_name, url, strlen(proto_name)) && url[strlen(proto_name)] == ':')
|
|
;
|
|
else if (av_strstart(url, "crypto", NULL) && !strncmp(proto_name, url + 7, strlen(proto_name)) && url[7 + strlen(proto_name)] == ':')
|
|
;
|
|
else if (strcmp(proto_name, "file") || !strncmp(url, "file,", 5))
|
|
return AVERROR_INVALIDDATA;
|
|
|
|
av_freep(pb);
|
|
av_dict_copy(&tmp, *opts, 0);
|
|
av_dict_copy(&tmp, opts2, 0);
|
|
ret = avio_open2(pb, url, AVIO_FLAG_READ, c->interrupt_callback, &tmp);
|
|
if (ret >= 0) {
|
|
// update cookies on http response with setcookies.
|
|
char *new_cookies = NULL;
|
|
|
|
if (!(s->flags & AVFMT_FLAG_CUSTOM_IO))
|
|
av_opt_get(*pb, "cookies", AV_OPT_SEARCH_CHILDREN, (uint8_t**)&new_cookies);
|
|
|
|
if (new_cookies) {
|
|
av_dict_set(opts, "cookies", new_cookies, AV_DICT_DONT_STRDUP_VAL);
|
|
}
|
|
|
|
}
|
|
|
|
av_dict_free(&tmp);
|
|
|
|
if (is_http)
|
|
*is_http = av_strstart(proto_name, "http", NULL);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static char *get_content_url(xmlNodePtr *baseurl_nodes,
|
|
int n_baseurl_nodes,
|
|
int max_url_size,
|
|
char *rep_id_val,
|
|
char *rep_bandwidth_val,
|
|
char *val)
|
|
{
|
|
int i;
|
|
char *text;
|
|
char *url = NULL;
|
|
char *tmp_str = av_mallocz(max_url_size);
|
|
|
|
if (!tmp_str)
|
|
return NULL;
|
|
|
|
for (i = 0; i < n_baseurl_nodes; ++i) {
|
|
if (baseurl_nodes[i] &&
|
|
baseurl_nodes[i]->children &&
|
|
baseurl_nodes[i]->children->type == XML_TEXT_NODE) {
|
|
text = xmlNodeGetContent(baseurl_nodes[i]->children);
|
|
if (text) {
|
|
memset(tmp_str, 0, max_url_size);
|
|
ff_make_absolute_url(tmp_str, max_url_size, "", text);
|
|
xmlFree(text);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (val)
|
|
ff_make_absolute_url(tmp_str, max_url_size, tmp_str, val);
|
|
|
|
if (rep_id_val) {
|
|
url = av_strireplace(tmp_str, "$RepresentationID$", (const char*)rep_id_val);
|
|
if (!url) {
|
|
goto end;
|
|
}
|
|
av_strlcpy(tmp_str, url, max_url_size);
|
|
}
|
|
if (rep_bandwidth_val && tmp_str[0] != '\0') {
|
|
// free any previously assigned url before reassigning
|
|
av_free(url);
|
|
url = av_strireplace(tmp_str, "$Bandwidth$", (const char*)rep_bandwidth_val);
|
|
if (!url) {
|
|
goto end;
|
|
}
|
|
}
|
|
end:
|
|
av_free(tmp_str);
|
|
return url;
|
|
}
|
|
|
|
static char *get_val_from_nodes_tab(xmlNodePtr *nodes, const int n_nodes, const char *attrname)
|
|
{
|
|
int i;
|
|
char *val;
|
|
|
|
for (i = 0; i < n_nodes; ++i) {
|
|
if (nodes[i]) {
|
|
val = xmlGetProp(nodes[i], attrname);
|
|
if (val)
|
|
return val;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static xmlNodePtr find_child_node_by_name(xmlNodePtr rootnode, const char *nodename)
|
|
{
|
|
xmlNodePtr node = rootnode;
|
|
if (!node) {
|
|
return NULL;
|
|
}
|
|
|
|
node = xmlFirstElementChild(node);
|
|
while (node) {
|
|
if (!av_strcasecmp(node->name, nodename)) {
|
|
return node;
|
|
}
|
|
node = xmlNextElementSibling(node);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static enum AVMediaType get_content_type(xmlNodePtr node)
|
|
{
|
|
enum AVMediaType type = AVMEDIA_TYPE_UNKNOWN;
|
|
int i = 0;
|
|
const char *attr;
|
|
char *val = NULL;
|
|
|
|
if (node) {
|
|
for (i = 0; i < 2; i++) {
|
|
attr = i ? "mimeType" : "contentType";
|
|
val = xmlGetProp(node, attr);
|
|
if (val) {
|
|
if (av_stristr((const char *)val, "video")) {
|
|
type = AVMEDIA_TYPE_VIDEO;
|
|
} else if (av_stristr((const char *)val, "audio")) {
|
|
type = AVMEDIA_TYPE_AUDIO;
|
|
} else if (av_stristr((const char *)val, "text")) {
|
|
type = AVMEDIA_TYPE_SUBTITLE;
|
|
}
|
|
xmlFree(val);
|
|
}
|
|
}
|
|
}
|
|
return type;
|
|
}
|
|
|
|
static int save_avio_options(AVFormatContext *s)
|
|
{
|
|
SmoothStreamingContext *c = s->priv_data;
|
|
const char *opts[] = {
|
|
"headers", "user_agent", "cookies", "http_proxy", "referer", "rw_timeout", "icy", NULL };
|
|
const char **opt = opts;
|
|
uint8_t *buf = NULL;
|
|
int ret = 0;
|
|
|
|
while (*opt) {
|
|
if (av_opt_get(s->pb, *opt, AV_OPT_SEARCH_CHILDREN, &buf) >= 0) {
|
|
if (buf[0] != '\0') {
|
|
ret = av_dict_set(&c->avio_opts, *opt, buf, AV_DICT_DONT_STRDUP_VAL);
|
|
if (ret < 0)
|
|
return ret;
|
|
} else {
|
|
av_freep(&buf);
|
|
}
|
|
}
|
|
opt++;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int nested_io_open(AVFormatContext *s, AVIOContext **pb, const char *url,
|
|
int flags, AVDictionary **opts)
|
|
{
|
|
av_log(s, AV_LOG_ERROR,
|
|
"A SmoothStreaming playlist item '%s' referred to an external file '%s'. "
|
|
"Opening this file was forbidden for security reasons\n",
|
|
s->url, url);
|
|
return AVERROR(EPERM);
|
|
}
|
|
|
|
#define OFFSET(x) offsetof(DASHContext, x)
|
|
#define FLAGS AV_OPT_FLAG_DECODING_PARAM
|
|
static const AVOption dash_options[] = {
|
|
{"allowed_extensions", "List of file extensions that dash is allowed to access",
|
|
OFFSET(allowed_extensions), AV_OPT_TYPE_STRING,
|
|
{.str = "aac,m4a,m4s,m4v,mov,mp4,webm,ts"},
|
|
INT_MIN, INT_MAX, FLAGS},
|
|
{ "cenc_decryption_key", "Media decryption key (hex)", OFFSET(cenc_decryption_key), AV_OPT_TYPE_STRING, {.str = NULL}, INT_MIN, INT_MAX, .flags = FLAGS },
|
|
{NULL}
|
|
};
|
|
|
|
static const AVClass dash_class = {
|
|
.class_name = "dash",
|
|
.item_name = av_default_item_name,
|
|
.option = dash_options,
|
|
.version = LIBAVUTIL_VERSION_INT,
|
|
};
|
|
|
|
AVInputFormat ff_dash_demuxer = {
|
|
.name = "dash",
|
|
.long_name = NULL_IF_CONFIG_SMALL("Dynamic Adaptive Streaming over HTTP"),
|
|
.priv_class = &dash_class,
|
|
.priv_data_size = sizeof(DASHContext),
|
|
.read_probe = dash_probe,
|
|
.read_header = dash_read_header,
|
|
.read_packet = dash_read_packet,
|
|
.read_close = dash_close,
|
|
.read_seek = dash_read_seek,
|
|
.flags = AVFMT_NO_BYTE_SEEK,
|
|
};
|