CSDN博客

img lifu

多媒体流的 Internet 传输演示程序

发表于2001/8/17 13:35:00  2065人阅读

多媒体流的 Internet 传输演示程序

老赫

Manufactured for ZXX公司面试

前言:本次面试因为招聘方有不看应聘者DEMO程序的优良传统习惯,

而且是只做在UNIX下的程序的

而且在本地研发也不做Net Conf System,而是玩Database的.(我讨厌Databse,整天要和数据报表,键约束,工作流程打交道,和用OpenGL做DEMO相比起来,Database简直无聊之极...)

(哇,老赫,I服了U,去面试连对方的工作平台和工作内容都没搞清楚...)

最后结果是它不理会我,我不理会它(继续蛰伏在机关吧...ZZzzzzzz)

事实上这些东西的实质(无论是socket,RTP协议,线程同步设计,压缩传输设计)都是没有平台特性的

还是把这些天的学习心得写出来,给需要者参考

(比如做电子教室的,Net Conference的,相关内容毕业设计的小伙子)

(但是可能有bug哦,Matt Pietrek,Jeffery Richter的书也有bug,何况我老赫,请行家商榷指正,不要拍板砖)

如果对你的学习,工作有所帮助,那么最好不过了,我老赫是最没有小农的保守思想的。:)

(引用要写明出处哦,还可以提高Imagicstudio的Counter值,不要学CSDN)

 

 

 

开发目标 :

在低带宽条件下实现实时音视频传输。

RTP 协议的软件实现。

 

几个考虑的实现方法:

一,直接使用 Netmeeting SDK ,做出与 Netmeeting 完全类似的东西

二,基于某个已实现 IP 层以上的多媒体通讯协议栈 ( Open Source H.323) 来实作

三,直接基于 IP 协议来实作

本次采用第三个方法,但是只是个 DEMO ,时间和精力所限,不可能完整的实现 H.323 协议,所以只借鉴 H.323 协议的部分思想,当然,就不具备 H.323 的通用特性了。

 

技术难点 :

低带宽的多媒体数据流同步,连续视音频流估计是最大的难点。

 

因为条件限制,数据压缩只能使用软 CODEC 。实际应用中肯定会采用相应硬件来实现。

多线程采样编码解码中的参数选择如声音缓冲区的大小和个数需要实验确定。

数据通讯方面, Peer-to -Peer 方式一般采用面向连接的通讯,即 Stream Socket ,具有传输可靠,有序的特点,可以较好的保证通讯的质量,但是由于 TCP 的三路握手特性,其速度较慢,且不支持多路特性,经过跟踪分析 Netmeeting 可知, Netmeeting 只在连接握手时使用 TCP 协议,传送实时视音频并不采用 TCP 协议,而是采用速度更快的 UDP 协议。所以本次主要研究 UDP 的应用。

广播 (Broadcast) 或者多播 (multi-cast) 方式属于 UDP socket, 不保证数据的可靠,有序和无重复性,所以编程控制较 TCP Stream Socket 为复杂。 ( 注意 :ATM 网络虽然没有乱序的问题,有数据阻塞和突发的现象 ) 所以消除迟延和包丢失对通讯造成的影响是普遍共同的问题。

 

仿效 Netmeeting TCP/IP 连接可用于两端软件的握手和建立连接,实际传输用 UDP 不能象某些文章介绍的去用TCP/IP实现多媒体流传输,

实用的系统用UDP在internet上传输多媒体流信息才是正确的方法。

 

程序有两种工作方式:

点对点和多播方式 , 通过界面选择工作方式。

 

点对点 : 等待 TCP/IP 连接,一旦有连接请求,询问用户,如果接收连接,允许 UDP 端口双向收发数据。

多播:直接进行,接收方询问后接收。

 

非压缩情况 :

每秒传输的视频数据 (QCIF 为例 ):176*144*24/8=76032 (bytes)

每秒传输的 16Bit 单声道音频数据 :

SampleFrequence*SampleBits/8=16k bytes

压缩考虑方案 :

音频 : 考虑以下几种压缩 CODEC

G.711,G.729,G.723.1,

H.323 内容:

TrueSpeech,GSM

视频 CODEC:

H.261,H.263

H.323 内容 :Wavelets,Motion-JPEG, JPEG Intel  实现 ,

 

选择压缩率,压缩速度,损失程度几项指标的平衡。

