海康RTSP取流URL格式
原文地址:https://www.jianshu.com/p/8efcea89b11f
一、预览取流
设备预览取流的RTSP URL有新老版本,2012年之前的设备(比如V2.0版本的Netra设备)支持老的取流格式,之后的设备新老取流格式都支持。
老版本
URL规定:
rtsp://username:password@<ipaddress>/<videotype>/ch<number>/<streamtype>
注: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@<address>:<port>/Streaming/Channels/<id>(?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@<address>:<port>/Streaming/tracks/<id>(?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要在最后编译)
tar xvf 对应压缩包名 cd 解压出来的文件夹 ./configure make sudo make install
测试
#include <iostream> #include <eXosip2/eXosip.h> #include <netinet/in.h> #include <string> using namespace std; int main() { eXosip_t *sip = eXosip_malloc(); if(eXosip_init(sip) == OSIP_SUCCESS) { cout << "eXosip init ok" << endl; } else { cout << "exosip init fail" << endl; } int ret = eXosip_listen_addr(sip, IPPROTO_UDP, NULL, 0, AF_INET, 0); if(ret == OSIP_SUCCESS) { cout << "exosiop listen addr success" << endl; } else cout << "listen addr fail, ret: " << ret << endl; eXosip_quit(sip); cout << "test" << endl; return 0; }
编译运行
g++ test.cpp -losip2 -leXosip2 ./a.out
uac.cpp
#include <eXosip2/eXosip.h> #include <stdio.h> #include <stdlib.h> #include <stdarg.h> #include <netinet/in.h> #include <iostream> #include <string.h> 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; int call_id,dialog_id; int i,flag; int flag1=1; int iReturnCode; 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的地址 char command; char tmp[4096]; std::cout << "r 向服务器注册" << std::endl; std::cout << "c 取消注册" << std::endl; std::cout << "i 发起呼叫请求" << std::endl; std::cout << "h 挂断" << std::endl; std::cout << "q 推出程序" << std::endl; std::cout << "s 执行方法INFO" << std::endl; std::cout << "m 执行方法MESSAGE" << std::endl; //初始化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"); } //绑定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:" << std::endl; std::cin >> command; switch(command) { case \'r\': std::cout << "This modal is not completed!" << std::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!" << std::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); }
uas.cpp
# include <eXosip2/eXosip.h> # include <stdio.h> # include <stdlib.h> # include <stdarg.h> # include <netinet/in.h> #include <iostream> #include <string.h> //# include <Winsock2.h> 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; int call_id, dialog_id; int i,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 char command; 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; case EXOSIP_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; case EXOSIP_CALL_ACK: printf ("ACK recieved!\n"); // printf ("the cid is %s, did is %s/n", je->did, je->cid); break; case EXOSIP_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"); } } }
编译并运行
g++ uac.cpp -o uac -losip2 -leXosip2 -lpthread -losipparser2 g++ uas.cpp -o uas -losip2 -leXosip2 -lpthread -losipparser2 ./uas ./uac
exosip对接海康摄像头
#include <eXosip2/eXosip.h> #include <stdio.h> #include <stdlib.h> #include <stdarg.h> #include <netinet/in.h> #include <iostream> #include <string.h> 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; int call_id,dialog_id; int i,flag; int flag1=1; int iReturnCode; int registerOk; 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的地址 char command; char tmp[4096]; std::cout << "r 向服务器注册" << std::endl; std::cout << "c 取消注册" << std::endl; std::cout << "i 发起呼叫请求" << std::endl; std::cout << "h 挂断" << std::endl; std::cout << "q 推出程序" << std::endl; std::cout << "s 执行方法INFO" << std::endl; std::cout << "m 执行方法MESSAGE" << std::endl; //初始化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"); } //绑定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" << std::endl; osip_usleep(100000*50); continue; } switch (je->type) { case EXOSIP_MESSAGE_NEW: { //printf("new msg method:%s\n", je->request->sip_method); if(MSG_IS_REGISTER(je->request)) { std::cout << "msg body:" <<std::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:" <<std::endl; std::cout << body->body <<std::endl; } else { std::cout << "msg body:" <<std::endl; std::cout << body->body <<std::endl; } } else { std::cout << "get body failed" << std::endl; } } else if(strncmp(je->request->sip_method, "BYE", 4) != 0){ std::cout << "unsupport new msg method :" << je->request->sip_method <<std::endl; } RegisterSuccess(excontext, je); } break; case EXOSIP_MESSAGE_ANSWERED: { printf("answered method:%s\n", je->request->sip_method); RegisterSuccess(excontext, je); } break; case EXOSIP_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; case EXOSIP_CALL_PROCEEDING: { printf("recv EXOSIP_CALL_PROCEEDING\n"); RegisterSuccess(excontext, je); } break; case EXOSIP_CALL_REQUESTFAILURE: { printf("recv EXOSIP_CALL_REQUESTFAILURE\n"); RegisterSuccess(excontext, je); } break; case EXOSIP_CALL_MESSAGE_ANSWERED: { printf("recv EXOSIP_CALL_MESSAGE_ANSWERED\n"); RegisterSuccess(excontext, je); } break; case EXOSIP_CALL_RELEASED: { printf("recv EXOSIP_CALL_RELEASED\n"); RegisterSuccess(excontext, je); } break; case EXOSIP_CALL_CLOSED: { printf("recv EXOSIP_CALL_CLOSED\n"); RegisterSuccess(excontext, je); } break; case EXOSIP_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); }
海康--》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裸流
https://github.com/debugger999/gb28181ToH264
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 import picamera import time import traceback import ctypes from librtmp import * global meta_packet global start_time class Writer(): # camera可以通过一个类文件的对象来输出,实现write方法即可 conn = None # rtmp连接 sps = None # 记录sps帧,发过以后就不需要再发了(抓包看到ffmpeg是这样的) pps = None # 同上 sps_len = 0 # 同上 pps_len = 0 # 同上 time_stamp = 0 def __init__(self, conn): self.conn = conn def write(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 and ord( data[index + 2]) == 0x00 and ord(data[index + 3]) == 0x01: indexs.append(index) index = index + 3 index = 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 = 0x17 data_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 = 0x27 data_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 not None: return data_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 = 1 self.conn.send_packet(meta_packet, queue=True) self.conn.send_packet(body_packet, queue=True) except Exception, e: traceback.print_exc() def flush(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)]) def get_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))) def get_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
- 支持直接发布h.264裸码流,参考:publish-h264-raw-data
- 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)
- 发送h.264裸码流:参考publish-h264-raw-data
接口接受的的数据(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。
参考博客:http://blog.csdn.net/win_lin/article/details/41170653
总结起来就是说,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
-
);
对于例子中的h264流文件:http://winlinvip.github.io/srs.release/3rdparty/720p.h264.raw
里面的数据是:
-
// 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)
参考:https://github.com/ossrs/srs/issues/66#issuecomment-62240521
使用:https://github.com/ossrs/srs/issues/66#issuecomment-62245512
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.
参考:https://github.com/ossrs/srs/issues/212#issuecomment-63755405
使用实例:https://github.com/ossrs/srs/issues/212#issuecomment-64164018
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