现在的位置: 首页 > 综合 > 正文

MMS流媒体下载原理

2014年01月11日 ⁄ 综合 ⁄ 共 11703字 ⁄ 字号 评论关闭
现在网上有很多可以点播的视频节目,大部分是MMS流媒体格式的,文件后缀一般是 WMV 或者ASF。虽然可以直接用 Windows Media Player 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 字节:固定为0xB00BFACE,就是英文单词 bOOb face(鲍勃的脸)   

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

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

    16-19 字节:对齐边界

    20-23 字节:命令包计数

    24-31 字节:双精度时间

    32-35 字节:对齐边界

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

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

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

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

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

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

    int CMMS::MakeCmd_0x01(BYTE data[])

    {

    LPCTSTR pPlayerVer="/x1C/x03NSPlayer/9.0.0.3372; ";
 
    int length=lstrlen(pPlayerVer)*2+8;
 
    int len8=(length+7)/8;

    LPBYTE pData=data;
 
    int size=0;

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

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

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

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

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

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

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

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

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

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

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

    *(DWORD*)(pData+size)=0x0004000B;
 
    size+=4;
 
    BYTE wPlayerVer[MAX_PATH*2];
 
    int len=2*MultiByteToWideChar(CP_ACP,0,pPlayerVer,-1,(WORD*wPlayerVer,MAX_PATH*2);
 
    CopyMemory(pData+size,wPlayerVer,len);
 
    size+=len;

    size=length+48;

    return(size);

    }

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

    struct sockaddr_in server;
 
    TCHAR DotAddr[2048]={0};
 
    TCHAR num[20]={0};
 
    int cbLen=sizeof(server);
 
    getsockname(m_CmdSock,(struct sockaddr*)&server,&cbLen);
 
    LPCTSTR pServAddr=inet_ntoa(server.sin_addr);
 
    lstrcpy(DotAddr,"////");
 
    lstrcat(DotAddr,pServAddr);
 
    lstrcat(DotAddr,"//TCP//");
 
    int port=ntohs(server.sin_port);
 
    sprintf(num,"%d",port);
 
    lstrcat(DotAddr,num);
 
    lstrcat(DotAddr,"//0");

    int tail=20-lstrlen(pServAddr)-lstrlen(num);
 
    for(int i=0;i<tail;++i)

    {

    lstrcat(DotAddr,"0");

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

    BYTE wMMS_Path[MAX_PATH*2];
 
    int len=2*MultiByteToWideChar(CP_ACP,0,pMMS_Path,-1,(WORD*wMMS_Path,MAX_PATH*2);
 
    CopyMemory(pData+size,wMMS_Path,len);
 
    size+=len;
 
    第五步发送0x15命令包,包体是12个固定的双字值,具体可参考所附代码。发送过程还和往常一样,这里主要强调一下接收过程,如何从这些二进制数据里提取需要的信息。首先注意到任何一个流媒体文件头的结尾,都是一个UINT64值,即 0x0101000000000000,可以利用这个特征先得到整个文件头:

    for(int packlen=0;;)
 
    {

    ZeroMemory(ret,2048);
  
    len=recv(m_CmdSock,(LPTSTR)ret,2048,0);
  
    int err=WSAGetLastError();
  
    if(SOCKET_ERROR==len)
  
    {
   
    AfxMessageBox("协商出错");
   
    return(0);
  
    }

    CopyMemory(m_Packet+packlen,ret,len);
  
    packlen+=len;

    pTag1=m_Packet+(packlen-8);
  
    tag1=*(UINT64*)pTag1;
  
    if(0x0101000000000000==tag1) break;
 
    }

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

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

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

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

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

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

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

    for(int i=0;i<packlen;i++)
 
    {

    if(0==state)
  
    {

    pTag1=m_Packet+i;
   
    tag1=*(UINT64*)pTag1;

    pTag2=m_Packet+(i+8);
   
    tag2=*(UINT64*)pTag2;

    if(0x11CF668E75B22630==tag1&&0x6CCE6200AA00D9A6==tag2)
   
    {
    
    state=1;
    
    offset=i;

    len=*(WORD*)(m_Packet+(i-2))-8;
    
    CopyMemory(m_Header+hdrlen,m_Packet+i,len);
    
    hdrlen+=len;
    
    i+=len;
   
    }
   
    else ++i;
  
    }
  
    else
  
    {
   
    len=*(WORD*)(m_Packet+(i+6))-8;
   
    CopyMemory(m_Header+hdrlen,m_Packet+(i+8),len);
   
    hdrlen+=len;
   
    i+=(len+8);
  
    }
 
    }
 
    m_HeaderLen=hdrlen; 

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

    LPBYTE pHeader=m_Packet+offset;
 
    for(i=0;i<m_HeaderLen;++i)
 
    {

    pTag1=pHeader+i;
  
    tag1=*(UINT64*)pTag1;

    pTag2=pHeader+(i+8);
  
    tag2=*(UINT64*)pTag2;

    if(0x11CFA9478CABDCA1==tag1&&0x6553200CC000E48E==tag2)
  
    {
   
    LPBYTE pTotalPacket=pHeader+(i+56);
   
    m_TotalPacket=*(DWORD*)pTotalPacket;

    LPBYTE pPacketLen=pHeader+(i+96);
   
    m_PacketLen=*(DWORD*)pPacketLen;

    CopyMemory(m_EndOfMMS,pHeader+(i+24),16);
   
    break;
  
    }
 
    }
 
    m_TotalData=m_TotalPacket*m_PacketLen+m_HeaderLen;
 
    注意到我们还同时保存了偏移24字节处的一些内容,共16字节。这和WMV格式有关,是在下载结束时,追加在文件末尾的标识信息,大可不必深究。
 
    第六步发送0x07命令包,这里有一点需要解释。包体的第六个双字,用它来指定本次下载的位置。如果是从头开始,可以定义为0或0xFFFFFFFF。如果是断点续传,指定包编号即可:

    int CMMS::MakeCmd_0x07(BYTE data[])
 
    {
 
    int length=24;
 
    int len8=(length+7)/8;

    LPBYTE pData=data;
 
    int size=0;

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

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

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

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

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

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

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

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

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

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

    *(DWORD*)(pData+size)=1;
 
    size+=4;
 
    *(DWORD*)(pData+size)=0x0001FFFF;
 
    size+=4;
 
    *(DWORD*)(pData+size)=0;
 
    size+=4;

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

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

    *(DWORD*)(pData+size)=m_PacketID;
 
    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=length+48;
  
    return(size);

    }

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

    BOOL bHasZero=1;
 
    for(int j=i;j<len;++j)
 
    {

    if(0x82==pData[j]&&0==pData[j+1]&&0==pData[j+2])
  
    {
   
    pTag=pData+(j+3);
   
    tag=*(WORD*)pTag;
   
    if((tag&0xFF00)>=0x5500&&(tag&0x00FF)>0)
   
    {
    
    if(LOBYTE(tag)&0x08||LOBYTE(tag)&0x10) bHasZero=0;
    
    break;
   
    }
  
    }
 
    }
 
    其中属性 0x48 比较特别,包长度不足时,系统会在后面自动添加一个递增的十六进制编号,不必再去添0,这就是上面代码 bHasZero=0 的含义。
 
    现在看一下完整的下载代码,找到包头后开始装配数据,附加的0字节个数在变量 ExtraZero 里。其中服务器间歇发送的 0x1B 同步信息,以及表示传输结束的 0x1E 通知消息也做了回应处理。关于流媒体文件的包长度,一般在1444字节到8000字节之间,这里把接收缓冲区大小设置为10240字节,应该足够用了:

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

    { 
 
    TCHAR tip[MAX_PATH]={0};
 
    BYTE data[8192]={0};

    DWORD PacketID,AckID;
 
    int offset,filen,pos;
 
    int size,len,err;

    end=min(end,(DWORD)m_TotalPacket);
 
    filen=m_HeaderLen+m_PacketLen*(end-start);

    LPBYTE pFile=new BYTE[(DWORD)m_TotalData];
 
    ZeroMemory(pFile,(DWORD)m_TotalData);

    pos=0;
 
    CopyMemory(pFile+pos,m_Header,m_HeaderLen);
 
    pos+=m_HeaderLen;
 
    offset=0;
 
    for(AckID=0,PacketID=start;PacketID<end;)
 
    {

    len=recv(m_CmdSock,(LPTSTR)(m_Packet+offset),10240-offset,0);
  
    err=WSAGetLastError();
  
    if(SOCKET_ERROR==len)
  
    {
   
    sprintf(tip,"接收数据出错 %d",err);
   
    AfxMessageBox(tip);
   
    break;
  
    }

    LPBYTE pData=m_Packet;
  
    len+=offset;
  
    for(int i=0;i<len;)
  
    {

    LPBYTE pTag=pData+i;
   
    UINT64 tag=*(UINT64*)pTag;
   
    if(offset>0)
   
    {
    
    tag=*(UINT64*)(pTag+offset);
    
    offset=0;
   
    }

    if(0xB00BFACE00000001==(tag&0xFFFFFFFF00FFFFFF))
   
    {
    
    LPBYTE pCmd=pData+(i+36);
    
    DWORD cmd=LOWORD(*(DWORD*)pCmd);
    
    if(0x21==cmd)
    
    {
     
    i+=*(DWORD*)(pTag+8)+16;
     
    continue;
    
    }
    
    else if(0x05==cmd)
    
    {
     
    i+=*(DWORD*)(pTag+8)+24;
     
    continue;
    
    }
    
    else if(0x1B==cmd)
    
    {
     
    if(m_pfnCallback&&m_pParam)
     
    {
      
    m_pfnCallback(m_pParam,"握手消息:",AckID++);
     
    }

    ZeroMemory(data,2048);
     
    size=MakeCmd_0x1B(data);
     
    len=SendData(data,size);
     
    break;
    
    }
    
    else if(0x1E==cmd)
    
    {
     
    PacketID=end;
     
    break;
    
    }
   
    }
   
    else
   
    {
    
    BOOL bHasZero=1;
    
    for(int j=i;j<len;++j)
    
    {
     
    if(0x82==pData[j]&&0==pData[j+1]&&0==pData[j+2])
     
    {
      
    pTag=pData+(j+3);
      
    tag=*(WORD*)pTag;
      
    if((tag&0xFF00)>=0x5500&&(tag&0x00FF)>0)
      
    {
       
    if(LOBYTE(tag)&0x08||LOBYTE(tag)&0x10) bHasZero=0;
       
    break;
      
    }
     
    }
    
    }

    if(j<len)
    
    {    
     
    WORD TrueLen,ExtraZero;
     
    LPBYTE pTrueLen;
 
    if(1==bHasZero)
     
    {
      
    pTrueLen=pData+(j+5);
      
    TrueLen=*(WORD*)pTrueLen;
     
    }
     
    else TrueLen=m_PacketLen;
     
    ExtraZero=m_PacketLen-TrueLen;

    if(j+TrueLen<=len)
 
    {
      
    CopyMemory(pFile+pos,pData+j,TrueLen);
      
    pos+=TrueLen;
  
    if(ExtraZero>0)
      
    {
       
    ZeroMemory(pFile+pos,ExtraZero);
       
    pos+=ExtraZero;
      
    }
 
    i+=TrueLen;
      
    ++PacketID;
      
    offset=0;

    if(m_pfnCallback&&m_pParam)
      
    {
       
    m_pfnCallback(m_pParam,"下载字节:",pos);
      
    }
     
    }
     
    else
     
    {
      
    offset=len-j;
      
    if(offset>0)
      
    {
       
    CopyMemory(m_Packet,pData+j,offset);
       
    break;
      
    }
      
    else
      
    {
       
    AfxMessageBox("offset<=0");
       
    return(0);
      
    }
     
    }
    
    }
    
    else
    
    {
     
    offset=len-i;
     
    if(offset>0)
     
    {
      
    CopyMemory(m_Packet,pData+i,offset);
      
    break;
     
    }
     
    else
     
    {
      
    AfxMessageBox("offset<=0");
      
    return(0);
     
    }
    
    }
   
    }
  
    }
 
    }
 
    m_pFile=pFile;
 
    m_FiLen=filen;

    return(pos);

    }

    因为是示例代码,所以只下载前 150 个数据包,起始点和终止点在 start 和 end 里,当end为-1时,表示全部下载。为简单起见,得到的文件就放在当前目录,名字是 zzhdr.wmv。大家可以根据实际情况自行调整。
 
    下载结束后,还要更新文件头中的一些信息,比较重要的是播放时间,否则用 Windows Media Player 9 播放时,滑块的位置就不正确了。可以用最后一帧的时间减去第一帧的时间,即得到总时间。帧时间从每个数据包偏移的第 6 或 7 或 8 字节开始,是一个双字。当包属性为 0x48 时,偏移在第 8 字节;当包属性为 0x40 或 0x41 时,偏移在第 7 字节;当包属性为 0x09时,偏移在第 6 字节。总时间是以毫秒为单位,所以还要乘上 10000,转化成系统要求的纳秒格式:

    int pos=m_HeaderLen;BYTE tag=m_pFile[pos+3];
 
    DWORD start,end;int offset;
 
    start=0;offset=5;if(tag&0x40) offset+=2;if(tag&0x10) offset+=2;if(tag&0x08) ++offset;start=*(WORD*)(m_pFile+(pos+offset));
 
    pos=m_HeaderLen+(m_End-m_Start-1)*m_PacketLen;tag=m_pFile[pos+3];

    end=0;offset=5;if(tag&0x40) offset+=2;if(tag&0x10) offset+=2;if(tag&0x08) ++offset;end=*(WORD*)(m_pFile+(pos+offset));
 
    UINT64 CurrTime=(end-start)*10000;

    最后还有几个不常用的命令包,比如:0x20 和 0x33。0x20命令包很少见,是服务器发给客户端的通知消息,一般在当前下载的媒体文件突然发生变化时出现。0x33 命令包用来选择下载视频流还是音频流。另外测试时可以使用一个开源项目作为基准,名字是:SDP Downloader 2.0。经比较,下载的数据和它是完全一样的,这也证明了上述通信过程的正确性,而且 ISA Server 2000 防火墙对下载也没有影响,也许是它没有封锁 1755 端口的原因吧。因为只是阐述原理,所以下载方式采用最简单的阻塞模型,可以根据需要修改成异步选择,事件选择,重叠I/O等伸缩性更好的非阻塞模型。以上分析是遵照 ASF 1.0 规范进行的,在 2.0 规范里,只是各部分对象的 GUID 发生了变化,只要在程序中修改相应的判断代码就可以了。笔者测试的几个MMS流媒体下载地址列在后面,这些资源时效性比较强,实在连接不上可以换其他的试试。在Google 里搜索 “mms wmv 下载”,能找到一些。附例程在 VS.NET 2002+WinXP 下调试通过。

    测试用例:

    mms://222.122.12.214/empas_wmv_high_060206/0025/002508.wmv

    mms://vod.tom.com/music/zhangzhicheng/baoyouwo.wmv

    mms://mmc.daumcast.net/mmc/1/500/0902418000208h.wmv

    参考文献

    MMS Streaming Protocol(Paul Wood)

抱歉!评论已关闭.