Commit f99d052a authored by Jonathan Thambidurai's avatar Jonathan Thambidurai Committed by Jean-Baptiste Kempf

Support for Http Dynamic Streaming

Signed-off-by: Jean-Baptiste Kempf's avatarJean-Baptiste Kempf <jb@videolan.org>
parent 29e0d176
Changes between 2.2.x and 3.0.0-git:
--------------------------------
Access:
* Support HDS (Http Dynamic Streaming) from Adobe (f4m, f4v, etc.)
Changes between 2.1.x and 2.2.0:
--------------------------------
......
......@@ -153,6 +153,7 @@ $Id$
* growl: announce currently playing stream to growl
* gstdecode: GStreamer based decoder module.
* h264: H264 decoder
* hds: HTTP Dynamic Streaming, per Adobe's specs
* headphone_channel_mixer: headphone channel mixer with virtual spatialization effect
* hevc: HEVC demuxer
* hotkeys: hotkeys control module
......
......@@ -104,6 +104,13 @@ libsmooth_plugin_la_SOURCES = \
libsmooth_plugin_la_CFLAGS = $(AM_CFLAGS)
stream_filter_LTLIBRARIES += libsmooth_plugin.la
libhds_plugin_la_SOURCES = \
stream_filter/hds/hds.c
libhds_plugin_la_CFLAGS = $(AM_CFLAGS)
stream_filter_LTLIBRARIES += libhds_plugin.la
libhttplive_plugin_la_SOURCES = stream_filter/httplive.c
libhttplive_plugin_la_CFLAGS = $(AM_CFLAGS) $(GCRYPT_CFLAGS)
libhttplive_plugin_la_LIBADD = $(GCRYPT_LIBS) -lgpg-error
......
/*****************************************************************************
* hds.c: Http Dynamic Streaming (HDS) stream filter
*****************************************************************************
*
* Author: Jonathan Thambidurai <jonathan _AT_ fastly _DOT_ com>
* Heavily inspired by SMooth module of Frédéric Yhuel <fyhuel _AT_ viotech _DOT_ net>
*
* This library 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.
*
* This library 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 this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <vlc_common.h>
#include <vlc_plugin.h>
#include <vlc_stream.h>
#include <vlc_strings.h> /* b64_decode */
#include <vlc_xml.h>
#include <vlc_charset.h> /* FromCharset */
#include <vlc_es.h> /* UNKNOWN_ES */
typedef struct chunk_s
{
int64_t duration; /* chunk duration in afrt timescale units */
uint64_t timestamp;
uint32_t frag_num;
uint32_t seg_num;
uint32_t frun_entry; /* Used to speed things up in vod situations */
uint32_t data_len;
uint32_t mdat_pos; /* position in the mdat */
uint32_t mdat_len;
void *next;
uint8_t *mdat_data;
uint8_t *data;
bool failed;
bool eof;
} chunk_t;
typedef struct segment_run_s
{
uint32_t first_segment;
uint32_t fragments_per_segment;
} segment_run_t;
typedef struct fragment_run_s
{
uint32_t fragment_number_start;
uint32_t fragment_duration;
uint64_t fragment_timestamp;
uint8_t discont;
} fragment_run_t;
typedef struct hds_stream_s
{
/* linked-list of chunks */
chunk_t *chunks_head;
chunk_t *chunks_livereadpos;
chunk_t *chunks_downloadpos;
char* quality_segment_modifier;
/* we can make this configurable */
uint64_t download_leadtime;
/* in timescale units */
uint32_t total_duration;
uint32_t afrt_timescale;
/* these two values come from the abst */
uint32_t timescale;
uint64_t live_current_time;
vlc_mutex_t abst_lock;
vlc_mutex_t dl_lock;
vlc_cond_t dl_cond;
/* can be left as null */
char* abst_url;
/* this comes from the manifest media section */
char* url;
/* this comes from the bootstrap info */
char* movie_id;
#define MAX_HDS_SERVERS 10
char* server_entries[MAX_HDS_SERVERS];
uint8_t server_entry_count;
#define MAX_HDS_SEGMENT_RUNS 256
segment_run_t segment_runs[MAX_HDS_SEGMENT_RUNS];
uint8_t segment_run_count;
#define MAX_HDS_FRAGMENT_RUNS 10000
fragment_run_t fragment_runs[MAX_HDS_FRAGMENT_RUNS];
uint32_t fragment_run_count;
} hds_stream_t;
/* this is effectively just a sanity check mechanism */
#define MAX_REQUEST_SIZE (50*1024*1024)
struct stream_sys_t
{
char *base_url; /* URL common part for chunks */
vlc_thread_t live_thread;
vlc_thread_t dl_thread;
/* we pend on peek until some number of segments arrives; otherwise
* the downstream system dies in case of playback */
uint64_t chunk_count;
vlc_array_t *hds_streams; /* available streams */
uint32_t flv_header_bytes_sent;
uint64_t duration_seconds;
bool live;
bool closed;
};
typedef struct _bootstrap_info {
uint8_t* data;
char* id;
char* url;
char* profile;
int data_len;
} bootstrap_info;
typedef struct _media_info {
char* stream_id;
char* media_url;
char* bootstrap_id;
} media_info;
/*****************************************************************************
* Module descriptor
*****************************************************************************/
static int Open( vlc_object_t * );
static void Close( vlc_object_t * );
vlc_module_begin()
set_category( CAT_INPUT )
set_subcategory( SUBCAT_INPUT_STREAM_FILTER )
set_description( N_("HTTP Dynamic Streaming") )
set_shortname( "Dynamic Streaming")
add_shortcut( "hds" )
set_capability( "stream_filter", 30 )
set_callbacks( Open, Close )
vlc_module_end()
static int Read( stream_t *, void *, unsigned );
static int Peek( stream_t *, const uint8_t **, unsigned );
static int Control( stream_t *, int , va_list );
static inline bool isFQUrl( char* url )
{
return ( NULL != vlc_strcasestr( url, "https://") ||
NULL != vlc_strcasestr( url, "http://" ) );
}
static bool isHDS( stream_t *s )
{
const char *peek;
int i_size = stream_Peek( s->p_source, (const uint8_t**) &peek, 200 );
if( i_size < 200 )
return false;
const char *str;
if( !memcmp( peek, "\xFF\xFE", 2 ) )
{
str = FromCharset( "UTF-16LE", peek, 512 );
}
else if( !memcmp( peek, "\xFE\xFF", 2 ) )
{
str = FromCharset( "UTF-16BE", peek, 512 );
}
else
str = peek;
if( str == NULL )
return false;
bool ret = strstr( str, "<manifest" ) != NULL;
return ret;
}
static uint8_t* parse_asrt( vlc_object_t* p_this,
hds_stream_t* s,
uint8_t* data,
uint8_t* data_end )
{
uint8_t* data_p = data;
uint32_t asrt_len = 0;
asrt_len = U32_AT( data_p );
if( asrt_len > data_end - data ||
data_end - data < 14 )
{
msg_Err( p_this, "Not enough asrt data (%"PRIu32", %lu)", asrt_len, data_end - data );
return NULL;
}
data_p += sizeof(asrt_len);
if( 0 != memcmp( "asrt", data_p, 4 ) )
{
msg_Err( p_this, "Cant find asrt in bootstrap" );
return NULL;
}
data_p += 4;
/* ignore flags and versions (we don't handle multiple updates) */
data_p += 4;
uint8_t quality_entry_count = *data_p;
bool quality_found = false;
data_p++;
if( ! s->quality_segment_modifier )
{
quality_found = true;
}
while( quality_entry_count-- > 0 )
{
char* str_start = (char*) data_p;
data_p = memchr( data_p, '\0', data_end - data_p );
if( ! data_p )
{
msg_Err( p_this, "Couldn't find quality entry string in asrt" );
return NULL;
}
data_p++;
if( ! quality_found )
{
if( ! strncmp( str_start, s->quality_segment_modifier,
strlen(s->quality_segment_modifier) ) )
{
quality_found = true;
}
}
if( data_p >= data_end )
{
msg_Err( p_this, "Premature end of asrt in quality entries" );
return NULL;
}
}
if( data_end - data_p < 4 )
{
msg_Err( p_this, "Premature end of asrt after quality entries" );
return NULL;
}
uint32_t segment_run_entry_count = U32_AT( data_p );
data_p += sizeof(segment_run_entry_count);
if( data_end - data_p < 8 * segment_run_entry_count )
{
msg_Err( p_this, "Not enough data in asrt for segment run entries" );
return NULL;
}
if( segment_run_entry_count >= MAX_HDS_SEGMENT_RUNS )
{
msg_Err( p_this, "Too many segment runs" );
return NULL;
}
while( segment_run_entry_count-- > 0 )
{
if( quality_found )
{
s->segment_runs[s->segment_run_count].first_segment = U32_AT(data_p);
}
data_p+=4;
if( quality_found )
{
s->segment_runs[s->segment_run_count].fragments_per_segment = U32_AT(data_p);
}
data_p+=4;
s->segment_run_count++;
}
return data_p;
}
static uint8_t* parse_afrt( vlc_object_t* p_this,
hds_stream_t* s,
uint8_t* data,
uint8_t* data_end )
{
uint8_t* data_p = data;
uint32_t afrt_len = U32_AT( data_p );
if( afrt_len > data_end - data ||
data_end - data < 9 )
{
msg_Err( p_this, "Not enough afrt data %u, %ld", afrt_len, data_end - data );
return NULL;
}
data_p += sizeof(afrt_len);
if( 0 != memcmp( data_p, "afrt", 4 ) )
{
msg_Err( p_this, "Cant find afrt in bootstrap" );
return NULL;
}
data_p += 4;
/* ignore flags and versions (we don't handle multiple updates) */
data_p += 4;
if( data_end - data_p < 9 )
{
msg_Err( p_this, "afrt is too short" );
return NULL;
}
s->afrt_timescale = U32_AT( data_p );
data_p += 4;
bool quality_found = false;
if( ! s->quality_segment_modifier )
{
quality_found = true;
}
uint32_t quality_entry_count = *data_p;
data_p++;
while( quality_entry_count-- > 0 )
{
char* str_start = (char*)data_p;
data_p = memchr( data_p, '\0', data_end - data_p );
if( ! data_p )
{
msg_Err( p_this, "Couldn't find quality entry string in afrt" );
return NULL;
}
data_p++;
if( ! quality_found )
{
if( ! strncmp( str_start, s->quality_segment_modifier,
strlen(s->quality_segment_modifier) ) )
{
quality_found = true;
}
}
}
if( data_end - data_p < 5 )
{
msg_Err( p_this, "No more space in afrt after quality entries" );
return NULL;
}
uint32_t fragment_run_entry_count = U32_AT( data_p );
data_p += sizeof(uint32_t);
while(fragment_run_entry_count-- > 0)
{
if( data_end - data_p < 16 )
{
msg_Err( p_this, "Not enough data in afrt" );
return NULL;
}
if( s->fragment_run_count >= MAX_HDS_FRAGMENT_RUNS )
{
msg_Err( p_this, "Too many fragment runs, exiting" );
return NULL;
}
s->fragment_runs[s->fragment_run_count].fragment_number_start = U32_AT(data_p);
data_p += 4;
s->fragment_runs[s->fragment_run_count].fragment_timestamp = U64_AT( data_p );
data_p += 8;
s->fragment_runs[s->fragment_run_count].fragment_duration = U32_AT( data_p );
data_p += 4;
s->fragment_runs[s->fragment_run_count].discont = 0;
if( s->fragment_runs[s->fragment_run_count].fragment_duration == 0 )
{
/* discontinuity flag */
s->fragment_runs[s->fragment_run_count].discont = *(data_p++);
}
s->fragment_run_count++;
}
return data_p;
}
static inline chunk_t* chunk_new()
{
chunk_t* chunk = calloc(1, sizeof(chunk_t));
return chunk;
}
static void chunk_free( chunk_t * chunk )
{
FREENULL( chunk->data );
free( chunk );
}
static void parse_BootstrapData( vlc_object_t* p_this,
hds_stream_t * s,
uint8_t* data,
uint8_t* data_end )
{
uint8_t* data_p = data;
uint32_t abst_len = U32_AT( data_p );
if( abst_len > data_end - data
|| data_end - data < 29 /* min size of data */ )
{
msg_Warn( p_this, "Not enough bootstrap data" );
return;
}
data_p += sizeof(abst_len);
if( 0 != memcmp( data_p, "abst", 4 ) )
{
msg_Warn( p_this, "Cant find abst in bootstrap" );
return;
}
data_p += 4;
/* version, flags*/
data_p += 4;
/* we ignore the version */
data_p += 4;
/* some flags we don't care about here because they are
* in the manifest
*/
data_p += 1;
/* timescale */
s->timescale = U32_AT( data_p );
data_p += sizeof(s->timescale);
s->live_current_time = U64_AT( data_p );
data_p += sizeof(s->live_current_time);
/* smtpe time code offset */
data_p += 8;
s->movie_id = strndup( (char*)data_p, data_end - data_p );
data_p += ( strlen( s->movie_id ) + 1 );
if( data_end - data_p < 4 ) {
msg_Warn( p_this, "Not enough bootstrap after Movie Identifier" );
return;
}
uint8_t server_entry_count = 0;
server_entry_count = (uint8_t) *data_p;
data_p++;
s->server_entry_count = 0;
while( server_entry_count-- > 0 )
{
if( s->server_entry_count < MAX_HDS_SERVERS )
{
s->server_entries[s->server_entry_count++] = strndup( (char*)data_p,
data_end - data_p );
data_p += strlen( s->server_entries[s->server_entry_count-1] ) + 1;
}
else
{
msg_Warn( p_this, "Too many servers" );
data_p = memchr( data_p, '\0', data_end - data_p );
if( ! data_p )
{
msg_Err( p_this, "Couldn't find server entry" );
return;
}
data_p++;
}
if( data_p >= data_end )
{
msg_Warn( p_this, "Premature end of bootstrap info while reading servers" );
return;
}
}
if( data_end - data_p < 3 ) {
msg_Warn( p_this, "Not enough bootstrap after Servers" );
return;
}
s->quality_segment_modifier = 0;
uint8_t quality_entry_count = *data_p;
data_p++;
if( quality_entry_count > 1 )
{
msg_Err( p_this, "I don't know what to do with multiple quality levels in the bootstrap - shouldn't this be handled at the manifest level?" );
return;
}
s->quality_segment_modifier = 0;
while( quality_entry_count-- > 0 )
{
if( s->quality_segment_modifier != 0 )
{
s->quality_segment_modifier = strndup( (char*)data_p, data_end - data_p );
}
data_p += strnlen( (char*)data_p, data_end - data_p ) + 1;
}
if( data_end - data_p < 2 ) {
msg_Warn( p_this, "Not enough bootstrap after quality entries" );
return;
}
/* ignoring "DrmData" */
data_p = memchr( data_p, '\0', data_end - data_p );
if( ! data_p )
{
msg_Err( p_this, "Couldn't find DRM Data" );
return;
}
data_p++;
if( data_end - data_p < 2 ) {
msg_Warn( p_this, "Not enough bootstrap after drm data" );
return;
}
/* ignoring "metadata" */
data_p = memchr( data_p, '\0', data_end - data_p );
if( ! data_p )
{
msg_Err( p_this, "Couldn't find metadata");
return;
}
data_p++;
if( data_end - data_p < 2 ) {
msg_Warn( p_this, "Not enough bootstrap after drm data" );
return;
}
uint8_t asrt_count = *data_p;
data_p++;
s->segment_run_count = 0;
while( asrt_count-- > 0 &&
data_end > data_p &&
(data_p = parse_asrt( p_this, s, data_p, data_end )) );
uint8_t afrt_count = *data_p;
data_p++;
s->fragment_run_count = 0;
while( afrt_count-- > 0 &&
data_end > data_p &&
(data_p = parse_afrt( p_this, s, data_p, data_end )) );
}
/* this only works with ANSI characters - this is ok
for the bootstrapinfo field which this function is
exclusively used for since it is merely a base64 encoding
*/
static bool is_whitespace( char c )
{
return ( ' ' == c ||
'\t' == c ||
'\n' == c ||
'\v' == c ||
'\f' == c ||
'\r' == c );
}
/* see above note for is_whitespace */
static void whitespace_substr( char** start,
char** end )
{
while( is_whitespace( **start ) && *start != *end ) {
(*start)++;
}
if( *start == *end )
return;
while( is_whitespace(*(*end - 1) ) ) {
end--;
}
}
/* returns length (could be zero, indicating all of remaining data) */
/* ptr is to start of data, right after 'mdat' string */
static uint32_t find_chunk_mdat( vlc_object_t* p_this,
uint8_t* chunkdata, uint8_t* chunkdata_end,
uint8_t** mdatptr )
{
uint8_t* boxname = 0;
uint8_t* boxdata = 0;
uint64_t boxsize = 0;
do {
if( chunkdata_end < chunkdata ||
chunkdata_end - chunkdata < 8 )
{
msg_Err( p_this, "Couldn't find mdat in box 1!" );
*mdatptr = 0;
return 0;
}
boxsize = (uint64_t)U32_AT( chunkdata );
chunkdata += 4;
boxname = chunkdata;
chunkdata += 4;
if( boxsize == 1 )
{
if( chunkdata_end - chunkdata >= 12 )
{
boxsize = U64_AT(chunkdata);
chunkdata += 8;
}
else
{
msg_Err( p_this, "Couldn't find mdat in box 2!");
*mdatptr = 0;
return 0;
}
boxdata = chunkdata;
chunkdata += (boxsize - 16);
}
else
{
boxdata = chunkdata;
chunkdata += (boxsize - 8);
}
} while ( 0 != memcmp( boxname, "mdat", 4 ) );
*mdatptr = boxdata;
return chunkdata_end - ((uint8_t*)boxdata);
}
/* returns data ptr if valid (updating the chunk itself
tells the reader that the chunk is safe to read, which is not yet correct)*/
static uint8_t* download_chunk( stream_t *s,
stream_sys_t* sys,
hds_stream_t* stream, chunk_t* chunk )
{
const char* quality = "";
char* server_base = sys->base_url;
if( stream->server_entry_count > 0 &&
strlen(stream->server_entries[0]) > 0 )
{
server_base = stream->server_entries[0];
}
if( stream->quality_segment_modifier )
{
quality = stream->quality_segment_modifier;
}
const char* movie_id = "";
if( stream->url && strlen(stream->url) > 0 )
{
if( isFQUrl( stream->url ) )
{
server_base = stream->url;
}
else
{
movie_id = stream->url;
}
}
char* fragment_url;
if( 0 > asprintf( &fragment_url, "%s/%s%sSeg%u-Frag%u",