最后选定 M-JPEG CODEC ,比率 1:24 ,无帧间相关的视频压缩,杜绝了 H.263 那样的花屏现象

GSM  比例 1:10  的音频方式 :16 bit,8 kHz  结果码率是  1.6 k bytes/sec, 效果可以接受

 

数据参数 :

Video

Internet:QCIF  176*144 ,5 FPS

Audio:

8KHz,16bit,Mono

 

控制协议 :

RTP/RCTP 理解其主要思想 ( 如何对乱序的信息包加顺序 ) 并编程实现

 

 

网络 API:

Windows Socket 2, 点对点或者多播方式 (multicast) ,方式可考虑异步或者同步方式。

多线程阻塞 ( 同步 ) 方式有更好的灵活性与效率,更能发挥 CPU 的计算能力。

 

安全性:本例不考虑

 

通讯协议的设计:

试编写如下程序,发送端不断发送编好序号的信息包。而接受端接收每隔 500 个包打印出包内序号,可以看到如下包序号 ,

511,1029,1618....

这说明 ,UDP 虽然能够提供多路和基本的校验和服务,但是它是乱序的,不可靠的。需要更多的控制信息才能可靠利用。

H.323 协议里, UDP 包的乱序重组是由实时传输协议 (RTP) 控制的。 RTP 给不可靠的 UDP 提供了流量测定,信息包顺序编号,时间戳和传输管理。

 

参考 H.323 里的 RTP 协议的原理,

在阅读 RFC 1889 后,我们知道 ,RTP 头有如下的格式 :(Win32 SDK RTP.H):

typedef struct _RTP_HEADER {

//--- NETWORK BYTE ORDER BEGIN ---//

    WORD  NumCSRC:4;                //  说明 CSRC 个数

    WORD  fExtHeader:1;                 //  如果为 1 ,表示固定头部结尾紧跟扩展的头部数据

    WORD  fPadding:1;                    // 填充  , 如果为 1, 表示 payload 尾有填充部分

    WORD  Version:2;                      //RTP 版本                

    WORD  PayloadType:7; //Payload 的类型,如,视频,音频,图象,数据等

    WORD  fMarker:1;  // 表示某种边界条件

//---- NETWORK BYTE ORDER END ----//

    WORD  SequenceNum; // 序列号,表示各包的顺序,用于重新组合为完整的信息

    DWORD Timestamp;  // 时间戳,用于表示该视频帧或语音流的时间标志

    DWORD SSRC;  // 标志信息来源 ID ,是随机数,同一会话的源有相同的 SSRC

} RTP_HEADER, *PRTP_HEADER;

其中,顺序号可作为数据包丢失率的统计分析和数据流的恢复。

时间戳可用于视音频流的同步。

可以构想这样的接收流程 :

双缓冲 ( 或多缓冲 ) 用于多媒体数据流的接收和播放。使用缓冲区的好处是可以把网络带来的延时抖动滤去,缓冲区大小待实验测定。

 

发送线程 :

音视频设置不同的 RTP 头的 Payload 类型。重点保证音频的连续。

视频,用 Vfw 或者 DirectShow 把外界图象采集到数据帧(或者数据流),用 Video Codec 压缩单帧图象为一个更小的缓冲区。按某个长度切割, RTP 打包,发送。

音频 , VAD (Voice Activity Detection) 原理进行采集,压缩, RTP 打包,发送。

 

接收线程按照收到的信息包按 Timestampe 分组, Sequence 排序,填充到缓冲区。

接受 Marker Bit 标志算接收一次数据完成。再经过有限的 N 组数据接收 (N 由实验测定 ) ,如果该组数据包完整,显示或者播放它,如果还缺数据,就简单的丢弃该缓冲区,将该缓冲区置为可接收数据状态。

 

以上方法可以解决 UDP 传输的分组丢失,重复,乱序对多媒体数据流带来的影响。

以下是参照 RTP 协议设计的数据包格式。

 

可靠的接收到完整的 LDU( 一帧图象,一段的声音 ) 后,把另一个空缓冲区换给接收者,让处于等待状态的播放线程播放接收满的缓冲区。

 

以下有两个设计方案 : 多线程和单线程

 

线程部分设计:

多线程设计的基本方法:

把程序所有的工作列出来,按一定的规则把他们划分为数个线程,再考虑同步,如果线程循环执行一次的时间差异太大,必须用同步对象 (Mutex,CriticalSection,Event,Semophere) 同步,一定不能用不断查询的办法,否则会吃掉大量的 CPU 资源。

