/* * Copyright (C) 2011 Raphael Coeffic * * For the code parts originating from rtmpdump (rtmpsrv.c): * Copyright (C) 2009 Andrej Stepanchuk * Copyright (C) 2009 Howard Chu * * This file is part of SEMS, a free SIP media server. * * SEMS is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. This program is released under * the GPL with the additional exemption that compiling, linking, * and/or using OpenSSL is allowed. * * For a license to use the SEMS software under conditions * other than those described here, or to purchase support for this * software, please contact iptel.org by e-mail at the following addresses: * info@iptel.org * * SEMS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include "RtmpConnection.h" #include "RtmpSender.h" #include "RtmpAudio.h" #include "RtmpSession.h" #include "RtmpUtils.h" #include "AmSessionContainer.h" #include "log.h" #include "librtmp/log.h" #include /* interval we disallow duplicate requests, in msec */ #define DUPTIME 5000 RtmpConnection::RtmpConnection(int fd) : prev_stream_id(0), play_stream_id(0), publish_stream_id(0), sender(NULL), session(NULL), registered(false), di_reg_client(NULL), rtmp_cfg(NULL) { memset(&rtmp,0,sizeof(RTMP)); RTMP_Init(&rtmp); rtmp.m_sb.sb_socket = fd; ident = AmSession::getNewId(); di_reg_client = RtmpFactory_impl::instance()->getRegClient(); rtmp_cfg = RtmpFactory_impl::instance()->getConfig(); } RtmpConnection::~RtmpConnection() { } void RtmpConnection::setSessionPtr(RtmpSession* s) { m_session.lock(); DBG("session ptr = 0x%p\n",s); session = s; m_session.unlock(); } void RtmpConnection::run() { RTMPPacket packet; memset(&packet,0,sizeof(RTMPPacket)); DBG("Starting connection (socket=%i)\n",rtmp.m_sb.sb_socket); if (!RTMP_Serve(&rtmp)) { ERROR("Handshake failed\n"); RTMP_Close(&rtmp); return; } // from here on, we send asynchronously sender = new RtmpSender(&rtmp); if(!sender){ ERROR("Could not allocate sender.\n"); RTMP_Close(&rtmp); return; } sender->start(); while(RTMP_IsConnected(&rtmp)) { if(!RTMP_ReadPacket(&rtmp,&packet)) { if(RTMP_IsTimedout(&rtmp)) continue; else break; } if(!RTMPPacket_IsReady(&packet)) continue; int err = processPacket(&packet); RTMPPacket_Free(&packet); if(err < 0) { ERROR("could not process packet\n"); break; } } if(registered){ RtmpFactory_impl::instance()->removeConnection(ident); removeRegistration(); registered = false; } detachSession(); // terminate the sender thread sender->stop(); sender->join(); delete sender; sender = NULL; RTMP_Close(&rtmp); DBG("connection closed\n"); AmThreadWatcher::instance()->add(this); } void RtmpConnection::on_stop() {} int RtmpConnection::processPacket(RTMPPacket* packet) { //DBG("received packet type %02X, size %u bytes, Stream ID %i", // packet->m_packetType, packet->m_nBodySize, packet->m_nInfoField2); switch (packet->m_packetType) { case 0x01: // chunk size HandleChangeChunkSize(&rtmp, packet); break; case 0x03: // bytes read report break; case 0x04: // ctrl HandleCtrl(packet); break; case 0x05: // server bw DBG("Server BW msg not yet supported.\n"); //TODO: HandleServerBW(&rtmp, packet); break; case 0x06: // client bw DBG("Client BW msg not yet supported.\n"); //TODO: HandleClientBW(&rtmp, packet); break; case RTMP_PACKET_TYPE_AUDIO: // audio data // // note(rco): librtmp writes the absolute timestamp into every packet // after parsing it. // //DBG("audio packet: ts = %8i\n",packet->m_nTimeStamp); rxAudio(packet); break; case RTMP_PACKET_TYPE_VIDEO: // video data DBG("video packet"); rxVideo(packet); break; case 0x0F: // flex stream send break; case 0x10: // flex shared object break; case 0x11: // flex message DBG("flex message\n"); if (invoke(packet, 1)) return -1; break; case RTMP_PACKET_TYPE_INFO: // metadata (notify) break; case 0x13: // shared object break; case INVOKE_PTYPE: // invoke DBG("invoke message\n"); if (invoke(packet, 0)) return -1; break; case 0x16: /* flv */ break; default: DBG("unknown packet type (0x%02x)",packet->m_packetType); break; } return 0; } // Returns 0 for OK/Failed/error, 1 for 'Stop or Complete' int RtmpConnection::invoke(RTMPPacket *packet, unsigned int offset) { const char *body; unsigned int nBodySize; int ret = 0, nRes; body = packet->m_body + offset; nBodySize = packet->m_nBodySize - offset; if (body[0] != 0x02) // make sure it is a string method name we start with { WARN("Sanity failed. no string method in invoke packet"); return 0; } AMFObject obj; nRes = AMF_Decode(&obj, body, nBodySize, FALSE); if (nRes < 0) { ERROR("error decoding invoke packet"); return 0; } AMF_Dump(&obj); AVal method; AMFProp_GetString(AMF_GetProp(&obj, NULL, 0), &method); double txn = AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 1)); DBG("client invoking <%s>\n",method.av_val); if (AVMATCH(&method, &av_connect)) { AMFObject cobj; AVal pname, pval; int i; packet->m_body = NULL; AMFProp_GetObject(AMF_GetProp(&obj, NULL, 2), &cobj); for (i=0; iSendConnectResult(txn); } else if (AVMATCH(&method, &av_createStream)) { sender->SendResultNumber(txn, ++prev_stream_id); DBG("createStream: new channel %i",prev_stream_id); } else if (AVMATCH(&method, &av_getStreamLength)) { // TODO: find value for live streams (0?, -1?) sender->SendResultNumber(txn, 0.0); } else if (AVMATCH(&method, &av_play)) { AMFProp_GetString(AMF_GetProp(&obj, NULL, 3), &rtmp.Link.playpath); DBG("playpath = <%.*s>\n", rtmp.Link.playpath.av_len, rtmp.Link.playpath.av_val); if (rtmp.Link.tcUrl.av_len) { DBG("tcUrl = <%.*s>\n", rtmp.Link.tcUrl.av_len, rtmp.Link.tcUrl.av_val); if (rtmp.Link.app.av_val) { DBG("app = <%.*s>\n", rtmp.Link.app.av_len, rtmp.Link.app.av_val); } if (rtmp.Link.flashVer.av_val) { DBG("flashVer = <%.*s>\n", rtmp.Link.flashVer.av_len, rtmp.Link.flashVer.av_val); } if (rtmp.Link.swfUrl.av_val) { DBG("swfUrl = <%.*s>\n", rtmp.Link.swfUrl.av_len, rtmp.Link.swfUrl.av_val); } if (rtmp.Link.pageUrl.av_val) { DBG("pageUrl = <%.*s>\n", rtmp.Link.pageUrl.av_len, rtmp.Link.pageUrl.av_val); } } play_stream_id = packet->m_nInfoField2; m_session.lock(); if(session) { session->setPlayStreamID(play_stream_id); sender->SendStreamBegin(); sender->SendPlayStart(); } m_session.unlock(); } else if(AVMATCH(&method, &av_publish)) { DBG("Client is now publishing (Stream ID = %i)\n", packet->m_nInfoField2); publish_stream_id = packet->m_nInfoField2; } else if(AVMATCH(&method, &av_closeStream)) { DBG("received closeStream with StreamID=%i\n",packet->m_nInfoField2); stopStream(packet->m_nInfoField2); } else if(AVMATCH(&method, &av_deleteStream)) { // - compare StreamID with play_stream_id // - if matched: stop session unsigned int stream_id = (unsigned int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 3)); DBG("received deleteStream with StreamID=%i\n",stream_id); stopStream(stream_id); } else if(AVMATCH(&method, &av_dial)) { AVal uri={0,0}; if(obj.o_num > 3){ AMFProp_GetString(AMF_GetProp(&obj, NULL, 3), &uri); } if(!uri.av_len){ // missing URI parameter sender->SendErrorResult(txn,"Sono.Call.NoUri"); } else { m_session.lock(); if(session){ sender->SendErrorResult(txn,"Sono.Call.Existing"); } else { session = startSession(uri.av_val); if(!session) { sender->SendErrorResult(txn,"Sono.Call.Failed"); } } m_session.unlock(); } } else if(AVMATCH(&method, &av_hangup)) { disconnectSession(); } else if(AVMATCH(&method, &av_register)) { if(!registered){ if(RtmpFactory_impl::instance()->addConnection(ident,this) < 0) { ERROR("could not register RTMP connection (ident='%s')\n",ident.c_str()); sender->SendErrorResult(txn,"Sono.Registration.Failed"); } else { registered = true; DBG("RTMP connection registered (ident='%s')\n",ident.c_str()); sender->SendRegisterResult(txn,ident.c_str()); } } if(registered){ if(di_reg_client && !rtmp_cfg->ImplicitRegistrar.empty()) { createRegistration(rtmp_cfg->ImplicitRegistrar, ident,rtmp_cfg->FromName); } } } else if(AVMATCH(&method, &av_accept)) { m_session.lock(); if(session) { session->accept(); } else { //TODO: send errror } m_session.unlock(); } AMF_Reset(&obj); return ret; } int RtmpConnection::SendCallStatus(int status) { return sender->SendCallStatus(status); } int RtmpConnection::NotifyIncomingCall(const string& uri) { return sender->NotifyIncomingCall(uri); } void RtmpConnection::HandleCtrl(const RTMPPacket *packet) { short nType = -1; unsigned int tmp; if (packet->m_body && packet->m_nBodySize >= 2) nType = AMF_DecodeInt16(packet->m_body); DBG("received ctrl. type: %d, len: %d", nType, packet->m_nBodySize); /*RTMP_LogHex(packet.m_body, packet.m_nBodySize);*/ if (packet->m_nBodySize >= 6) { switch (nType) { case 0: tmp = AMF_DecodeInt32(packet->m_body + 2); DBG("Stream Begin %d", tmp); break; case 1: tmp = AMF_DecodeInt32(packet->m_body + 2); DBG("Stream EOF %d", tmp); if (rtmp.m_pausing == 1) rtmp.m_pausing = 2; break; case 2: tmp = AMF_DecodeInt32(packet->m_body + 2); DBG("Stream Dry %d", tmp); break; case 3: tmp = AMF_DecodeInt32(packet->m_body + 2); DBG("Stream Ack %d", tmp); break; case 4: tmp = AMF_DecodeInt32(packet->m_body + 2); DBG("Stream IsRecorded %d", tmp); break; case 6: /* server ping. reply with pong. */ tmp = AMF_DecodeInt32(packet->m_body + 2); DBG("Ping %d", tmp); sender->SendCtrl(0x07, tmp, 0); break; /* FMS 3.5 servers send the following two controls to let the client * know when the server has sent a complete buffer. I.e., when the * server has sent an amount of data equal to m_nBufferMS in duration. * The server meters its output so that data arrives at the client * in realtime and no faster. * * The rtmpdump program tries to set m_nBufferMS as large as * possible, to force the server to send data as fast as possible. * In practice, the server appears to cap this at about 1 hour's * worth of data. After the server has sent a complete buffer, and * sends this BufferEmpty message, it will wait until the play * duration of that buffer has passed before sending a new buffer. * The BufferReady message will be sent when the new buffer starts. * (There is no BufferReady message for the very first buffer; * presumably the Stream Begin message is sufficient for that * purpose.) * * If the network speed is much faster than the data bitrate, then * there may be long delays between the end of one buffer and the * start of the next. * * Since usually the network allows data to be sent at * faster than realtime, and rtmpdump wants to download the data * as fast as possible, we use this RTMP_LF_BUFX hack: when we * get the BufferEmpty message, we send a Pause followed by an * Unpause. This causes the server to send the next buffer immediately * instead of waiting for the full duration to elapse. (That's * also the purpose of the ToggleStream function, which rtmpdump * calls if we get a read timeout.) * * Media player apps don't need this hack since they are just * going to play the data in realtime anyway. It also doesn't work * for live streams since they obviously can only be sent in * realtime. And it's all moot if the network speed is actually * slower than the media bitrate. */ case 31: tmp = AMF_DecodeInt32(packet->m_body + 2); RTMP_Log(RTMP_LOGDEBUG, "%s, Stream BufferEmpty %d", __FUNCTION__, tmp); if (!(rtmp.Link.lFlags & RTMP_LF_BUFX)) break; if (!rtmp.m_pausing) { rtmp.m_pauseStamp = rtmp.m_channelTimestamp[rtmp.m_mediaChannel]; sender->SendPause(TRUE, rtmp.m_pauseStamp); rtmp.m_pausing = 1; } else if (rtmp.m_pausing == 2) { sender->SendPause(FALSE, rtmp.m_pauseStamp); rtmp.m_pausing = 3; } break; case 32: tmp = AMF_DecodeInt32(packet->m_body + 2); RTMP_Log(RTMP_LOGDEBUG, "%s, Stream BufferReady %d", __FUNCTION__, tmp); break; default: tmp = AMF_DecodeInt32(packet->m_body + 2); RTMP_Log(RTMP_LOGDEBUG, "%s, Stream xx %d", __FUNCTION__, tmp); break; } } if (nType == 0x1A) { RTMP_Log(RTMP_LOGDEBUG, "%s, SWFVerification ping received: ", __FUNCTION__); #ifdef CRYPTO /*RTMP_LogHex(packet.m_body, packet.m_nBodySize); */ /* respond with HMAC SHA256 of decompressed SWF, key is the 30byte player key, also the last 30 bytes of the server handshake are applied */ if (rtmp.Link.SWFSize) { SendCtrl(0x1B, 0, 0); } else { ERROR("Ignoring SWFVerification request, use --swfVfy!"); } #else ERROR("Ignoring SWFVerification request, no CRYPTO support!"); #endif } } //#define DUMP_AUDIO 1 #ifdef DUMP_AUDIO static void dump_audio(RTMPPacket *packet) { static int dump_fd=0; if(dump_fd == 0){ dump_fd = open("speex_out.raw",O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH); if(dump_fd < 0) ERROR("could not open speex_out.raw: %s\n",strerror(errno)); } if(dump_fd < 0) return; uint32_t pkg_size = packet->m_nBodySize-1; write(dump_fd,&pkg_size,sizeof(uint32_t)); write(dump_fd,packet->m_body+1,pkg_size); } #endif void RtmpConnection::rxAudio(RTMPPacket *packet) { if(!packet) return; #ifdef DUMP_AUDIO dump_audio(); #endif // soundType (byte & 0x01) » 0 // 0: mono, 1: stereo // soundSize (byte & 0x02) » 1 // 0: 8-bit, 1: 16-bit // soundRate (byte & 0x0C) » 2 // 0: 5.5 kHz, 1: 11 kHz, 2: 22 kHz, 3: 44 kHz // soundFormat (byte & 0xf0) » 4 // 0: Uncompressed, 1: ADPCM, 2: MP3, 5: Nellymoser 8kHz mono, 6: Nellymoser, 11: Speex m_session.lock(); // stream not yet started if(!session){ m_session.unlock(); return; } session->bufferPacket(*packet); m_session.unlock(); } RtmpSession* RtmpConnection::startSession(const char* uri) { unique_ptr n_session(new RtmpSession(this)); AmSipDialog* dialout_dlg = n_session->dlg; string dialout_id = AmSession::getNewId(); dialout_dlg->setLocalTag(dialout_id); dialout_dlg->setCallid(AmSession::getNewId()); dialout_dlg->setRemoteParty("<" + string(uri) + ">"); dialout_dlg->setRemoteUri(uri); string local_party = "\"" + rtmp_cfg->FromName + "\" FromDomain.empty()){ local_party += rtmp_cfg->FromDomain; } else { int out_if = dialout_dlg->getOutboundIf(); local_party += AmConfig::SIP_Ifs[out_if].LocalIP; if(AmConfig::SIP_Ifs[out_if].LocalPort != 5060) local_party += ":" + int2str(AmConfig::SIP_Ifs[out_if].LocalPort); } local_party += ">"; dialout_dlg->setLocalParty(local_party); n_session->setCallgroup(dialout_id); switch(AmSessionContainer::instance()->addSession(dialout_id, n_session.get())){ case AmSessionContainer::ShutDown: DBG("Server shuting down... do not create a new session.\n"); return NULL; case AmSessionContainer::AlreadyExist: DBG("Session already exist !?\n"); return NULL; case AmSessionContainer::Inserted: break; } AmMimeBody sdp_body; sdp_body.addPart(SIP_APPLICATION_SDP); RtmpSession* pn_session = n_session.release(); if(dialout_dlg->sendRequest(SIP_METH_INVITE,&sdp_body) < 0) { ERROR("dialout_dlg->sendRequest() returned an error\n"); AmSessionContainer::instance()->destroySession(pn_session); return NULL; } pn_session->start(); return pn_session; } void RtmpConnection::detachSession() { m_session.lock(); DBG("detaching session: erasing session ptr... (s=%p)\n",session); if(session){ session->setConnectionPtr(NULL); session = NULL; } m_session.unlock(); } void RtmpConnection::disconnectSession() { m_session.lock(); if(session){ session->disconnect(); } m_session.unlock(); } void RtmpConnection::createRegistration(const string& domain, const string& user, const string& display_name, const string& auth_user, const string& passwd) { if(!di_reg_client) return; AmArg di_args,ret; di_args.push(domain.c_str()); di_args.push(user.c_str()); di_args.push(display_name.c_str()); // display name di_args.push(auth_user.c_str()); // auth_user di_args.push(passwd.c_str()); // pwd di_args.push(FACTORY_Q_NAME); di_args.push(""); di_args.push(""); di_args.push(ident.c_str()); di_reg_client->invoke("createRegistration", di_args, ret); } void RtmpConnection::removeRegistration() { if(!di_reg_client || ident.empty()) return; AmArg di_args,ret; di_args.push(ident.c_str()); di_reg_client->invoke("removeRegistration", di_args, ret); } void RtmpConnection::stopStream(unsigned int stream_id) { if(stream_id == play_stream_id){ play_stream_id = 0; sender->SendStreamEOF(); sender->SendPlayStop(); } else if(stream_id == publish_stream_id) { publish_stream_id = 0; } }