MMS流媒体格式的原理和实现步骤介绍-苏州安嘉

admin 发布于 2024-04-22 阅读(67)

现在网上有很多可以点播的视频节目,大部分是MMS流媒体格式的,文件后缀一般是 WMV 或者ASF。虽然可以直接用 Media 9 播放,但是经常会被一次又一次的“正在缓冲”打断,再好的影片也没耐心了。如果能像其他类型的资源,下载到硬盘上观看就方便多了。下面详细介绍具体原理和实现步骤。这里以WMV格式为例,其实ASF也是一样的,只不过它的图像品质更高些。

首先简要介绍一下客户端与服务器的完整通信过程。第一步,客户端发送0x01命令包,发动连接请求。服务器经检查无误后,返回一个新的0x01命令包作为应答;第二步,客户端发送0x18命令包,请求测试网络带宽情况。服务器收到后,发送3个随机数据包作为应答,总长度一般为2080字节;第三步,客户端发送0x02命令包,告知自己的IP地址和端口号。服务器确认后,返回新的0x02命令包作为应答,其中包含了一串英文来表示接受,翻译过来就是“上帝的漏斗”;第四步,客户端发送0x05命令包,请求所需文件的名字和路径。服务器收到后,返回0x06命令包作为应答,告知一些流媒体的属性,比如:录制类型,最高比特率等;第五步,客户端发送0x15命令包,请求文件头。服务器会返回0x11命令包,其中包含了文件头的内容,可以从中解析出头部长度,总包数,包长度等信息,这一步最复杂,数据可能会被拆分成多个部分发送过来。现在双方的联系就算正式建立了,可以开始下载真实数据。这时客户端发送0x07命令包请求数据,可以全部下载,也可以指定从哪个数据包开始下载,为将来设计断点续传提供了方便。服务器收到后,返回0x21和0x05命令包作为应答,然后把数据流打碎,一截一截地发送过来,每隔一段时间还会发送0x1B命令包作为同步消息,客户端也回送0x1B命令包作为应答。因为每次传过来的数据量长度是不确定的,所以要通过判断报头标记,组装成完整的数据包后,再写入文件就可以了。

整个通信过程看上去并不是很困难,不过微软并没有公开MMS规范,所以只能通过在网上搜索破解文档,就难免有一些未知含义的字节,但也无关大碍。现在具体描述每一步的实现方式。第一步发送0x01命令包,包头的结构如下所示:

0-3 字节:固定为1

4-7 字节:固定为,就是英文单词 bOOb face(鲍勃的脸)

8-11 字节:协议类型后面数据的长度

12-15 字节:协议类型,就是MMS和空格的ASCII码

16-19 字节:对齐边界

20-23 字节:命令包计数

24-31 字节:双精度时间

32-35 字节:对齐边界

36-39 字节:本命令代号,固定为,后两个字节的3表示传输方向是从客户端到服务器。

到这里包头的定义就结束了,以后其他命令包的包头也是基本相同的,不同的只是包体和附加数据。下面来看0x01命令包的包体数据:

40-43 字节:MMS协议标志,此处为

44-47 字节:固定为,意义未知

48-结束:以 格式编码的播放器版本

现在看一下完整的命令包组装代码:

int CMMS::(BYTE data[])

="/x1C//9.0.0.3372; ";

int =()*2+8;

int len8=(+7)/8;

pData=data;

int size=0;

*(DWORD*)(pData+size)=1;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(DWORD*)(pData+size)=+32;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(DWORD*)(pData+size)=len8+4;

size+=4;

*(DWORD*)(pData+size)=++;

size+=4;

*(DWORD*)(pData+size)=0;

size+=4;

*(DWORD*)(pData+size)=0;

size+=4;

*(DWORD*)(pData+size)=len8+2;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

BYTE [*2];

