2014-07-14 19:52:23 +03:00
/*
* WebM DASH Manifest XML muxer
* Copyright ( c ) 2014 Vignesh Venkatasubramanian
*
* 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
*/
/*
* WebM DASH Specification :
* https : //sites.google.com/a/webmproject.org/wiki/adaptive-streaming/webm-dash-specification
2015-04-01 04:40:01 +02:00
* ISO DASH Specification :
* http : //standards.iso.org/ittf/PubliclyAvailableStandards/c065274_ISO_IEC_23009-1_2014.zip
2014-07-14 19:52:23 +03:00
*/
2015-04-01 04:40:01 +02:00
# include <float.h>
2014-07-14 19:52:23 +03:00
# include <stdint.h>
# include <string.h>
2023-09-02 12:11:59 +02:00
# include <time.h>
2014-07-14 19:52:23 +03:00
# include "avformat.h"
# include "matroska.h"
2023-01-27 16:06:00 +02:00
# include "mux.h"
2014-07-14 19:52:23 +03:00
# include "libavutil/avstring.h"
# include "libavutil/dict.h"
2024-03-25 02:30:37 +02:00
# include "libavutil/mem.h"
2014-07-14 19:52:23 +03:00
# include "libavutil/opt.h"
2015-04-01 04:40:01 +02:00
# include "libavutil/time_internal.h"
2014-07-14 19:52:23 +03:00
2021-08-01 08:36:09 +02:00
# include "libavcodec/codec_desc.h"
2014-07-14 19:52:23 +03:00
typedef struct AdaptationSet {
char id [ 10 ] ;
int * streams ;
int nb_streams ;
} AdaptationSet ;
typedef struct WebMDashMuxContext {
const AVClass * class ;
char * adaptation_sets ;
AdaptationSet * as ;
int nb_as ;
2014-11-11 20:02:05 +02:00
int representation_id ;
2015-04-01 04:40:01 +02:00
int is_live ;
int chunk_start_index ;
int chunk_duration ;
char * utc_timing_url ;
double time_shift_buffer_depth ;
2015-04-22 02:35:29 +02:00
int minimum_update_period ;
2014-07-14 19:52:23 +03:00
} WebMDashMuxContext ;
static const char * get_codec_name ( int codec_id )
{
2020-03-30 03:09:41 +02:00
return avcodec_descriptor_get ( codec_id ) - > name ;
2014-07-14 19:52:23 +03:00
}
static double get_duration ( AVFormatContext * s )
{
int i = 0 ;
double max = 0.0 ;
for ( i = 0 ; i < s - > nb_streams ; i + + ) {
AVDictionaryEntry * duration = av_dict_get ( s - > streams [ i ] - > metadata ,
DURATION , NULL , 0 ) ;
2014-08-15 21:33:21 +03:00
if ( ! duration | | atof ( duration - > value ) < 0 ) continue ;
2014-07-14 19:52:23 +03:00
if ( atof ( duration - > value ) > max ) max = atof ( duration - > value ) ;
}
return max / 1000 ;
}
2015-05-14 19:32:24 +02:00
static int write_header ( AVFormatContext * s )
2014-07-14 19:52:23 +03:00
{
2015-04-01 04:40:01 +02:00
WebMDashMuxContext * w = s - > priv_data ;
2020-07-17 07:32:52 +02:00
AVIOContext * pb = s - > pb ;
2014-07-14 19:52:23 +03:00
double min_buffer_time = 1.0 ;
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " <?xml version= \" 1.0 \" encoding= \" UTF-8 \" ?> \n " ) ;
avio_printf ( pb , " <MPD \n " ) ;
avio_printf ( pb , " xmlns:xsi= \" http://www.w3.org/2001/XMLSchema-instance \" \n " ) ;
avio_printf ( pb , " xmlns= \" urn:mpeg:DASH:schema:MPD:2011 \" \n " ) ;
avio_printf ( pb , " xsi:schemaLocation= \" urn:mpeg:DASH:schema:MPD:2011 \" \n " ) ;
avio_printf ( pb , " type= \" %s \" \n " , w - > is_live ? " dynamic " : " static " ) ;
2015-04-01 04:40:01 +02:00
if ( ! w - > is_live ) {
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " mediaPresentationDuration= \" PT%gS \" \n " ,
2015-04-01 04:40:01 +02:00
get_duration ( s ) ) ;
}
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " minBufferTime= \" PT%gS \" \n " , min_buffer_time ) ;
avio_printf ( pb , " profiles= \" %s \" %s " ,
2022-04-08 02:57:42 +02:00
w - > is_live ? " urn:mpeg:dash:profile:isoff-live:2011 " : " urn:mpeg:dash:profile:webm-on-demand:2012 " ,
2015-04-01 04:40:01 +02:00
w - > is_live ? " \n " : " > \n " ) ;
if ( w - > is_live ) {
2015-04-07 21:21:02 +02:00
time_t local_time = time ( NULL ) ;
struct tm gmt_buffer ;
struct tm * gmt = gmtime_r ( & local_time , & gmt_buffer ) ;
2015-04-14 19:54:23 +02:00
char gmt_iso [ 21 ] ;
2015-05-14 19:32:24 +02:00
if ( ! strftime ( gmt_iso , 21 , " %Y-%m-%dT%H:%M:%SZ " , gmt ) ) {
return AVERROR_UNKNOWN ;
}
2020-03-18 00:13:05 +02:00
if ( s - > flags & AVFMT_FLAG_BITEXACT ) {
2015-04-07 21:21:02 +02:00
av_strlcpy ( gmt_iso , " " , 1 ) ;
}
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " availabilityStartTime= \" %s \" \n " , gmt_iso ) ;
avio_printf ( pb , " timeShiftBufferDepth= \" PT%gS \" \n " , w - > time_shift_buffer_depth ) ;
avio_printf ( pb , " minimumUpdatePeriod= \" PT%dS \" " , w - > minimum_update_period ) ;
avio_printf ( pb , " > \n " ) ;
2015-04-28 01:26:34 +02:00
if ( w - > utc_timing_url ) {
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " <UTCTiming \n " ) ;
avio_printf ( pb , " schemeIdUri= \" urn:mpeg:dash:utc:http-iso:2014 \" \n " ) ;
avio_printf ( pb , " value= \" %s \" /> \n " , w - > utc_timing_url ) ;
2015-04-28 01:26:34 +02:00
}
2015-04-01 04:40:01 +02:00
}
2015-05-14 19:32:24 +02:00
return 0 ;
2014-07-14 19:52:23 +03:00
}
static void write_footer ( AVFormatContext * s )
{
2014-09-16 18:24:56 +03:00
avio_printf ( s - > pb , " </MPD> \n " ) ;
2014-07-14 19:52:23 +03:00
}
2020-07-17 07:32:52 +02:00
static int subsegment_alignment ( AVFormatContext * s , const AdaptationSet * as )
{
2014-07-14 19:52:23 +03:00
int i ;
AVDictionaryEntry * gold = av_dict_get ( s - > streams [ as - > streams [ 0 ] ] - > metadata ,
CUE_TIMESTAMPS , NULL , 0 ) ;
2014-08-15 21:33:21 +03:00
if ( ! gold ) return 0 ;
2014-07-14 19:52:23 +03:00
for ( i = 1 ; i < as - > nb_streams ; i + + ) {
AVDictionaryEntry * ts = av_dict_get ( s - > streams [ as - > streams [ i ] ] - > metadata ,
CUE_TIMESTAMPS , NULL , 0 ) ;
2021-02-24 08:46:22 +02:00
if ( ! ts | | ! av_strstart ( ts - > value , gold - > value , NULL ) ) return 0 ;
2014-07-14 19:52:23 +03:00
}
return 1 ;
}
2020-07-17 07:32:52 +02:00
static int bitstream_switching ( AVFormatContext * s , const AdaptationSet * as )
{
2014-07-14 19:52:23 +03:00
int i ;
2020-07-17 07:32:52 +02:00
const AVStream * gold_st = s - > streams [ as - > streams [ 0 ] ] ;
AVDictionaryEntry * gold_track_num = av_dict_get ( gold_st - > metadata ,
2014-07-14 19:52:23 +03:00
TRACK_NUMBER , NULL , 0 ) ;
2020-07-17 07:32:52 +02:00
AVCodecParameters * gold_par = gold_st - > codecpar ;
2014-08-15 21:33:21 +03:00
if ( ! gold_track_num ) return 0 ;
2014-07-14 19:52:23 +03:00
for ( i = 1 ; i < as - > nb_streams ; i + + ) {
2020-07-17 07:32:52 +02:00
const AVStream * st = s - > streams [ as - > streams [ i ] ] ;
AVDictionaryEntry * track_num = av_dict_get ( st - > metadata ,
2014-07-14 19:52:23 +03:00
TRACK_NUMBER , NULL , 0 ) ;
2020-07-17 07:32:52 +02:00
AVCodecParameters * par = st - > codecpar ;
2014-08-15 21:33:21 +03:00
if ( ! track_num | |
2021-02-24 08:46:22 +02:00
! av_strstart ( track_num - > value , gold_track_num - > value , NULL ) | |
2016-04-10 21:58:15 +02:00
gold_par - > codec_id ! = par - > codec_id | |
gold_par - > extradata_size ! = par - > extradata_size | |
2019-09-23 23:23:10 +02:00
( par - > extradata_size > 0 & &
memcmp ( gold_par - > extradata , par - > extradata , par - > extradata_size ) ) ) {
2014-07-14 19:52:23 +03:00
return 0 ;
}
}
return 1 ;
}
2014-10-01 20:13:32 +03:00
/*
* Writes a Representation within an Adaptation Set . Returns 0 on success and
* < 0 on failure .
*/
2020-07-17 07:32:52 +02:00
static int write_representation ( AVFormatContext * s , AVStream * st , char * id ,
2014-10-01 20:13:32 +03:00
int output_width , int output_height ,
2020-07-17 07:32:52 +02:00
int output_sample_rate )
{
2015-04-01 04:40:01 +02:00
WebMDashMuxContext * w = s - > priv_data ;
2020-07-17 07:32:52 +02:00
AVIOContext * pb = s - > pb ;
const AVCodecParameters * par = st - > codecpar ;
AVDictionaryEntry * bandwidth = av_dict_get ( st - > metadata , BANDWIDTH , NULL , 0 ) ;
2017-04-12 06:33:28 +02:00
const char * bandwidth_str ;
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " <Representation id= \" %s \" " , id ) ;
2020-07-17 06:17:44 +02:00
if ( bandwidth ) {
bandwidth_str = bandwidth - > value ;
} else if ( w - > is_live ) {
// if bandwidth for live was not provided, use a default
2020-07-17 07:32:52 +02:00
bandwidth_str = ( par - > codec_type = = AVMEDIA_TYPE_AUDIO ) ? " 128000 " : " 1000000 " ;
2017-04-12 06:33:28 +02:00
} else {
2020-07-17 06:17:44 +02:00
return AVERROR ( EINVAL ) ;
2017-04-12 06:33:28 +02:00
}
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " bandwidth= \" %s \" " , bandwidth_str ) ;
if ( par - > codec_type = = AVMEDIA_TYPE_VIDEO & & output_width )
avio_printf ( pb , " width= \" %d \" " , par - > width ) ;
if ( par - > codec_type = = AVMEDIA_TYPE_VIDEO & & output_height )
avio_printf ( pb , " height= \" %d \" " , par - > height ) ;
if ( par - > codec_type = = AVMEDIA_TYPE_AUDIO & & output_sample_rate )
avio_printf ( pb , " audioSamplingRate= \" %d \" " , par - > sample_rate ) ;
2015-04-01 04:40:01 +02:00
if ( w - > is_live ) {
// For live streams, Codec and Mime Type always go in the Representation tag.
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " codecs= \" %s \" " , get_codec_name ( par - > codec_id ) ) ;
avio_printf ( pb , " mimeType= \" %s/webm \" " ,
par - > codec_type = = AVMEDIA_TYPE_VIDEO ? " video " : " audio " ) ;
2015-04-01 04:40:01 +02:00
// For live streams, subsegments always start with key frames. So this
// is always 1.
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " startsWithSAP= \" 1 \" " ) ;
avio_printf ( pb , " > " ) ;
2015-04-01 04:40:01 +02:00
} else {
2020-07-17 07:32:52 +02:00
AVDictionaryEntry * irange = av_dict_get ( st - > metadata , INITIALIZATION_RANGE , NULL , 0 ) ;
AVDictionaryEntry * cues_start = av_dict_get ( st - > metadata , CUES_START , NULL , 0 ) ;
AVDictionaryEntry * cues_end = av_dict_get ( st - > metadata , CUES_END , NULL , 0 ) ;
AVDictionaryEntry * filename = av_dict_get ( st - > metadata , FILENAME , NULL , 0 ) ;
2020-07-17 06:17:44 +02:00
if ( ! irange | | ! cues_start | | ! cues_end | | ! filename )
return AVERROR ( EINVAL ) ;
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " > \n " ) ;
avio_printf ( pb , " <BaseURL>%s</BaseURL> \n " , filename - > value ) ;
avio_printf ( pb , " <SegmentBase \n " ) ;
avio_printf ( pb , " indexRange= \" %s-%s \" > \n " , cues_start - > value , cues_end - > value ) ;
avio_printf ( pb , " <Initialization \n " ) ;
avio_printf ( pb , " range= \" 0-%s \" /> \n " , irange - > value ) ;
avio_printf ( pb , " </SegmentBase> \n " ) ;
2015-04-01 04:40:01 +02:00
}
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " </Representation> \n " ) ;
2014-10-01 20:13:32 +03:00
return 0 ;
}
/*
* Checks if width of all streams are the same . Returns 1 if true , 0 otherwise .
*/
2020-07-17 07:32:52 +02:00
static int check_matching_width ( AVFormatContext * s , const AdaptationSet * as )
{
2014-10-01 20:13:32 +03:00
int first_width , i ;
if ( as - > nb_streams < 2 ) return 1 ;
2016-04-10 21:58:15 +02:00
first_width = s - > streams [ as - > streams [ 0 ] ] - > codecpar - > width ;
2014-10-01 20:13:32 +03:00
for ( i = 1 ; i < as - > nb_streams ; i + + )
2016-04-10 21:58:15 +02:00
if ( first_width ! = s - > streams [ as - > streams [ i ] ] - > codecpar - > width )
2014-10-01 20:13:32 +03:00
return 0 ;
return 1 ;
}
/*
* Checks if height of all streams are the same . Returns 1 if true , 0 otherwise .
*/
2020-07-17 07:32:52 +02:00
static int check_matching_height ( AVFormatContext * s , const AdaptationSet * as )
{
2014-10-01 20:13:32 +03:00
int first_height , i ;
if ( as - > nb_streams < 2 ) return 1 ;
2016-04-10 21:58:15 +02:00
first_height = s - > streams [ as - > streams [ 0 ] ] - > codecpar - > height ;
2014-10-01 20:13:32 +03:00
for ( i = 1 ; i < as - > nb_streams ; i + + )
2016-04-10 21:58:15 +02:00
if ( first_height ! = s - > streams [ as - > streams [ i ] ] - > codecpar - > height )
2014-10-01 20:13:32 +03:00
return 0 ;
return 1 ;
}
/*
* Checks if sample rate of all streams are the same . Returns 1 if true , 0 otherwise .
*/
2020-07-17 07:32:52 +02:00
static int check_matching_sample_rate ( AVFormatContext * s , const AdaptationSet * as )
{
2014-10-01 20:13:32 +03:00
int first_sample_rate , i ;
if ( as - > nb_streams < 2 ) return 1 ;
2016-04-10 21:58:15 +02:00
first_sample_rate = s - > streams [ as - > streams [ 0 ] ] - > codecpar - > sample_rate ;
2014-10-01 20:13:32 +03:00
for ( i = 1 ; i < as - > nb_streams ; i + + )
2016-04-10 21:58:15 +02:00
if ( first_sample_rate ! = s - > streams [ as - > streams [ i ] ] - > codecpar - > sample_rate )
2014-10-01 20:13:32 +03:00
return 0 ;
return 1 ;
}
2020-07-17 07:32:52 +02:00
static void free_adaptation_sets ( AVFormatContext * s )
{
2015-04-17 02:32:21 +02:00
WebMDashMuxContext * w = s - > priv_data ;
int i ;
for ( i = 0 ; i < w - > nb_as ; i + + ) {
av_freep ( & w - > as [ i ] . streams ) ;
}
av_freep ( & w - > as ) ;
w - > nb_as = 0 ;
}
2015-04-01 04:40:01 +02:00
/*
2020-07-15 00:06:10 +02:00
* Parses a live header filename and returns the position of the ' _ ' and ' . '
* delimiting < file_description > and < representation_id > .
2015-04-01 04:40:01 +02:00
*
* Name of the header file should conform to the following pattern :
* < file_description > _ < representation_id > . hdr where < file_description > can be
* anything . The chunks should be named according to the following pattern :
* < file_description > _ < representation_id > _ < chunk_number > . chk
*/
2020-07-15 00:06:10 +02:00
static int split_filename ( char * filename , char * * underscore_pos ,
char * * period_pos )
{
* underscore_pos = strrchr ( filename , ' _ ' ) ;
if ( ! * underscore_pos )
return AVERROR ( EINVAL ) ;
* period_pos = strchr ( * underscore_pos , ' . ' ) ;
if ( ! * period_pos )
return AVERROR ( EINVAL ) ;
return 0 ;
2015-04-01 04:40:01 +02:00
}
2014-07-14 19:52:23 +03:00
/*
* Writes an Adaptation Set . Returns 0 on success and < 0 on failure .
*/
static int write_adaptation_set ( AVFormatContext * s , int as_index )
{
WebMDashMuxContext * w = s - > priv_data ;
AdaptationSet * as = & w - > as [ as_index ] ;
2020-07-17 07:32:52 +02:00
const AVStream * st = s - > streams [ as - > streams [ 0 ] ] ;
AVCodecParameters * par = st - > codecpar ;
2014-10-01 20:13:32 +03:00
AVDictionaryEntry * lang ;
2020-07-17 07:32:52 +02:00
AVIOContext * pb = s - > pb ;
2014-07-14 19:52:23 +03:00
int i ;
static const char boolean [ 2 ] [ 6 ] = { " false " , " true " } ;
int subsegmentStartsWithSAP = 1 ;
2014-10-01 20:13:32 +03:00
// Width, Height and Sample Rate will go in the AdaptationSet tag if they
// are the same for all contained Representations. otherwise, they will go
2015-04-01 04:40:01 +02:00
// on their respective Representation tag. For live streams, they always go
// in the Representation tag.
2014-10-01 20:13:32 +03:00
int width_in_as = 1 , height_in_as = 1 , sample_rate_in_as = 1 ;
2016-04-10 21:58:15 +02:00
if ( par - > codec_type = = AVMEDIA_TYPE_VIDEO ) {
2020-07-17 07:32:52 +02:00
width_in_as = ! w - > is_live & & check_matching_width ( s , as ) ;
2015-04-01 04:40:01 +02:00
height_in_as = ! w - > is_live & & check_matching_height ( s , as ) ;
2014-10-01 20:13:32 +03:00
} else {
2015-04-01 04:40:01 +02:00
sample_rate_in_as = ! w - > is_live & & check_matching_sample_rate ( s , as ) ;
2014-10-01 20:13:32 +03:00
}
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " <AdaptationSet id= \" %s \" " , as - > id ) ;
avio_printf ( pb , " mimeType= \" %s/webm \" " ,
2016-04-10 21:58:15 +02:00
par - > codec_type = = AVMEDIA_TYPE_VIDEO ? " video " : " audio " ) ;
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " codecs= \" %s \" " , get_codec_name ( par - > codec_id ) ) ;
2014-07-14 19:52:23 +03:00
2020-07-17 07:32:52 +02:00
lang = av_dict_get ( st - > metadata , " language " , NULL , 0 ) ;
if ( lang )
avio_printf ( pb , " lang= \" %s \" " , lang - > value ) ;
2014-07-14 19:52:23 +03:00
2016-04-10 21:58:15 +02:00
if ( par - > codec_type = = AVMEDIA_TYPE_VIDEO & & width_in_as )
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " width= \" %d \" " , par - > width ) ;
2016-04-10 21:58:15 +02:00
if ( par - > codec_type = = AVMEDIA_TYPE_VIDEO & & height_in_as )
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " height= \" %d \" " , par - > height ) ;
2016-04-10 21:58:15 +02:00
if ( par - > codec_type = = AVMEDIA_TYPE_AUDIO & & sample_rate_in_as )
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " audioSamplingRate= \" %d \" " , par - > sample_rate ) ;
2014-07-14 19:52:23 +03:00
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " bitstreamSwitching= \" %s \" " ,
2014-07-14 19:52:23 +03:00
boolean [ bitstream_switching ( s , as ) ] ) ;
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " subsegmentAlignment= \" %s \" " ,
2015-04-01 04:40:01 +02:00
boolean [ w - > is_live | | subsegment_alignment ( s , as ) ] ) ;
2014-07-14 19:52:23 +03:00
for ( i = 0 ; i < as - > nb_streams ; i + + ) {
AVDictionaryEntry * kf = av_dict_get ( s - > streams [ as - > streams [ i ] ] - > metadata ,
CLUSTER_KEYFRAME , NULL , 0 ) ;
2015-04-01 04:40:01 +02:00
if ( ! w - > is_live & & ( ! kf | | ! strncmp ( kf - > value , " 0 " , 1 ) ) ) subsegmentStartsWithSAP = 0 ;
2014-07-14 19:52:23 +03:00
}
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " subsegmentStartsWithSAP= \" %d \" " , subsegmentStartsWithSAP ) ;
avio_printf ( pb , " > \n " ) ;
2014-07-14 19:52:23 +03:00
2015-04-01 04:40:01 +02:00
if ( w - > is_live ) {
AVDictionaryEntry * filename =
2020-07-17 07:32:52 +02:00
av_dict_get ( st - > metadata , FILENAME , NULL , 0 ) ;
2020-07-15 00:06:10 +02:00
char * underscore_pos , * period_pos ;
2020-07-14 22:49:15 +02:00
int ret ;
if ( ! filename )
return AVERROR ( EINVAL ) ;
2020-07-15 00:06:10 +02:00
ret = split_filename ( filename - > value , & underscore_pos , & period_pos ) ;
2015-04-01 04:40:01 +02:00
if ( ret ) return ret ;
2020-07-15 00:06:10 +02:00
* underscore_pos = ' \0 ' ;
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " <ContentComponent id= \" 1 \" type= \" %s \" /> \n " ,
2016-04-10 21:58:15 +02:00
par - > codec_type = = AVMEDIA_TYPE_VIDEO ? " video " : " audio " ) ;
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " <SegmentTemplate " ) ;
avio_printf ( pb , " timescale= \" 1000 \" " ) ;
avio_printf ( pb , " duration= \" %d \" " , w - > chunk_duration ) ;
avio_printf ( pb , " media= \" %s_$RepresentationID$_$Number$.chk \" " ,
2020-07-15 00:06:10 +02:00
filename - > value ) ;
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " startNumber= \" %d \" " , w - > chunk_start_index ) ;
avio_printf ( pb , " initialization= \" %s_$RepresentationID$.hdr \" " ,
2020-07-15 00:06:10 +02:00
filename - > value ) ;
2020-07-17 07:32:52 +02:00
avio_printf ( pb , " /> \n " ) ;
2020-07-15 00:06:10 +02:00
* underscore_pos = ' _ ' ;
2015-04-01 04:40:01 +02:00
}
2014-07-14 19:52:23 +03:00
for ( i = 0 ; i < as - > nb_streams ; i + + ) {
2020-07-15 00:06:10 +02:00
char buf [ 25 ] , * representation_id = buf , * underscore_pos , * period_pos ;
2020-07-17 07:32:52 +02:00
AVStream * st = s - > streams [ as - > streams [ i ] ] ;
2015-04-17 02:32:21 +02:00
int ret ;
2015-04-01 04:40:01 +02:00
if ( w - > is_live ) {
AVDictionaryEntry * filename =
2020-07-17 07:32:52 +02:00
av_dict_get ( st - > metadata , FILENAME , NULL , 0 ) ;
2015-08-20 17:18:14 +02:00
if ( ! filename )
return AVERROR ( EINVAL ) ;
2020-07-15 00:06:10 +02:00
ret = split_filename ( filename - > value , & underscore_pos , & period_pos ) ;
if ( ret < 0 )
2015-04-17 02:32:21 +02:00
return ret ;
2020-07-15 00:06:10 +02:00
representation_id = underscore_pos + 1 ;
* period_pos = ' \0 ' ;
2015-04-01 04:40:01 +02:00
} else {
2020-07-15 00:06:10 +02:00
snprintf ( buf , sizeof ( buf ) , " %d " , w - > representation_id + + ) ;
2015-04-01 04:40:01 +02:00
}
2020-07-17 07:32:52 +02:00
ret = write_representation ( s , st , representation_id , ! width_in_as ,
2015-04-17 02:32:21 +02:00
! height_in_as , ! sample_rate_in_as ) ;
2015-04-20 20:20:41 +02:00
if ( ret ) return ret ;
2020-07-15 00:06:10 +02:00
if ( w - > is_live )
* period_pos = ' . ' ;
2014-07-14 19:52:23 +03:00
}
avio_printf ( s - > pb , " </AdaptationSet> \n " ) ;
return 0 ;
}
static int parse_adaptation_sets ( AVFormatContext * s )
{
WebMDashMuxContext * w = s - > priv_data ;
char * p = w - > adaptation_sets ;
char * q ;
enum { new_set , parsed_id , parsing_streams } state ;
2017-04-20 14:14:42 +02:00
if ( ! w - > adaptation_sets ) {
av_log ( s , AV_LOG_ERROR , " The 'adaptation_sets' option must be set. \n " ) ;
return AVERROR ( EINVAL ) ;
}
2014-07-14 19:52:23 +03:00
// syntax id=0,streams=0,1,2 id=1,streams=3,4 and so on
state = new_set ;
2020-05-18 04:43:25 +02:00
while ( 1 ) {
if ( * p = = ' \0 ' ) {
if ( state = = new_set )
break ;
else
return AVERROR ( EINVAL ) ;
} else if ( state = = new_set & & * p = = ' ' ) {
2020-05-18 04:19:32 +02:00
p + + ;
2014-07-14 19:52:23 +03:00
continue ;
2020-05-18 04:19:32 +02:00
} else if ( state = = new_set & & ! strncmp ( p , " id= " , 3 ) ) {
2015-04-17 02:32:21 +02:00
void * mem = av_realloc ( w - > as , sizeof ( * w - > as ) * ( w - > nb_as + 1 ) ) ;
2019-02-13 11:15:04 +02:00
const char * comma ;
2015-04-17 02:32:21 +02:00
if ( mem = = NULL )
2015-04-01 18:11:48 +02:00
return AVERROR ( ENOMEM ) ;
2015-04-17 02:32:21 +02:00
w - > as = mem ;
+ + w - > nb_as ;
2014-07-14 19:52:23 +03:00
w - > as [ w - > nb_as - 1 ] . nb_streams = 0 ;
w - > as [ w - > nb_as - 1 ] . streams = NULL ;
p + = 3 ; // consume "id="
q = w - > as [ w - > nb_as - 1 ] . id ;
2019-02-13 11:15:04 +02:00
comma = strchr ( p , ' , ' ) ;
if ( ! comma | | comma - p > = sizeof ( w - > as [ w - > nb_as - 1 ] . id ) ) {
av_log ( s , AV_LOG_ERROR , " 'id' in 'adaptation_sets' is malformed. \n " ) ;
return AVERROR ( EINVAL ) ;
}
2014-07-14 19:52:23 +03:00
while ( * p ! = ' , ' ) * q + + = * p + + ;
* q = 0 ;
p + + ;
state = parsed_id ;
} else if ( state = = parsed_id & & ! strncmp ( p , " streams= " , 8 ) ) {
p + = 8 ; // consume "streams="
state = parsing_streams ;
} else if ( state = = parsing_streams ) {
struct AdaptationSet * as = & w - > as [ w - > nb_as - 1 ] ;
2020-05-18 03:17:50 +02:00
int64_t num ;
2019-12-20 22:21:59 +02:00
int ret = av_reallocp_array ( & as - > streams , + + as - > nb_streams ,
sizeof ( * as - > streams ) ) ;
if ( ret < 0 )
return ret ;
2020-05-18 03:17:50 +02:00
num = strtoll ( p , & q , 10 ) ;
if ( ! av_isdigit ( * p ) | | ( * q ! = ' ' & & * q ! = ' \0 ' & & * q ! = ' , ' ) | |
num < 0 | | num > = s - > nb_streams ) {
2017-04-20 17:17:44 +02:00
av_log ( s , AV_LOG_ERROR , " Invalid value for 'streams' in adapation_sets. \n " ) ;
return AVERROR ( EINVAL ) ;
}
2020-05-18 03:17:50 +02:00
as - > streams [ as - > nb_streams - 1 ] = num ;
2014-07-14 19:52:23 +03:00
if ( * q = = ' \0 ' ) break ;
if ( * q = = ' ' ) state = new_set ;
p = + + q ;
} else {
return - 1 ;
}
}
return 0 ;
}
static int webm_dash_manifest_write_header ( AVFormatContext * s )
{
int i ;
double start = 0.0 ;
2015-04-17 02:32:21 +02:00
int ret ;
2014-07-14 19:52:23 +03:00
WebMDashMuxContext * w = s - > priv_data ;
2020-03-30 02:50:02 +02:00
for ( unsigned i = 0 ; i < s - > nb_streams ; i + + ) {
enum AVCodecID codec_id = s - > streams [ i ] - > codecpar - > codec_id ;
if ( codec_id ! = AV_CODEC_ID_VP8 & & codec_id ! = AV_CODEC_ID_VP9 & &
2021-06-20 22:19:18 +02:00
codec_id ! = AV_CODEC_ID_AV1 & & codec_id ! = AV_CODEC_ID_VORBIS & &
codec_id ! = AV_CODEC_ID_OPUS )
2020-03-30 02:50:02 +02:00
return AVERROR ( EINVAL ) ;
}
2015-04-17 02:32:21 +02:00
ret = parse_adaptation_sets ( s ) ;
if ( ret < 0 ) {
2015-05-14 19:32:24 +02:00
goto fail ;
}
ret = write_header ( s ) ;
if ( ret < 0 ) {
goto fail ;
2015-04-17 02:32:21 +02:00
}
2014-07-14 19:52:23 +03:00
avio_printf ( s - > pb , " <Period id= \" 0 \" " ) ;
avio_printf ( s - > pb , " start= \" PT%gS \" " , start ) ;
2015-04-01 04:40:01 +02:00
if ( ! w - > is_live ) {
avio_printf ( s - > pb , " duration= \" PT%gS \" " , get_duration ( s ) ) ;
}
2014-07-14 19:52:23 +03:00
avio_printf ( s - > pb , " > \n " ) ;
for ( i = 0 ; i < w - > nb_as ; i + + ) {
2015-04-17 02:32:21 +02:00
ret = write_adaptation_set ( s , i ) ;
if ( ret < 0 ) {
2015-05-14 19:32:24 +02:00
goto fail ;
2015-04-17 02:32:21 +02:00
}
2014-07-14 19:52:23 +03:00
}
avio_printf ( s - > pb , " </Period> \n " ) ;
write_footer ( s ) ;
2015-05-14 19:32:24 +02:00
fail :
free_adaptation_sets ( s ) ;
return ret < 0 ? ret : 0 ;
2014-07-14 19:52:23 +03:00
}
static int webm_dash_manifest_write_packet ( AVFormatContext * s , AVPacket * pkt )
{
return AVERROR_EOF ;
}
# define OFFSET(x) offsetof(WebMDashMuxContext, x)
static const AVOption options [ ] = {
{ " adaptation_sets " , " Adaptation sets. Syntax: id=0,streams=0,1,2 id=1,streams=3,4 and so on " , OFFSET ( adaptation_sets ) , AV_OPT_TYPE_STRING , { 0 } , 0 , 0 , AV_OPT_FLAG_ENCODING_PARAM } ,
2015-11-21 23:05:07 +02:00
{ " live " , " create a live stream manifest " , OFFSET ( is_live ) , AV_OPT_TYPE_BOOL , { . i64 = 0 } , 0 , 1 , AV_OPT_FLAG_ENCODING_PARAM } ,
2015-04-01 04:40:01 +02:00
{ " chunk_start_index " , " start index of the chunk " , OFFSET ( chunk_start_index ) , AV_OPT_TYPE_INT , { . i64 = 0 } , 0 , INT_MAX , AV_OPT_FLAG_ENCODING_PARAM } ,
{ " chunk_duration_ms " , " duration of each chunk (in milliseconds) " , OFFSET ( chunk_duration ) , AV_OPT_TYPE_INT , { . i64 = 1000 } , 0 , INT_MAX , AV_OPT_FLAG_ENCODING_PARAM } ,
{ " utc_timing_url " , " URL of the page that will return the UTC timestamp in ISO format " , OFFSET ( utc_timing_url ) , AV_OPT_TYPE_STRING , { 0 } , 0 , 0 , AV_OPT_FLAG_ENCODING_PARAM } ,
{ " time_shift_buffer_depth " , " Smallest time (in seconds) shifting buffer for which any Representation is guaranteed to be available. " , OFFSET ( time_shift_buffer_depth ) , AV_OPT_TYPE_DOUBLE , { . dbl = 60.0 } , 1.0 , DBL_MAX , AV_OPT_FLAG_ENCODING_PARAM } ,
2015-04-22 02:35:29 +02:00
{ " minimum_update_period " , " Minimum Update Period (in seconds) of the manifest. " , OFFSET ( minimum_update_period ) , AV_OPT_TYPE_INT , { . i64 = 0 } , 0 , INT_MAX , AV_OPT_FLAG_ENCODING_PARAM } ,
2014-07-14 19:52:23 +03:00
{ NULL } ,
} ;
static const AVClass webm_dash_class = {
. class_name = " WebM DASH Manifest muxer " ,
2024-01-19 14:33:28 +02:00
. item_name = av_default_item_name ,
2014-07-14 19:52:23 +03:00
. option = options ,
. version = LIBAVUTIL_VERSION_INT ,
} ;
2023-01-27 16:06:00 +02:00
const FFOutputFormat ff_webm_dash_manifest_muxer = {
. p . name = " webm_dash_manifest " ,
. p . long_name = NULL_IF_CONFIG_SMALL ( " WebM DASH Manifest " ) ,
. p . mime_type = " application/xml " ,
. p . extensions = " xml " ,
2014-07-14 19:52:23 +03:00
. priv_data_size = sizeof ( WebMDashMuxContext ) ,
. write_header = webm_dash_manifest_write_header ,
. write_packet = webm_dash_manifest_write_packet ,
2023-01-27 16:06:00 +02:00
. p . priv_class = & webm_dash_class ,
2014-07-14 19:52:23 +03:00
} ;