海康RTSP取流URL格式
一、预览取流
设备预览取流的RTSP URL有新老版本,2012年之前的设备(比如V2.0版本的Netra设备)支持老的取流格式,之后的设备新老取流格式都支持。
老版本
URL规定:
rtsp://username:password@//ch/
注:VLC可以支持解析URL里的用户名密码,实际发给设备的RTSP请求不支持带用户名密码。
举例说明:
DS-9016HF-ST的IP通道01主码流:rtsp://admin:12345@172.6.22.106:554/h264/ch33/main/av_stream
DS-9016HF-ST的模拟通道01子码流:rtsp://admin:12345@172.6.22.106:554/h264/ch1/sub/av_stream
DS-9016HF-ST的零通道主码流(零通道无子码流):rtsp://admin:12345@172.6.22.106:554/h264/ch0/main/av_stream
DS-2DF7274-A的第三码流: rtsp://admin:12345@172.6.10.11:554/h264/ch1/stream3/av_stream
URL规定:
rtsp://username:password@
:/Streaming/Channels/(?parm1=value1&parm2-=value2…)注:VLC可以支持解析URL里的用户名密码,实际发给设备的RTSP请求不支持带用户名密码。
详细描述:
举例说明:
DS-9632N-ST的IP通道01主码流:rtsp://admin:12345@172.6.22.234:554/Streaming/Channels/101?transportmode=unicast
DS-9016HF-ST的IP通道01主码流:rtsp://admin:12345@172.6.22.106:554/Streaming/Channels/1701?transportmode=unicast
DS-9016HF-ST的模拟通道01子码流:rtsp://admin:12345@172.6.22.106:554/Streaming/Channels/102?transportmode=unicast
(单播):rtsp://admin:12345@172.6.22.106:554/Streaming/Channels/102?transportmode=multicast
(多播):rtsp://admin:12345@172.6.22.106:554/Streaming/Channels/102 (?后面可省略,默认单播)
DS-9016HF-ST的零通道主码流(零通道无子码流):rtsp://admin:12345@172.6.22.106:554/Streaming/Channels/001
DS-2DF7274-A的第三码流:rtsp://admin:12345@172.6.10.11:554/Streaming/Channels/103
注:前面老URL,NVR(>=64路的除外)的IP通道从33开始;新URL,通道号全部按顺序从1开始。
URL规定:
rtsp://username:password@
:/Streaming/tracks/(?parm1=value1&parm2-=value2…)注:VLC可以支持解析URL里的用户名密码,实际发给设备的RTSP请求不支持带用户名密码。
举例说明:
DS-9016HF-ST的模拟通道01:rtsp://admin:12345@172.6.22.106:554/Streaming/tracks/101?starttime=20120802t063812z&endtime=20120802t064816z
DS-9016HF-ST的IP通道01:rtsp://admin:12345@172.6.22.106:554/Streaming/tracks/1701?starttime=20131013t093812z&endtime=20131013t104816z
表示以单播形式回放指定设备的通道中的录像文件,时间范围是starttime到endtime,
其中starttime和endtime的格式要符合ISO 8601。具体格式是:
YYYYMMDD”T”HHmmSS.fraction”Z” ,Y是年,M是月,D是日,T是时间分格符,H是小时,M是分,S是秒,Z是可选的、表示Zulu (GMT) 时间。
VLC播放示例:
媒体--》打开网络串流--》网络:
rtsp://username:password@192.168.1.17:554/MPEG-4/ch1/main/av_stream
Linux下编译eXosip2库以及测试
原文作者:这个名字不知道有没有人用啊
原文链接:https://blog.csdn.net/weixin_43272766/article/details/89899257
环境:
Ubuntu18.04 + libosip2-5.1.0 + libexosip2-5.1.0 + c-ares-1.15.0
下载
https://c-ares.haxx.se/ 好像不使用也可以
http://ftp.twaren.net/Unix/NonGNU//osip/
http://ftp.yzu.edu.tw/nongnu/exosip/
依次解压编译(注意顺序,exosip要在最后编译)
tarxvf 对应压缩包名
cd 解压出来的文件夹
./configuremake
sudo make install
测试
#include #include#include#include
using namespacestd;intmain()
{
eXosip_t*sip =eXosip_malloc();if(eXosip_init(sip) ==OSIP_SUCCESS)
{
cout<< "eXosip init ok" <
}else{
cout<< "exosip init fail" <
}int ret = eXosip_listen_addr(sip, IPPROTO_UDP, NULL, 0, AF_INET, 0);if(ret ==OSIP_SUCCESS)
{
cout<< "exosiop listen addr success" <
}elsecout<< "listen addr fail, ret:" << ret <
eXosip_quit(sip);
cout<< "test" <
}
编译运行
g++ test.cpp -losip2 -leXosip2
./a.out
uac.cpp
#include #include#include#include#include#include#include
int main(int argc,char *argv[])
{
struct eXosip_t*excontext;
eXosip_event_t*je;
osip_message_t*reg=NULL;
osip_message_t*invite=NULL;
osip_message_t*ack=NULL;
osip_message_t*info=NULL;
osip_message_t*message=NULL;intcall_id,dialog_id;inti,flag;int flag1=1;intiReturnCode;char identity[30]="sip:140@127.0.0.1"; //UAC1,端口是15060
char registar[30]="sip:133@127.0.0.1:15061"; //UAS,端口是15061
char source_call[30]="sip:140@127.0.0.1";char dest_call[30]="sip:133@127.0.0.1:15061";//identify和register这一组地址是和source和destination地址相同的//在这个例子中,uac和uas通信,则source就是自己的地址,而目的地址就是uac1的地址
charcommand;char tmp[4096];
std::cout<< "r 向服务器注册" <<:endl>
std::cout<< "c 取消注册" <<:endl>
std::cout<< "i 发起呼叫请求" <<:endl>
std::cout<< "h 挂断" <<:endl>
std::cout<< "q 推出程序" <<:endl>
std::cout<< "s 执行方法INFO" <<:endl>
std::cout<< "m 执行方法MESSAGE" <<:endl>
excontext =eXosip_malloc();
iReturnCode=eXosip_init(excontext);if (iReturnCode != 0)
{
printf("Can't initialize eXosip!\n");
return-1;
}else{
printf("eXosip_init successfully!\n");
}//绑定uac自己的端口15060,并进行端口监听
iReturnCode = eXosip_listen_addr(excontext, IPPROTO_UDP, NULL, 15060, AF_INET, 0);if(iReturnCode!=0)
{
eXosip_quit(excontext);
fprintf(stderr,"Couldn't initialize transport layer!\n");
return-1;
}while(true)
{//输入命令
std::cout << "Please input the command:" <<:endl>
std::cin>>command;
switch(command)
{case 'r':
std::cout<< "This modal is not completed!" <<:endl>
break;case 'i'://INVITE,发起呼叫请求
i=eXosip_call_build_initial_invite(excontext,&invite,dest_call,source_call,NULL,"This is a call for conversation");if(i!=0)
{
std::cout<< "Initial INVITE failed!" <<:endl>
break;
}//符合SDP格式,其中属性a是自定义格式,也就是说可以存放自己的信息,//但是只能有两列,比如帐户信息//但是经过测试,格式vot必不可少,原因未知,估计是协议栈在传输时需要检查的
snprintf(tmp,4096,"v=0\r\n"
"o=anonymous 0 0 IN IP4 0.0.0.0\r\n"
"t=1 10\r\n"
"a=username:rainfish\r\n"
"a=password:123\r\n");
osip_message_set_body(invite,tmp,strlen(tmp));
osip_message_set_content_type(invite,"application/sdp");
eXosip_lock(excontext);
i=eXosip_call_send_initial_invite(excontext,invite); //invite SIP INVITE message to send
eXosip_unlock(excontext);//发送了INVITE消息,等待应答
flag1=1;while(flag1)
{
je=eXosip_event_wait(excontext,0,200); //Wait for an eXosip event//(超时时间秒,超时时间毫秒)
if(je==NULL)
{
printf("No response or the time is over!\n");
break;
}
switch(je->type) //可能会到来的事件类型
{case EXOSIP_CALL_INVITE: //收到一个INVITE请求
printf("a new invite received!\n");
break;case EXOSIP_CALL_PROCEEDING: //收到100 trying消息,表示请求正在处理中
printf("proceeding!\n");
break;case EXOSIP_CALL_RINGING: //收到180 Ringing应答,表示接收到INVITE请求的UA
printf("ringing!\n");
printf("call_id is %d,dialog_id is %d \n",je->cid,je->did);
break;case EXOSIP_CALL_ANSWERED: //收到200 OK,表示请求已经被成功接受,用户应答
printf("ok!connected!\n");
call_id=je->cid;
dialog_id=je->did;
printf("call_id is %d,dialog_id is %d \n",je->cid,je->did);//回送ack应答消息
eXosip_call_build_ack(excontext,je->did,&ack);
eXosip_call_send_ack(excontext,je->did,ack);
flag1=0; //推出While循环
break;case EXOSIP_CALL_CLOSED: //a BYE was received for this call
printf("the other sid closed!\n");
break;case EXOSIP_CALL_ACK: //ACK received for 200ok to INVITE
printf("ACK received!\n");
break;
default://收到其他应答
printf("other response!\n");
break;
}
eXosip_event_free(je);//Free ressource in an eXosip event
}
break;case 'h': //挂断
printf("Holded!\n");
eXosip_lock(excontext);
eXosip_call_terminate(excontext,call_id,dialog_id);
eXosip_unlock(excontext);
break;case 'c':
printf("This modal is not commpleted!\n");
break;case 's': //传输INFO方法
eXosip_call_build_info(excontext,dialog_id,&info);
snprintf(tmp,4096,"\nThis is a sip message(Method:INFO)");
osip_message_set_body(info,tmp,strlen(tmp));//格式可以任意设定,text/plain代表文本信息;
osip_message_set_content_type(info,"text/plain");
eXosip_call_send_request(excontext,dialog_id,info);
break;case 'm'://传输MESSAGE方法,也就是即时消息,和INFO方法相比,我认为主要区别是://MESSAGE不用建立连接,直接传输信息,而INFO消息必须在建立INVITE的基础上传输
printf("the method : MESSAGE\n");
eXosip_message_build_request(excontext,&message,"MESSAGE",dest_call,source_call,NULL);//内容,方法, to ,from ,route
snprintf(tmp,4096,"This is a sip message(Method:MESSAGE)");
osip_message_set_body(message,tmp,strlen(tmp));//假设格式是xml
osip_message_set_content_type(message,"text/xml");
eXosip_message_send_request(excontext,message);
break;case 'q':
eXosip_quit(excontext);
printf("Exit the setup!\n");
flag=0;
break;
}
}
return(0);
}
View Code
uas.cpp
# include # include# include# include# include#include#include
//# include
int main (int argc, char *argv[])
{
struct eXosip_t*excontext;
eXosip_event_t*je =NULL;
osip_message_t*ack =NULL;
osip_message_t*invite =NULL;
osip_message_t*answer =NULL;
sdp_message_t*remote_sdp =NULL;intcall_id, dialog_id;inti,j,iReturnCode;int id;char sour_call[30] = "sip:140@127.0.0.1";char dest_call[30] = "sip:133@127.0.0.1:15060";//client ip
charcommand;char tmp[4096];char localip[128];int pos = 0;//初始化sip
excontext =eXosip_malloc();
iReturnCode=eXosip_init(excontext);if (iReturnCode != 0)
{
printf ("Can't initialize eXosip!\n");
return-1;
}else{
printf ("eXosip_init successfully!\n");
}
iReturnCode= eXosip_listen_addr(excontext,IPPROTO_UDP, NULL, 15061, AF_INET, 0);if (iReturnCode != 0)
{
eXosip_quit (excontext);
fprintf (stderr,"eXosip_listen_addr error!\nCouldn't initialize transport layer!\n");
}for(;;)
{//侦听是否有消息到来
je = eXosip_event_wait (excontext,0,50);//协议栈带有此语句,具体作用未知
eXosip_lock (excontext);
eXosip_default_action (excontext,je);//eXosip_automatic_refresh (excontext);
eXosip_unlock (excontext);if (je == NULL)//没有接收到消息
continue;//printf ("the cid is %s, did is %s/n", je->did, je->cid);
switch (je->type)
{case EXOSIP_MESSAGE_NEW://新的消息到来
printf ("EXOSIP_MESSAGE_NEW!\n");if (MSG_IS_MESSAGE (je->request))//如果接受到的消息类型是MESSAGE
{
{
osip_body_t*body;
osip_message_get_body (je->request, 0, &body);
printf ("I get the msg is: %s\n", body->body);//printf ("the cid is %s, did is %s/n", je->did, je->cid);
}//按照规则,需要回复OK信息
eXosip_message_build_answer (excontext,je->tid, 200,&answer);
eXosip_message_send_answer (excontext,je->tid, 200,answer);
}
break;caseEXOSIP_CALL_INVITE://得到接收到消息的具体信息
printf ("Received a INVITE msg from %s:%s, UserName is %s, password is %s\n",je->request->req_uri->host,
je->request->req_uri->port, je->request->req_uri->username, je->request->req_uri->password);//得到消息体,认为该消息就是SDP格式.
remote_sdp = eXosip_get_remote_sdp (excontext,je->did);
call_id= je->cid;
dialog_id= je->did;
eXosip_lock (excontext);
eXosip_call_send_answer (excontext,je->tid, 180, NULL);
i= eXosip_call_build_answer (excontext,je->tid, 200, &answer);if (i != 0)
{
printf ("This request msg is invalid!Cann't response!\n");
eXosip_call_send_answer (excontext,je->tid, 400, NULL);
}else{
snprintf (tmp,4096,"v=0\r\n"
"o=anonymous 0 0 IN IP4 0.0.0.0\r\n"
"t=1 10\r\n"
"a=username:rainfish\r\n"
"a=password:123\r\n");//设置回复的SDP消息体,下一步计划分析消息体//没有分析消息体,直接回复原来的消息,这一块做的不好。
osip_message_set_body (answer, tmp, strlen(tmp));
osip_message_set_content_type (answer,"application/sdp");
eXosip_call_send_answer (excontext,je->tid, 200, answer);
printf ("send 200 over!\n");
}
eXosip_unlock (excontext);//显示出在sdp消息体中的attribute 的内容,里面计划存放我们的信息
printf ("the INFO is :\n");while (!osip_list_eol ( &(remote_sdp->a_attributes), pos))
{
sdp_attribute_t*at;
at= (sdp_attribute_t *) osip_list_get ( &remote_sdp->a_attributes, pos);
printf ("%s : %s\n", at->a_att_field, at->a_att_value);//这里解释了为什么在SDP消息体中属性a里面存放必须是两列
pos++;
}
break;caseEXOSIP_CALL_ACK:
printf ("ACK recieved!\n");//printf ("the cid is %s, did is %s/n", je->did, je->cid);
break;caseEXOSIP_CALL_CLOSED:
printf ("the remote hold the session!\n");//eXosip_call_build_ack(dialog_id, &ack);//eXosip_call_send_ack(dialog_id, ack);
i = eXosip_call_build_answer (excontext,je->tid, 200, &answer);if (i != 0)
{
printf ("This request msg is invalid!Cann't response!\n");
eXosip_call_send_answer (excontext,je->tid, 400, NULL);
}else{
eXosip_call_send_answer (excontext,je->tid, 200, answer);
printf ("bye send 200 over!\n");
}
break;case EXOSIP_CALL_MESSAGE_NEW://至于该类型和EXOSIP_MESSAGE_NEW的区别,源代码这么解释的
/*// request related events within calls (except INVITE)
EXOSIP_CALL_MESSAGE_NEW, < announce new incoming request.
// response received for request outside calls
EXOSIP_MESSAGE_NEW, < announce new incoming request.
我也不是很明白,理解是:EXOSIP_CALL_MESSAGE_NEW是一个呼叫中的新的消息到来,比如ring trying都算,所以在接受到后必须判断
该消息类型,EXOSIP_MESSAGE_NEW而是表示不是呼叫内的消息到来。
该解释有不妥地方,仅供参考。*/printf("EXOSIP_CALL_MESSAGE_NEW\n");if (MSG_IS_INFO(je->request) ) //如果传输的是INFO方法
{
eXosip_lock (excontext);
i= eXosip_call_build_answer (excontext,je->tid, 200, &answer);if (i == 0)
{
eXosip_call_send_answer (excontext,je->tid, 200, answer);
}
eXosip_unlock (excontext);
{
osip_body_t*body;
osip_message_get_body (je->request, 0, &body);
printf ("the body is %s\n", body->body);
}
}
break;
default:
printf ("Could not parse the msg!\n");
}
}
}
View Code
编译并运行
g++ uac.cpp -o uac -losip2 -leXosip2 -lpthread -losipparser2
g++ uas.cpp -o uas -losip2 -leXosip2 -lpthread -losipparser2
./uas
./uac
exosip对接海康摄像头
#include #include#include#include#include#include#include
static void RegisterSuccess(struct eXosip_t * peCtx,eXosip_event_t *je)
{int iReturnCode = 0;
osip_message_t* pSRegister =NULL;
iReturnCode= eXosip_message_build_answer (peCtx,je->tid,200,&pSRegister);if ( iReturnCode == 0 && pSRegister !=NULL )
{
eXosip_lock(peCtx);
eXosip_message_send_answer (peCtx,je->tid,200,pSRegister);
eXosip_unlock(peCtx);//osip_message_free(pSRegister);
}
}void RegisterFailed(struct eXosip_t * peCtx,eXosip_event_t *je) {int iReturnCode = 0;
osip_message_t* pSRegister =NULL;
iReturnCode= eXosip_message_build_answer (peCtx,je->tid,401,&pSRegister);if ( iReturnCode == 0 && pSRegister !=NULL )
{
eXosip_lock(peCtx);
eXosip_message_send_answer (peCtx,je->tid,401,pSRegister);
eXosip_unlock(peCtx);
}
}int main(int argc,char *argv[])
{struct eXosip_t *excontext;
eXosip_event_t*je;
osip_message_t*reg=NULL;
osip_message_t*invite=NULL;
osip_message_t*ack=NULL;
osip_message_t*info=NULL;
osip_message_t*message=NULL;intcall_id,dialog_id;inti,flag;int flag1=1;intiReturnCode;intregisterOk;char *p;char identity[30]="sip:140@127.0.0.1"; //UAC1,端口是15060
char registar[30]="sip:133@127.0.0.1:15061"; //UAS,端口是15061
char source_call[30]="sip:140@127.0.0.1";char dest_call[30]="sip:133@127.0.0.1:15061";//identify和register这一组地址是和source和destination地址相同的//在这个例子中,uac和uas通信,则source就是自己的地址,而目的地址就是uac1的地址
charcommand;char tmp[4096];
std::cout<< "r 向服务器注册" <<:endl>
std::cout<< "c 取消注册" <<:endl>
std::cout<< "i 发起呼叫请求" <<:endl>
std::cout<< "h 挂断" <<:endl>
std::cout<< "q 推出程序" <<:endl>
std::cout<< "s 执行方法INFO" <<:endl>
std::cout<< "m 执行方法MESSAGE" <<:endl>
excontext =eXosip_malloc();
iReturnCode=eXosip_init(excontext);if (iReturnCode != 0)
{
printf("Can't initialize eXosip!\n");return -1;
}else{
printf("eXosip_init successfully!\n");
}//绑定uac自己的端口15060,并进行端口监听
iReturnCode = eXosip_listen_addr(excontext, IPPROTO_UDP, NULL, 5060, AF_INET, 0);if(iReturnCode!=0)
{
eXosip_quit(excontext);
fprintf(stderr,"Couldn't initialize transport layer!\n");return -1;
}while(true)
{
eXosip_event_t*je =NULL;
je= eXosip_event_wait (excontext, 0, 4);if (je ==NULL) {
std::cout<< "event is null" <<:endl>
osip_usleep(100000*50);continue;
}switch (je->type) {caseEXOSIP_MESSAGE_NEW:
{//printf("new msg method:%s\n", je->request->sip_method);
if(MSG_IS_REGISTER(je->request)) {
std::cout<< "msg body:" <<:endl>
registerOk= 1;
}else if(MSG_IS_MESSAGE(je->request)){
osip_body_t*body =NULL;
osip_message_get_body(je->request, 0, &body);if(body !=NULL) {
p= strstr(body->body, "Keepalive");if(p !=NULL) {
registerOk= 1;
std::cout<< "msg body:" <<:endl>
std::cout<< body->body <<:endl>
}else{
std::cout<< "msg body:" <<:endl>
std::cout<< body->body <<:endl>
}
}else{
std::cout<< "get body failed" <<:endl>
}
}else if(strncmp(je->request->sip_method, "BYE", 4) != 0){
std::cout<< "unsupport new msg method :" << je->request->sip_method <<:endl>
}
RegisterSuccess(excontext, je);
}break;caseEXOSIP_MESSAGE_ANSWERED:
{
printf("answered method:%s\n", je->request->sip_method);
RegisterSuccess(excontext, je);
}break;caseEXOSIP_CALL_ANSWERED:
{
osip_message_t*ack=NULL;
call_id= je->cid;
dialog_id= je->did;
printf("call answered method:%s, call_id:%d, dialog_id:%d\n", je->request->sip_method, call_id, dialog_id);
eXosip_call_build_ack(excontext, je->did, &ack);
eXosip_lock(excontext);
eXosip_call_send_ack(excontext, je->did, ack);
eXosip_unlock(excontext);
}break;caseEXOSIP_CALL_PROCEEDING:
{
printf("recv EXOSIP_CALL_PROCEEDING\n");
RegisterSuccess(excontext, je);
}break;caseEXOSIP_CALL_REQUESTFAILURE:
{
printf("recv EXOSIP_CALL_REQUESTFAILURE\n");
RegisterSuccess(excontext, je);
}break;caseEXOSIP_CALL_MESSAGE_ANSWERED:
{
printf("recv EXOSIP_CALL_MESSAGE_ANSWERED\n");
RegisterSuccess(excontext, je);
}break;caseEXOSIP_CALL_RELEASED:
{
printf("recv EXOSIP_CALL_RELEASED\n");
RegisterSuccess(excontext, je);
}break;caseEXOSIP_CALL_CLOSED:
{
printf("recv EXOSIP_CALL_CLOSED\n");
RegisterSuccess(excontext, je);
}break;caseEXOSIP_CALL_MESSAGE_NEW:
{
printf("recv EXOSIP_CALL_MESSAGE_NEW\n");
RegisterSuccess(excontext, je);
}break;default:
{
printf("##test,%s:%d, unsupport type:%d\n", __FILE__, __LINE__, je->type);
RegisterSuccess(excontext, je);
}break;
}
eXosip_event_free(je);
}return(0);
}
View Code
海康--》sip服务器发送注册包
sip服务器--》 海康发送200包
sip服务器--》 海康摄像头发送message保活包
问题汇总:
./a.out: error while loading shared libraries: libeXosip2.so.12: cannot open shared object file: No such file or directory
解决办法:
sudo vim /etc/ld.so.conf
末尾添加 /usr/local/lib
保存退出,执行sudo ldconfig
参考文档:
exosip官网: http://savannah.nongnu.org/projects/exosip/
oSIP官网:http://savannah.gnu.org/projects/osip/
开发手册:https://wenku.baidu.com/view/88cb5112cc7931b765ce15b0.html?sxts=1570885286497
eXosip开发手册 :https://blog.csdn.net/mantis_1984/article/details/52948216
https://www.cnblogs.com/swing07/p/10862480.html
https://blog.csdn.net/zzqgtt/article/details/87179815
https://gitee.com/leixiaohua1020/simplest_librtmp_example
https://github.com/ossrs/librtmp
https://github.com/logisticpeach/librtp
https://www.jianshu.com/p/cc7df89d98f4
https://blog.csdn.net/wwyyxx26/article/details/15224879
https://www.bbsmax.com/A/RnJWZqYEzq/
https://www.cnblogs.com/codenow/p/4871704.html
从海康7816的ps流里获取数据h264数据
github: 作为上级域,可以对接海康、大华、宇视等gb28181平台,获取ps流,转换为标准h.264裸流
http://www.voidcn.com/article/p-cimuzoim-bab.html
https://blog.csdn.net/mo4776/article/details/78239344
https://blog.csdn.net/wh8_2011/article/details/48415105
python-librtmp
https://www.jb51.net/article/165927.htm
librtmp使用的是0.3.0,使用树莓派noir官方摄像头适配的。
目的是能使用Python进行rtmp推流,方便在h264帧里加入弹幕等操作。通过wireshark抓ffmpeg的包一点点改动,最终可以在red5和斗鱼上推流了。
#-- coding: utf-8 --#http://blog.csdn.net/luhanglei
importpicameraimporttimeimporttracebackimportctypesfrom librtmp import *
globalmeta_packetglobalstart_timeclass Writer(): #camera可以通过一个类文件的对象来输出,实现write方法即可
conn = None #rtmp连接
sps = None #记录sps帧,发过以后就不需要再发了(抓包看到ffmpeg是这样的)
pps = None #同上
sps_len = 0 #同上
pps_len = 0 #同上
time_stamp=0def __init__(self, conn):
self.conn=conndefwrite(self, data):try:#寻找h264帧间隔符
indexs =[]
index=0
data_len=len(data)while index < data_len - 3:if ord(data[index]) == 0x00 and ord(data[index + 1]) == 0x00 andord(
data[index+ 2]) == 0x00 and ord(data[index + 3]) == 0x01:
indexs.append(index)
index= index + 3index= index + 1
#寻找h264帧间隔符 完成
#通过间隔符个数确定类型,树莓派摄像头的第一帧是sps+pps同时发的
if len(indexs) == 1: #非sps pps帧
buf = data[4: len(data)] #裁掉原来的头(00 00 00 01),把帧内容拿出来
buf_len =len(buf)
type= ord(buf[0]) & 0x1f
if type == 0x05: #关键帧,根据wire shark抓包结果,需要拼装sps pps 帧内容 三部分,长度都用4个字节表示
body0 = 0x17data_body_array=[bytes(bytearray(
[body0,0x01, 0x00, 0x00, 0x00, (self.sps_len >> 24) & 0xff, (self.sps_len >> 16) & 0xff,
(self.sps_len>> 8) & 0xff,
self.sps_len& 0xff])), self.sps,
bytes(bytearray(
[(self.pps_len>> 24) & 0xff, (self.pps_len >> 16) & 0xff, (self.pps_len >> 8) & 0xff,
self.pps_len& 0xff])),
self.pps,
bytes(bytearray(
[(buf_len>> 24) & 0xff, (buf_len >> 16) & 0xff, (buf_len >> 8) & 0xff, (buf_len) & 0xff])),
buf
]
mbody= ''.join(data_body_array)
time_stamp= 0 #第一次发出的时候,发时间戳0,此后发真时间戳
if self.time_stamp !=0:
time_stamp= int((time.time() - start_time) * 1000)
packet_body= RTMPPacket(type=PACKET_TYPE_VIDEO, format=PACKET_SIZE_LARGE, channel=0x06,
timestamp=time_stamp, body=mbody)
packet_body.packet.m_nInfoField2= 1
else: #非关键帧
body0 = 0x27data_body_array=[bytes(bytearray(
[body0,0x01, 0x00, 0x00, 0x00, (buf_len >> 24) & 0xff, (buf_len >> 16) & 0xff,
(buf_len>> 8) & 0xff,
(buf_len)& 0xff])), buf]
mbody= ''.join(data_body_array)#if (self.time_stamp == 0):
self.time_stamp = int((time.time() - start_time) * 1000)
packet_body= RTMPPacket(type=PACKET_TYPE_VIDEO, format=PACKET_SIZE_MEDIUM, channel=0x06,
timestamp=self.time_stamp, body=mbody)
self.conn.send_packet(packet_body)elif len(indexs) == 2: #sps pps帧
if self.sps is notNone:returndata_body_array= [bytes(bytearray([0x17, 0x00, 0x00, 0x00, 0x00, 0x01]))]
sps= data[indexs[0] + 4: indexs[1]]
sps_len=len(sps)
pps= data[indexs[1] + 4: len(data)]
pps_len=len(pps)
self.sps=sps
self.sps_len=sps_len
self.pps=pps
self.pps_len=pps_len
data_body_array.append(sps[1:4])
data_body_array.append(bytes(bytearray([0xff, 0xe1, (sps_len >> 8) & 0xff, sps_len & 0xff])))
data_body_array.append(sps)
data_body_array.append(bytes(bytearray([0x01, (pps_len >> 8) & 0xff, pps_len & 0xff])))
data_body_array.append(pps)
data_body= ''.join(data_body_array)
body_packet= RTMPPacket(type=PACKET_TYPE_VIDEO, format=PACKET_SIZE_LARGE, channel=0x06,
timestamp=0, body=data_body)
body_packet.packet.m_nInfoField2= 1self.conn.send_packet(meta_packet, queue=True)
self.conn.send_packet(body_packet, queue=True)exceptException, e:
traceback.print_exc()defflush(self):pass
def get_property_string(string): #返回两字节string长度及string
length =len(string)return ''.join([chr((length >> 8) & 0xff), chr(length & 0xff), string])def get_meta_string(string): #按照meta packet要求格式返回bytes,带02前缀
return ''.join([chr(0x02), get_property_string(string)])defget_meta_double(db):
nums= [0x00]
fp=ctypes.pointer(ctypes.c_double(db))
cp=ctypes.cast(fp, ctypes.POINTER(ctypes.c_longlong))for i in range(7, -1, -1):
nums.append((cp.contents.value>> (i * 8)) & 0xff)return ''.join(bytes(bytearray(nums)))defget_meta_boolean(isTrue):
nums= [0x01]if(isTrue):
nums.append(0x01)else:
nums.append(0x00)return ''.join(bytes(bytearray(nums)))
conn=RTMP('rtmp://192.168.199.154/oflaDemo/test', #推流地址
live=True)
librtmp.RTMP_EnableWrite(conn.rtmp)
conn.connect()
start_time=time.time()#拼装视频格式的数据包
meta_body_array = [get_meta_string('@setDataFrame'), get_meta_string('onMetaData'),
bytes(bytearray([0x08, 0x00, 0x00, 0x00, 0x06])), #两个字符串和ECMA array头,共计6个元素,注释掉了音频相关数据
get_property_string('width'), get_meta_double(640.0),
get_property_string('height'), get_meta_double(480.0),
get_property_string('videodatarate'), get_meta_double(0.0),
get_property_string('framerate'), get_meta_double(25.0),
get_property_string('videocodecid'), get_meta_double(7.0),#get_property_string('audiodatarate'), get_meta_double(125.0),
#get_property_string('audiosamplerate'), get_meta_double(44100.0),
#get_property_string('audiosamplesize'), get_meta_double(16.0),
#get_property_string('stereo'), get_meta_boolean(True),
#get_property_string('audiocodecid'), get_meta_double(10.0),
get_property_string('encoder'), get_meta_string('Lavf57.56.101'),
bytes(bytearray([0x00, 0x00, 0x09]))
]
meta_body= ''.join(meta_body_array)print meta_body.encode('hex')
meta_packet= RTMPPacket(type=PACKET_TYPE_INFO, format=PACKET_SIZE_LARGE, channel=0x04,
timestamp=0, body=meta_body)
meta_packet.packet.m_nInfoField2= 1 #修改stream id
stream = conn.create_stream(writeable=True)
with picamera.PiCamera() as camera:
camera.start_preview()
time.sleep(2)
camera.start_recording(Writer(conn), format='h264', resize=(640, 480), intra_period=25,
quality=25) #开始录制,数据输出到Writer的对象里
while True:#永远不停止
time.sleep(60)
camera.stop_recording()
camera.stop_preview()
srs-librtmp
https://blog.csdn.net/ai2000ai/article/details/78329039
SRS提供的librtmp
应用场景
librtmp的主要应用场景包括:
播放RTMP流:譬如rtmpdump,将服务器的流读取后保存为flv文件。
推流:提供推流到RTMP服务器。
基于同步阻塞socket,客户端用可以了。
arm:编译出来给arm-linux用,譬如某些设备上,采集后推送到RTMP服务器。
不支持直接发布h.264裸码流,而srs-librtmp支持,参考:publish-h264-raw-data
备注:关于链接ssl,握手协议,简单握手和复杂握手,参考RTMP握手协议
备注:ARM上使用srs-librtmp需要交叉编译,参考srs-arm,即使用交叉编译环境编译srs-librtmp(可以不依赖于其他库,ssl/st都不需要)
librtmp做Server
群里有很多人问,librtmp如何做server,实在不胜其骚扰,所以单列一章。
server的特点是会有多个客户端连接,至少有两个:一个推流连接,一个播放连接。所以server有两种策略:
每个连接一个线程或进程:像apache。这样可以用同步socket来收发数据(同步简单)。坏处就是没法支持很高并发,1000个已经到顶了,得开1000个线程/进程啊。
使用单进程,但是用异步socket:像nginx这样。好处就是能支持很高并发。坏处就是异步socket麻烦。
rtmpdump提供的librtmp,当然是基于同步socket的。所以使用librtmp做server,只能采取第一种方法,即用多线程处理多个连接。多线程多麻烦啊!要锁,同步,而且还支持不了多少个。
librtmp的定位就是客户端程序,偏偏要超越它的定位去使用,这种大约只有中国人才能这样“无所畏惧”。
嵌入式设备上做rtmp server,当然可以用srs/crtmpd/nginx-rtmp,轮也轮不到librtmp。
SRS为何提供librtmp
srs提供的客户端srs-librtmp的定位和librtmp不一样,主要是:
librtmp的代码确实很烂,毋庸置疑,典型的代码堆积。
librtmp接口定义不良好,这个对比srs就可以看出,使用起来得看实现代码。
没有实例:接口的使用最好提供实例,srs提供了publish/play/rtmpdump实例。
最小依赖关系:srs调整了模块化,只取出了core/kernel/rtmp三个模块,其他代码没有编译到srs-librtmp中,避免了冗余。
最少依赖库:srs-librtmp只依赖c/c++标准库(若需要复杂握手需要依赖openssl,srs也编译出来了,只需要加入链接即可)。
不依赖st:srs-librtmp使用同步阻塞socket,没有使用st(st主要是服务器处理并发需要)。
SRS提供了测速函数,直接调用srs-librtmp就可以完成到服务器的测速。参考:Bandwidth Test
SRS提供了日志接口,可以获取服务器端的信息,譬如版本,对应的session id。参考:Tracable log
SRS可以直接导出一个srs-librtmp的project,编译成.h和.a使用。或者导出为.h和.cpp,一个大文件。参考:export srs librtmp
一句话,srs为何提供客户端开发库?因为rtmp客户端开发不方便,不直观,不简洁。
Export Srs Librtmp
SRS在2.0提供了导出srs-librtmp的编译选项,可以将srs-librtmp单独导出为project,单独编译生成.h和.a,方便在linux和windows平台编译。
使用方法,导出为project,可以make成.h和.a:
dir=/home/winlin/srs-librtmp &&
rm -rf $dir &&
./configure --export-librtmp-project=$dir &&
cd $dir && make &&
./objs/research/librtmp/srs_play rtmp://ossrs.net/live/livestream
SRS将srs-librtmp导出为独立可以make的项目,生成.a静态库和.h头文件,以及生成了srs-librtmp的所有实例。
还可以直接导出为一个文件,提供了简单的使用实例,其他实例参考research的其他例子:
dir=/home/winlin/srs-librtmp &&
rm -rf $dir &&
./configure --export-librtmp-single=$dir &&
cd $dir && gcc example.c srs_librtmp.cpp -g -O0 -lstdc++ -o example &&
strip example && ./example
备注:导出目录支持相对目录和绝对目录。
编译srs-librtmp
编译SRS时,会自动编译srs-librtmp,譬如:
./configure --with-librtmp --without-ssl
编译会生成srs-librtmp和对应的实例。
备注:支持librtmp只需要打开--with-librtmp,但推荐打开--without-ssl,不依赖于ssl,对于一般客户端(不需要模拟flash)足够了。这样srs-librtmp不依赖于任何其他库,在x86/x64/arm等平台都可以编译和运行
备注:就算打开了--with-ssl,srslibrtmp也只提供simple_handshake函数,不提供complex_handshake函数。所以推荐关闭ssl,不依赖于ssl,没有实际的用处。
SRS编译成功后,用户就可以使用这些库开发
Windows下编译srs-librtmp
srs-librtmp可以只依赖于c++和socket,可以在windows下编译。
先使用SRS导出srs-librtmp,然后在vs中编译,参考:export srs librtmp
使用了一些linux的头文件,需要做一些portal。
注意:srs-librtmp客户端推流和抓流,不需要ssl库。代码都是c++/stl,网络部分用的是同步socket。
数据格式
srs-librtmp提供了一系列接口函数,就数据按照一定格式发送到服务器,或者从服务器读取音视频数据。
数据接口包括:
读取数据包:int srs_read_packet(int* type, u_int32_t* timestamp, char** data, int* size)
发送数据包:int srs_write_packet(int type, u_int32_t timestamp, char* data, int size)
接口接受的的数据(char* data),音视频数据,格式为flv的Video/Audio数据。参考srs的doc目录的规范文件video_file_format_spec_v10_1.pdf
音频数据格式参考:E.4.2.1 AUDIODATA,p76,譬如,aac编码的音频数据。
视频数据格式参考:E.4.3.1 VIDEODATA,p78,譬如,h.264编码的视频数据。
脚本数据格式参考:E.4.4.1 SCRIPTDATA,p80,譬如,onMetadata,流的信息(宽高,码率,分辨率等)
数据类型(int type)定义如下(E.4.1 FLV Tag,page 75):
音频:8 = audio,宏定义:SRS_RTMP_TYPE_AUDIO
视频:9 = video,宏定义:SRS_RTMP_TYPE_VIDEO
脚本数据:18 = script data,宏定义:SRS_RTMP_TYPE_SCRIPT
其他的数据,譬如时间戳,都是通过参数接受和发送。
另外,文档其他重要信息:
flv文件头格式:E.2 The FLV header,p74。
flv文件主体格式:E.3 The FLV File Body,p74。
tag头格式:E.4.1 FLV Tag,p75。
使用flv格式的原因:
flv的格式足够简单。
ffmpeg也是用的这种格式
收到流后加上flv tag header,就可以直接保存为flv文件
从flv文件解封装数据后,只要将tag的内容给接口就可以,flv的tag头很简单。
Publish H.264 Raw Data
SRS-librtmp支持发布h.264裸码流,直接调用api即可将数据发送给SRS。
总结起来就是说,H264的裸码流(帧)转换RTMP时:
dts和pts是不在h264流中的,外部给出。
SPS和PPS在RTMP一个包里面发出去。
RTMP包=5字节RTMP包头+H264头+H264数据,具体参考:SrsAvcAacCodec::video_avc_demux
直接提供接口,发送h264数据,其中包含annexb的头:N[00] 00 00 01, where N>=0.
加了一个直接发送h264裸码流的接口:
/**
* write h.264 raw frame over RTMP to rtmp server.
* @param frames the input h264 raw data, encoded h.264 I/P/B frames data.
* frames can be one or more than one frame,
* each frame prefixed h.264 annexb header, by N[00] 00 00 01, where N>=0,
* for instance, frame = header(00 00 00 01) + payload(67 42 80 29 95 A0 14 01 6E 40)
* about annexb, @see H.264-AVC-ISO_IEC_14496-10.pdf, page 211.
* @paam frames_size the size of h264 raw data.
* assert frames_size > 0, at least has 1 bytes header.
* @param dts the dts of h.264 raw data.
* @param pts the pts of h.264 raw data.
*
* @remark, user should free the frames.
* @remark, the tbn of dts/pts is 1/1000 for RTMP, that is, in ms.
* @remark, cts = pts - dts
*
* @return 0, success; otherswise, failed.
*/
extern int srs_h264_write_raw_frames(srs_rtmp_t rtmp,
char* frames, int frames_size, u_int32_t dts, u_int32_t pts
);
里面的数据是:
// SPS
000000016742802995A014016E40
// PPS
0000000168CE3880
// IFrame
0000000165B8041014C038008B0D0D3A071.....
// PFrame
0000000141E02041F8CDDC562BBDEFAD2F.....
调用时,可以SPS和PPS一起发,帧一次发一个:
// SPS+PPS
srs_h264_write_raw_frame('000000016742802995A014016E400000000168CE3880', size, dts, pts)
// IFrame
srs_h264_write_raw_frame('0000000165B8041014C038008B0D0D3A071......', size, dts, pts)
// PFrame
srs_h264_write_raw_frame('0000000141E02041F8CDDC562BBDEFAD2F......', size, dts, pts)
调用时,可以一次发一次frame也行:
// SPS
srs_h264_write_raw_frame('000000016742802995A014016E4', size, dts, pts)
// PPS
srs_h264_write_raw_frame('00000000168CE3880', size, dts, pts)
// IFrame
srs_h264_write_raw_frame('0000000165B8041014C038008B0D0D3A071......', size, dts, pts)
// PFrame
srs_h264_write_raw_frame('0000000141E02041F8CDDC562BBDEFAD2F......', size, dts, pts)
Publish Audio Raw Stream
srs-librtmp提供了api可以将音频裸码流发布到SRS,支持AAC ADTS格式。
API定义如下:
/**
* write an audio raw frame to srs.
* not similar to h.264 video, the audio never aggregated, always
* encoded one frame by one, so this api is used to write a frame.
*
* @param sound_format Format of SoundData. The following values are defined:
* 0 = Linear PCM, platform endian
* 1 = ADPCM
* 2 = MP3
* 3 = Linear PCM, little endian
* 4 = Nellymoser 16 kHz mono
* 5 = Nellymoser 8 kHz mono
* 6 = Nellymoser
* 7 = G.711 A-law logarithmic PCM
* 8 = G.711 mu-law logarithmic PCM
* 9 = reserved
* 10 = AAC
* 11 = Speex
* 14 = MP3 8 kHz
* 15 = Device-specific sound
* Formats 7, 8, 14, and 15 are reserved.
* AAC is supported in Flash Player 9,0,115,0 and higher.
* Speex is supported in Flash Player 10 and higher.
* @param sound_rate Sampling rate. The following values are defined:
* 0 = 5.5 kHz
* 1 = 11 kHz
* 2 = 22 kHz
* 3 = 44 kHz
* @param sound_size Size of each audio sample. This parameter only pertains to
* uncompressed formats. Compressed formats always decode
* to 16 bits internally.
* 0 = 8-bit samples
* 1 = 16-bit samples
* @param sound_type Mono or stereo sound
* 0 = Mono sound
* 1 = Stereo sound
* @param timestamp The timestamp of audio.
*
* @example /trunk/research/librtmp/srs_aac_raw_publish.c
* @example /trunk/research/librtmp/srs_audio_raw_publish.c
*
* @remark for aac, the frame must be in ADTS format.
* @see aac-mp4a-format-ISO_IEC_14496-3+2001.pdf, page 75, 1.A.2.2 ADTS
* @remark for aac, only support profile 1-4, AAC main/LC/SSR/LTP,
* @see aac-mp4a-format-ISO_IEC_14496-3+2001.pdf, page 23, 1.5.1.1 Audio object type
*
* @see https://github.com/ossrs/srs/issues/212
* @see E.4.2.1 AUDIODATA of video_file_format_spec_v10_1.pdf
*
* @return 0, success; otherswise, failed.
*/
extern int srs_audio_write_raw_frame(srs_rtmp_t rtmp,
char sound_format, char sound_rate, char sound_size, char sound_type,
char* frame, int frame_size, u_int32_t timestamp
);
/**
* whether aac raw data is in adts format,
* which bytes sequence matches '1111 1111 1111'B, that is 0xFFF.
* @param aac_raw_data the input aac raw data, a encoded aac frame data.
* @param ac_raw_size the size of aac raw data.
*
* @reamrk used to check whether current frame is in adts format.
* @see aac-mp4a-format-ISO_IEC_14496-3+2001.pdf, page 75, 1.A.2.2 ADTS
* @example /trunk/research/librtmp/srs_aac_raw_publish.c
*
* @return 0 false; otherwise, true.
*/
extern srs_bool srs_aac_is_adts(char* aac_raw_data, int ac_raw_size);
/**
* parse the adts header to get the frame size,
* which bytes sequence matches '1111 1111 1111'B, that is 0xFFF.
* @param aac_raw_data the input aac raw data, a encoded aac frame data.
* @param ac_raw_size the size of aac raw data.
*
* @return failed when <=0 failed; otherwise, ok.
*/
extern int srs_aac_adts_frame_size(char* aac_raw_data, int ac_raw_size);
调用实例参考#212,以及srs_audio_raw_publish.c和srs_aac_raw_publish.c,参考examples.
srs-librtmp Examples
SRS提供了实例sample,也会在编译srs-librtmp时自动编译:
research/librtmp/srs_play.c:播放RTMP流实例。
research/librtmp/srs_publish.c:推送RTMP流实例。
research/librtmp/srs_ingest_flv.c:读取本地FLV文件并推送RTMP流实例。
research/librtmp/srs_ingest_mp4.c:读取本地MP4文件并推送RTMP流实例。
research/librtmp/srs_ingest_rtmp.c:读取RTMP流并推送RTMP流实例。
research/librtmp/srs_bandwidth_check.c:带宽测试工具。
research/librtmp/srs_flv_injecter.c:点播FLV关键帧注入文件。
research/librtmp/srs_flv_parser.c:FLV文件查看工具。
research/librtmp/srs_detect_rtmp.c:RTMP流检测工具。
research/librtmp/srs_h264_raw_publish.c:H.264裸码流发布到SRS实例。
research/librtmp/srs_audio_raw_publish.c: Audio裸码流发布到SRS实例。
research/librtmp/srs_aac_raw_publish.c: Audio AAC ADTS裸码流发布到SRS实例。
research/librtmp/srs_rtmp_dump.c: 将RTMP流录制成flv文件实例。
./objs/srs_ingest_hls: 将HLS流采集成RTMP推送给SRS。
运行实例
启动SRS:
make && ./objs/srs -c srs.conf
推流实例:
make && ./objs/research/librtmp/objs/srs_publish rtmp://127.0.0.1:1935/live/livestream
备注:推流实例发送的视频数据不是真正的视频数据,实际使用时,譬如从摄像头取出h.264裸码流,需要封装成接口要求的数据,然后调用接口发送出去。或者直接发送h264裸码流。
播放实例:
make && ./objs/research/librtmp/objs/srs_play rtmp://ossrs.net/live/livestreamsuck rtmp stream like rtmpdump