int len=2*(,0,,-1,(WORD*,*2);

(pData+size,,len);

size+=len;

size=+48;

(size);

第二步发送0x18命令包很简单,没有附加数据,包体是两个双字,固定为 值和 值,可参考所附例程。第三步发送0x02命令包,需要构造一个由IP地址与端口号组成的字符串,一般使用 得到所需的内容。另外还要在末尾补零以达到边界对齐。若嫌麻烦,这里也可以随便写,比如IP地址定义为:192.168.66.88 ,端口号定义为:7799。笔者试验过,对后面的通信过程没有影响。下面代码是按常规方式得到地址和端口:

;

TCHAR [2048]={0};

TCHAR num[20]={0};

int cbLen=();

(,( *)&,&cbLen);

=(.);

(,"");

(,);

(,"//TCP//");

int port=ntohs(.);

(num,"%d",port);

(,num);

(,"//0");

int tail=20-()-(num);

for(int i=0;i

(,"0");

第四步发送0x05命令包,附加数据是编码的文件全路径名,其中包含要下载的媒体文件名。包体是四个固定值,分别为:1,,0,0,文件路径的编码变换如下所示:

BYTE [*2];

int len=2*(,0,,-1,(WORD*,*2);

(pData+size,,len);

size+=len;

第五步发送0x15命令包,包体是12个固定的双字值,具体可参考所附代码。发送过程还和往常一样,这里主要强调一下接收过程,如何从这些二进制数据里提取需要的信息。首先注意到任何一个流媒体文件头的结尾,都是一个值,即 ,可以利用这个特征先得到整个文件头:

for(int =0;;)

(ret,2048);

len=recv(,()ret,2048,0);

int err=();

if(==len)

("协商出错");

(0);

(+,ret,len);

+=len;

pTag1=+(-8);

tag1=*(*)pTag1;

if(==tag1) break;

有了上面的数据,现在可以开始分析了。前56个字节是服务器返回的提示信息,后面会跟多个数据包,这就是分批发送过来的文件头。每个包的结构如下:

0-3 字节:包计数,从0开始

4-5 字节:包属性,前一字节为2或3,后一字节为0,4,8,12四个值之一

6-7 字节:本包长度,包含这前面的8个字节

8-包尾:本次的部分文件头数据

那么如何定位第一个数据包呢?用 查看一下会发现,每个流媒体文件头都是以一个独特的GUID开始的,把它拆成两个值即是: 和。

那么组装文件头的代码就很容易写了:

for(int i=0;i

if(0==state)

pTag1=+i;

tag1=*(*)pTag1;

pTag2=+(i+8);

tag2=*(*)pTag2;

if(==tag1&&==tag2)

state=1;

=i;

len=*(WORD*)(+(i-2))-8;

(+,+i,len);

+=len;

i+=len;

else ++i;

else

len=*(WORD*)(+(i+6))-8;

(+,+(i+8),len);

+=len;

i+=(len+8);

=;

现在有了正确的文件头,下一步的工作就是得到包总数和包长度。它们也是在一个GUID后面,即: 和 。包总数从此处向后偏移56个字节,包长度从此处向后偏移96字节,得到这两个关键值的代码如下所示:

=+;

for(i=0;i

pTag1=+i;

tag1=*(*)pTag1;

pTag2=+(i+8);

tag2=*(*)pTag2;

if(==tag1&&==tag2)

=+(i+56);

=*(DWORD*);

=+(i+96);

=*(DWORD*);

(,+(i+24),16);

break;

=*+;

注意到我们还同时保存了偏移24字节处的一些内容,共16字节。这和WMV格式有关,是在下载结束时,追加在文件末尾的标识信息,大可不必深究。

第六步发送0x07命令包,这里有一点需要解释。包体的第六个双字,用它来指定本次下载的位置。如果是从头开始,可以定义为0或。如果是断点续传,指定包编号即可:

int CMMS::(BYTE data[])

int =24;

int len8=(+7)/8;

pData=data;

int size=0;

*(DWORD*)(pData+size)=1;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(DWORD*)(pData+size)=+32;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(DWORD*)(pData+size)=len8+4;

size+=4;

*(DWORD*)(pData+size)=++;

size+=4;

*(DWORD*)(pData+size)=0;

size+=4;

*(DWORD*)(pData+size)=0;

size+=4;

*(DWORD*)(pData+size)=len8+2;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(DWORD*)(pData+size)=1;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(DWORD*)(pData+size)=0;

size+=4;

*(DWORD*)(pData+size)=0;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(DWORD*)(pData+size)=;

size+=4;

*(BYTE*)(pData+size)=0xFF;

size+=1;

*(BYTE*)(pData+size)=0xFF;

size+=1;

*(BYTE*)(pData+size)=0xFF;

size+=1;

*(BYTE*)(pData+size)=0;

size+=1;

*(DWORD*)(pData+size)=4;

size+=4;

size=+48;

(size);

到此为止,通信回合结束,客户端与服务器连接正常,可以下载真实的媒体数据了。现在的每个数据包都是以 82 00 00 这三个标识字节开始,后面有两个字节是包属性,接下来的两个字节是本次包长度,再后面才是数据。若这次的包长度小于文件规定长度,必须用0补全。对于包属性,一般是 0x40,0x41,0x48,0x09 这四个值之一,后缀有可能是 0x55,0x59,0x5D 这三个值之一。不过为了增加广普适用性,可以不必定死在这几个值,所以,判断包头方式改写成这样:

BOOL =1;

for(int j=i;j

if(0x82==pData[j]&&0==pData[j+1]&&0==pData[j+2])

pTag=pData+(j+3);

tag=*(WORD*)pTag;

if((tag&)>=&&(tag&)>0)

if((tag)&0x08||(tag)&0x10) =0;

break;

其中属性 0x48 比较特别,包长度不足时,系统会在后面自动添加一个递增的十六进制编号,不必再去添0,这就是上面代码 =0 的含义。

现在看一下完整的下载代码,找到包头后开始装配数据,附加的0字节个数在变量 里。其中服务器间歇发送的 0x1B 同步信息,以及表示传输结束的 0x1E 通知消息也做了回应处理。关于流媒体文件的包长度,一般在1444字节到8000字节之间,这里把接收缓冲区大小设置为10240字节,应该足够用了:

int CMMS::(DWORD start,DWORD& end)

TCHAR tip[]={0};

BYTE data[8192]={0};

DWORD ,AckID;

int ,filen,pos;

int size,len,err;

end=min(end,(DWORD));

filen=+*(end-start);

pFile=new BYTE[(DWORD)];

(pFile,(DWORD));

pos=0;

(pFile+pos,,);

pos+=;

=0;

for(AckID=0,=start;

len=recv(,()(+),10240-,0);

err=();

if(==len)

(tip,"接收数据出错 %d",err);

(tip);

break;

pData=;

len+=;

for(int i=0;i

pTag=pData+i;

tag=*(*)pTag;

if(>0)

tag=*(*)(pTag+);

=0;

if(==(tag&))

pCmd=pData+(i+36);

DWORD cmd=(*(DWORD*)pCmd);

if(0x21==cmd)

i+=*(DWORD*)(pTag+8)+16;

;

else if(0x05==cmd)

i+=*(DWORD*)(pTag+8)+24;

;

else if(0x1B==cmd)

if(&&)

(,"握手消息:",AckID++);

(data,2048);

size=(data);

len=(data,size);

break;

else if(0x1E==cmd)

=end;

break;

else

BOOL =1;

for(int j=i;j

if(0x82==pData[j]&&0==pData[j+1]&&0==pData[j+2])

pTag=pData+(j+3);

tag=*(WORD*)pTag;

if((tag&)>=&&(tag&)>0)

if((tag)&0x08||(tag)&0x10) =0;

break;

if(j

WORD ,;

;

if(1==)

=pData+(j+5);

=*(WORD*);

else =;

=-;

if(j+)

(pFile+pos,);

pos+=;

i+=;

++;

=0;

if(&&)

(,"下载字节:",pos);

else

=len-j;

if(>0)

(,pData+j,);

break;

else

("

标签:  字节 发送 命令 长度 附加 

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。