1 简介
在dash 协议出现以前, 各家公司都开发自己的私有流媒体协议,如微软的SS, 苹果的HLS,Adobe 公司的HDS, 3GPP组织的AHS。这给客户端开发者带来了很大的困扰,后由MPEG 组织牵头,参考前几家公司的流媒体协议,共同制定DASH 协议,也称MPEG-DASH,协议标准号为:ISO/IEC23009, 标准共计八部分,其中媒体呈现(MPD)是最重要的一部分,占据正文的70% 以上。关于DASH 协议的演进历史如下图所示。
dash 协议一经推出,就被很多公司接入,有着一统江湖的趋势,除了由MPEG 组织背书外,还与DASH 技术优势由莫大的关系,如下图是dash 协议与其他流媒体协议的比较。
2 mpd 文件分析
1 period 字段
一条完整的mpeg dash stream 可能由一个或者多个period 构成,同一period 内意味着可用媒体内容及其各个可用的码率不会发生变化。直播情况下,需要定期更新MPD 文件。
2 Adaptationset 字段
一个Period 由一个或者多个Adaptationset组成, Adaptationset 由一组可供切换的不同码率的码流组成,这些码流中可能包含一个或者多个media content components。
3 media content component 字段
一个media content component 表示一个不同音视频内容,比如不同语言的音轨属于不同的media content component ,而同一音轨的不同码率属于相同的media content component .
4 Representation 字段
每个Adaptationset 包含一个或者多个Representation , 一个Representation 包含一个或者多个media stream,每个media stream 对应一个media content component, 为了适应不同带宽,dash 网络可能从一个Representation 切换到另一个Representation。
5 Segment 字段
与HLS 协议的segment 一样,每个Representation 由一个或者多个segment 组成, 每个segment 由一个对应的URL 指定,也可能由相同url + 不同的byte range 指定。
MPD 的数据格式组成如下图所示: period - > adaptation -> Representation -> segment
以上就是关于mpd 格式的介绍, 下面我们根据一个原始mpd 文件来分析下mpd 格式内容
<?xml version="1.0" encoding="utf-8"?>
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:mpeg:dash:schema:mpd:2011"
xmlns:xlink="http://www.w3.org/1999/xlink"
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
profiles="urn:mpeg:dash:profile:isoff-live:2011"
type="static"
mediaPresentationDuration="PT38M57.3S"
minBufferTime="PT27.7S">
<ProgramInformation>
</ProgramInformation>
<Period id="0" start="PT0.0S"> <-- period 字段 ,最外面一层壳 -->
<AdaptationSet id="0" contentType="video" segmentAlignment="true" bitstreamSwitching="true" lang="eng"> <-- Adaptationset 字段,video/audio 属不同的Adaptationset-->
<Representation id="0" mimeType="video/mp4" codecs="mp4v.20" bandwidth="197991" width="320" height="240" frameRate="18/1"> <-- Representation 里面存放不同码率的stream -->
<SegmentTemplate timescale="18432" initialization="init-stream$RepresentationID$.mp4" media="chunk-stream$RepresentationID$-$Number%05d$.mp4" startNumber="202"> <--segment , init segment (存放媒体初始化信息)+ stream segment(实际的音视频数据)-->
<SegmentTimeline>
<S t="42299392" d="256000" r="2" />
<S d="14336" />
</SegmentTimeline>
</SegmentTemplate>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="audio" segmentAlignment="true" bitstreamSwitching="true" lang="eng">
<Representation id="1" mimeType="audio/mp4" codecs="mp4a.40.2" bandwidth="125579" audioSamplingRate="44100">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
<SegmentTemplate timescale="44100" initialization="init-stream$RepresentationID$.mp4" media="chunk-stream$RepresentationID$-$Number%05d$.mp4" startNumber="100">
<SegmentTimeline>
<S t="49667072" d="612352" r="2" />
<S d="31744" />
</SegmentTimeline>
</SegmentTemplate>
</Representation>
</AdaptationSet>
</Period>
</MPD>
3 ffmpeg 对dash 切片
ffmpeg.exe -re -i test.mp4 -codec copy -f dash -window_size 4 -extra_window_size 5 index.mpd
用ffmeg 对音视频进行切片时,它会生成三类文件,mpd + init-stream0/1.mp4 + chunk-streamx-xxxx.mp4 。 其中mpd 用来记录切片的整体信息,init-stream0/1.mp4 相当于MP4文件的moov box , 它记录了当前stream 的metadata信息,还有sps,pps 等等信息。
dash 切片成的格式为fmp4, 可以简单理解成分片化的MP4,是dash 采用的媒体文件格式。普通的MP4 文件由moov + mdat 组成, 而fmp4用 moov + N * segment 组成, segment = moof + mdat。 其中moov 描述文件层次的metadata信息,moof 描述segment 层次metadata 信息。
4 ffmpeg parse mpd 文件
mpd 文件是xml 格式的,具由层次结构,主题里面包含子主题,从上到下,period -> adaptationset -> representation -> segmenttimeline 。由下图可是period 字段除了具有duration 信息,还包含子主题adaptationset 信息,以此类推...., 在parse mpd 文件时,也是从上往下parse.
假设当前有一stream, 具有不同码率video/audio,那么mpd 文件又是如何描述此信息的呢?
stream 用总字段duration , video/audio 用adaptationset 描述,不同码率的video/audio 用representation 描述,在往下是segmenttimeline 代表实际的data.。
5 dash data read
static int dash_read_packet(AVFormatContext *s, AVPacket *pkt)
{
....
while (!ff_check_interrupt(c->interrupt_callback) && !ret) {
ret = av_read_frame(cur->ctx, pkt);//如果当前segment 还有data,直接调用av_read_frame
if (ret >= 0) {
/* If we got a packet, return it */
cur->cur_timestamp = av_rescale(pkt->pts, (int64_t)cur->ctx->streams[0]->time_base.num * 90000, cur->ctx->streams[0]->time_base.den);
pkt->stream_index = cur->stream_index;
return 0;
}
if (cur->is_restart_needed) {//需要open 下一个segment ,调用reopen_demux_for_componet(), 重新建立url 连接
cur->cur_seg_offset = 0;
cur->init_sec_buf_read_offset = 0;
if (cur->input)
ff_format_io_close(cur->parent, &cur->input);
ret = reopen_demux_for_component(s, cur);
cur->is_restart_needed = 0;
}
}
return AVERROR_EOF;
}
6 dash seek
dash seek 相关code 如下,总结来说就是单一fragment, 和普通片源没差异。 如果是多个fragment 组成的,则需要寻找最靠近的那个fragment ,然后重新建立连接,open 这个fragment。此处有个疑问,假如fragment duration 很长,会不会seek 不精准?
static int dash_seek(AVFormatContext *s, struct representation *pls, int64_t seek_pos_msec, int flags, int dry_run)
{
// 如果时单一fragment , 直接call av_seek_frame()
if (pls->n_fragments == 1) {
pls->cur_timestamp = 0;
pls->cur_seg_offset = 0;
if (dry_run)
return 0;
ff_read_frame_flush(pls->ctx);
return av_seek_frame(pls->ctx, -1, seek_pos_msec * 1000, flags);
}
// 如果不是单一fragment ,则寻找靠近的frament
...... //此处删除如何寻找最靠近frament 的代码逻辑
ret = dry_run ? 0 : reopen_demux_for_component(s, pls); //找到最靠近的fragment后,重新建立url 连接
return ret;
}
7 总结
从宏观角度看,dash 协议,会将一个stream 切成很多不同码率的segment ,我们parse 出相应的segment ,然后建立网络连接,去读取相应的segment 就好了,当然牵涉如何具体实现,不会这么简单。回看HLS 协议,其实他们两做法很相似,切片,然后去请求这些切片数据。