按完成时间划分,无疑,数据的采集,数据的播放所需周期最长,压缩 + 发送就可以在数据的采集空闲时间完成,数据接收 + 解压在播放的空闲时间里完成。双缓冲区资源用两个互斥对象来保证单一线程访问 , 互斥对象由先运行的线程负责创建与销毁。

因此,线程做如下安排:

发送端:

数据采集线程,压缩 + 发送线程

采样线程先运行(互斥对象由它维护),随后压缩发送线程才运行。由两个互斥量同步。缓冲区数据的传递通过压缩发送线程的方法传递到压缩发送线程里。

接收端:

数据播放线程,接收 + 解压线程

接收方 , 解压接收线程先运行(互斥对象由它维护),播放线程被挂起直到接收解压完毕并把数据送到播放线程的空余缓冲区。

 

方案二是把采集和发送两线程合并的方案,

由于声音的采集是一个异步的过程,线程正好在录音的空闲时间完成压缩和发送。(要求是压缩时间和发送时间足够短,短于一次声音采集的时间,符合这个条件问题应该也不大)每发送一音频 LDU ,检测有无视频编码完毕,如果有,同时将视频发送出去。

 

同样,声音接收也是这样,线程在开始播放后迅速进行接收到的下一帧数据的解压等处理,处理完成后并等待播放结束后进行下一次播放。

这样的方案是存在延迟的:

并且延迟时间为一个声音 LDU 的播放时间 + 网络延迟

如果 LDU 足够大,那么缓冲效果会很好 ( 可以更多的等待延迟到达的语音数据而不出现语音断续 )

但是使用者将无法忍受带来的延迟,

所以应该根据网络质量来选择合适的 LDU 时间。

两个方法都将实现出来并比较效果的好坏而选择之。

视音频由同一端口发出,由 Payload type 区别,接收者把数据放到不同的缓冲区。

视音频接收完整后进行同步播出。同步的原则是时间戳差距最小的视频 LDU 和音频 LDU 同时播出。

其他 :

界面部分采用 Skin 类或者 ActiveSkin 控件,可以有比 Netmeeting 更好的外观。

见上图(VFW捕获窗口中人物为铁哥们鼻豆母雷,一个不编程却老骂人家软件臭的家伙)

 

最后,为了让接收端更好的使用该程序,免去安装软件的麻烦,应该至少将接收端放到 Web 页面里去执行。 Webmaster 使用程序进行视音频的传输,广播。 Intranet 用户只需要打开浏览器就可以收到内容。

这时选用 C++ Builder ActiveForm 把程序代码嵌入到网页会有事半功倍的效果。

 

附:队列缓冲区类的申明

//---------------------------------------------------------------------------

#ifndef queuebufH

#define queuebufH

//---------------------------------------------------------------------------

/*

队列缓冲区类,用于缓冲和接收 RTP

可检测逻辑单元是否已经完整到达

*/

#define PURE_DATA_SIZE 1024

#define TOTAL_PACKET_SIZE (PURE_DATA_SIZE+sizeof(RTP_HEADER))

 

class CQueueBuffer

{

private:

long m_lOnePureDataSize;

long m_lMemberCount;   //  队列可容纳的数据成员个数

long m_lBufferSize;    //  整个队列缓冲区的大小 : PURE_DATA_SIZE *  lQueueMemberCount

VOID *m_pQueueData;        //  队列缓冲区数据指针

bool *m_pFilledArray;

DWORD m_dwDataInPos;

 

 

public:

    int m_nTimeStamp;

    BOOL m_bEmpty;              //  队列缓冲区是否为全空

    int m_nFailedCount;         //  失败记数器

    long m_lActualMemberCount;

 

    virtual ~CQueueBuffer();

    CQueueBuffer(long nMemberCount,long lOnePureDataSize);

    bool PutPacketInPos(int nPos,LPVOID pPureData);

    bool CheckFixLengthComplete(void);

    bool CheckComplete(int nCount);

    void EmptyAll(void);

    void* GetBuffer(void)

    {

    return m_pQueueData;

    }

};

 

 

typedef struct MMStreamPacket{

DWORD dwValidSize;

char szReserve[32];

} STRAMPACKET_HEADER;

#endif

 

 

阅读全文
0 0

相关文章推荐

img
取 消
img