/* * Copyright (C) 2005-2020 Team Kodi * https://kodi.tv * * SPDX-License-Identifier: GPL-2.0-or-later * See LICENSE.md for more information. */ #include "rtsp_client.hpp" #include "Socket.h" #include #include #include #include #include #if defined(_WIN32) || defined(_WIN64) #define strtok_r strtok_s #define strncasecmp _strnicmp int vasprintf(char** sptr, char* fmt, va_list argv) { int wanted = vsnprintf(*sptr = nullptr, 0, fmt, argv); if ((wanted < 0) || ((*sptr = (char*)malloc(1 + wanted)) == nullptr)) return -1; return vsprintf(*sptr, fmt, argv); } int asprintf(char** sptr, char* fmt, ...) { int retval; va_list argv; va_start(argv, fmt); retval = vasprintf(sptr, fmt, argv); va_end(argv); return retval; } #endif #define RTSP_DEFAULT_PORT 554 #define RTSP_RECEIVE_BUFFER 2048 #define RTP_HEADER_SIZE 12 #define VLEN 100 #define KEEPALIVE_INTERVAL 60 #define KEEPALIVE_MARGIN 5 #define UDP_ADDRESS_LEN 16 #define RTCP_BUFFER_SIZE 1024 using namespace std; using namespace OCTO; enum rtsp_state { RTSP_IDLE, RTSP_DESCRIBE, RTSP_SETUP, RTSP_PLAY, RTSP_RUNNING }; enum rtsp_result { RTSP_RESULT_OK = 200, }; struct rtsp_client { char* content_base; char* control; char session_id[64]; uint16_t stream_id; int keepalive_interval; char udp_address[UDP_ADDRESS_LEN]; uint16_t udp_port; Socket tcp_sock; Socket udp_sock; Socket rtcp_sock; enum rtsp_state state; int cseq; size_t fifo_size; uint16_t last_seq_nr; string name; int level; int quality; }; struct url { string protocol; string host; int port; string path; }; struct rtcp_app { uint8_t subtype; uint8_t pt; uint16_t len; uint32_t ssrc; char name[4]; uint16_t identifier; uint16_t string_len; }; static rtsp_client* rtsp = nullptr; static url parse_url(const std::string& str) { static const string prot_end = "://"; static const string host_end = "/"; url result; string::const_iterator begin = str.begin(); string::const_iterator end = search(begin, str.end(), prot_end.begin(), prot_end.end()); result.protocol.reserve(distance(begin, end)); transform(begin, end, back_inserter(result.protocol), ::tolower); advance(end, prot_end.size()); begin = end; end = search(begin, str.end(), host_end.begin(), host_end.end()); result.host.reserve(distance(begin, end)); transform(begin, end, back_inserter(result.host), ::tolower); advance(end, host_end.size()); begin = end; result.port = RTSP_DEFAULT_PORT; result.path.reserve(distance(begin, str.end())); transform(begin, str.end(), back_inserter(result.path), ::tolower); return result; } void split_string(const string& s, char delim, vector& elems) { stringstream ss; ss.str(s); string item; while (getline(ss, item, delim)) { elems.push_back(item); } } static int tcp_sock_read_line(string& line) { static string buf; while (true) { string::size_type pos = buf.find("\r\n"); if (pos != string::npos) { line = buf.substr(0, pos); buf.erase(0, pos + 2); return 0; } char tmp_buf[2048]; int size = rtsp->tcp_sock.receive(tmp_buf, sizeof(tmp_buf), 1); if (size <= 0) { return 1; } buf.append(&tmp_buf[0], &tmp_buf[size]); } } static string compose_url(const url& u) { stringstream res; res << u.protocol << "://" << u.host; if (u.port > 0) res << ":" << u.port; res << "/" << u.path; return res.str(); } static void parse_session(char* request_line, char* session, unsigned max, int* timeout) { char* state; char* tok; tok = strtok_r(request_line, ";", &state); if (tok == nullptr) return; strncpy(session, tok, min(strlen(tok), (size_t)(max - 1))); while ((tok = strtok_r(nullptr, ";", &state)) != nullptr) { if (strncmp(tok, "timeout=", 8) == 0) { *timeout = atoi(tok + 8); if (*timeout > 5) *timeout -= KEEPALIVE_MARGIN; else if (*timeout > 0) *timeout = 1; } } } static int parse_port(char* str, uint16_t* port) { int p = atoi(str); if (p < 0 || p > UINT16_MAX) return -1; *port = p; return 0; } static int parse_transport(char* request_line) { char* state; char* tok; int err; tok = strtok_r(request_line, ";", &state); if (tok == nullptr || strncmp(tok, "RTP/AVP", 7) != 0) return -1; tok = strtok_r(nullptr, ";", &state); if (tok == nullptr || strncmp(tok, "multicast", 9) != 0) return 0; while ((tok = strtok_r(nullptr, ";", &state)) != nullptr) { if (strncmp(tok, "destination=", 12) == 0) { strncpy(rtsp->udp_address, tok + 12, min(strlen(tok + 12), (size_t)(UDP_ADDRESS_LEN - 1))); } else if (strncmp(tok, "port=", 5) == 0) { char port[6]; char* end; memset(port, 0x00, 6); strncpy(port, tok + 5, min(strlen(tok + 5), (size_t)5)); if ((end = strstr(port, "-")) != nullptr) *end = '\0'; err = parse_port(port, &rtsp->udp_port); if (err) return err; } } return 0; } #define skip_whitespace(x) \ while (*x == ' ') \ x++ static enum rtsp_result rtsp_handle() { uint8_t buffer[512]; int rtsp_result = 0; bool have_header = false; size_t content_length = 0; size_t read = 0; char *in, *val; string in_str; /* Parse header */ while (!have_header) { if (tcp_sock_read_line(in_str) < 0) break; in = const_cast(in_str.c_str()); if (strncmp(in, "RTSP/1.0 ", 9) == 0) { rtsp_result = atoi(in + 9); } else if (strncmp(in, "Content-Base:", 13) == 0) { free(rtsp->content_base); val = in + 13; skip_whitespace(val); rtsp->content_base = strdup(val); } else if (strncmp(in, "Content-Length:", 15) == 0) { val = in + 16; skip_whitespace(val); content_length = atoi(val); } else if (strncmp("Session:", in, 8) == 0) { val = in + 8; skip_whitespace(val); parse_session(val, rtsp->session_id, 64, &rtsp->keepalive_interval); } else if (strncmp("Transport:", in, 10) == 0) { val = in + 10; skip_whitespace(val); if (parse_transport(val) != 0) { rtsp_result = -1; break; } } else if (strncmp("com.ses.streamID:", in, 17) == 0) { val = in + 17; skip_whitespace(val); rtsp->stream_id = atoi(val); } else if (in[0] == '\0') { have_header = true; } } /* Discard further content */ while (content_length > 0 && (read = rtsp->tcp_sock.receive((char*)buffer, sizeof(buffer), min(sizeof(buffer), content_length)))) content_length -= read; return (enum rtsp_result)rtsp_result; } bool rtsp_open(const string& name, const string& url_str) { string setup_url_str; const char* psz_setup_url; stringstream setup_ss; stringstream play_ss; url setup_url; rtsp_close(); rtsp = new rtsp_client(); if (rtsp == nullptr) return false; rtsp->name = name; rtsp->level = 0; rtsp->quality = 0; kodi::Log(ADDON_LOG_DEBUG, "try to open '%s'", url_str.c_str()); url dst = parse_url(url_str); kodi::Log(ADDON_LOG_DEBUG, "connect to host '%s'", dst.host.c_str()); if (!rtsp->tcp_sock.connect(dst.host, dst.port)) { kodi::Log(ADDON_LOG_ERROR, "Failed to connect to RTSP server %s:%d", dst.host.c_str(), dst.port); goto error; } // TODO: tcp keep alive? if (asprintf(&rtsp->content_base, "rtsp://%s:%d/", dst.host.c_str(), dst.port) < 0) { rtsp->content_base = nullptr; goto error; } rtsp->last_seq_nr = 0; rtsp->keepalive_interval = (KEEPALIVE_INTERVAL - KEEPALIVE_MARGIN); setup_url = dst; // reverse the satip protocol trick, as SAT>IP believes to be RTSP if (!strncasecmp(setup_url.protocol.c_str(), "satip", 5)) { setup_url.protocol = "rtsp"; } setup_url_str = compose_url(setup_url); psz_setup_url = setup_url_str.c_str(); // TODO: Find available port rtsp->udp_sock = Socket(af_inet, pf_inet, sock_dgram, udp); rtsp->udp_port = 6785; if (!rtsp->udp_sock.bind(rtsp->udp_port)) { goto error; } setup_ss << "SETUP " << setup_url_str << " RTSP/1.0\r\n"; setup_ss << "CSeq: " << rtsp->cseq++ << "\r\n"; setup_ss << "Transport: RTP/AVP;unicast;client_port=" << rtsp->udp_port << "-" << (rtsp->udp_port + 1) << "\r\n\r\n"; rtsp->tcp_sock.send(setup_ss.str()); if (rtsp_handle() != RTSP_RESULT_OK) { kodi::Log(ADDON_LOG_ERROR, "Failed to setup RTSP session"); goto error; } if (asprintf(&rtsp->control, "%sstream=%d", rtsp->content_base, rtsp->stream_id) < 0) { rtsp->control = nullptr; goto error; } play_ss << "PLAY " << rtsp->control << " RTSP/1.0\r\n"; play_ss << "CSeq: " << rtsp->cseq++ << "\r\n"; play_ss << "Session: " << rtsp->session_id << "\r\n\r\n"; rtsp->tcp_sock.send(play_ss.str()); if (rtsp_handle() != RTSP_RESULT_OK) { kodi::Log(ADDON_LOG_ERROR, "Failed to play RTSP session"); goto error; } rtsp->rtcp_sock = Socket(af_inet, pf_inet, sock_dgram, udp); if (!rtsp->rtcp_sock.bind(rtsp->udp_port + 1)) { goto error; } if (!rtsp->rtcp_sock.set_non_blocking(true)) { goto error; } return true; error: rtsp_close(); return false; } static void parse_rtcp(const char* buf, int size) { int offset = 0; while (size > 4) { const rtcp_app* app = reinterpret_cast(buf + offset); uint16_t len = 4 * (ntohs(app->len) + 1); if ((app->pt != 204) || (memcmp(app->name, "SES1", 4) != 0)) { size -= len; offset += len; continue; } uint16_t string_len = ntohs(app->string_len); string app_data(&buf[offset + sizeof(rtcp_app)], string_len); vector elems; split_string(app_data, ';', elems); if (elems.size() != 4) { return; } vector tuner; split_string(elems[2], ',', tuner); if (tuner.size() < 4) { return; } rtsp->level = atoi(tuner[1].c_str()); rtsp->quality = atoi(tuner[3].c_str()); return; } } int rtsp_read(void* buf, unsigned buf_size) { sockaddr addr; socklen_t addr_len = sizeof(addr); int ret = rtsp->udp_sock.recvfrom((char*)buf, buf_size, (sockaddr*)&addr, &addr_len); char rtcp_buf[RTCP_BUFFER_SIZE]; int rtcp_len = rtsp->rtcp_sock.recvfrom(rtcp_buf, RTCP_BUFFER_SIZE, (sockaddr*)&addr, &addr_len); parse_rtcp(rtcp_buf, rtcp_len); // TODO: check ip return ret; } static void rtsp_teardown() { if (!rtsp->tcp_sock.is_valid()) { return; } if (rtsp->session_id[0] > 0) { char* msg; int len; stringstream ss; rtsp->udp_sock.close(); ss << "TEARDOWN " << rtsp->control << " RTSP/1.0\r\n"; ss << "CSeq: " << rtsp->cseq++ << "\r\n"; ss << "Session: " << rtsp->session_id << "\r\n\r\n"; rtsp->tcp_sock.send(ss.str()); if (rtsp_handle() != RTSP_RESULT_OK) { kodi::Log(ADDON_LOG_ERROR, "Failed to teardown RTSP session"); return; } } } void rtsp_close() { if (rtsp) { rtsp_teardown(); rtsp->tcp_sock.close(); rtsp->udp_sock.close(); rtsp->rtcp_sock.close(); delete rtsp; rtsp = nullptr; } } void rtsp_fill_signal_status(kodi::addon::PVRSignalStatus& signal_status) { if (rtsp) { signal_status.SetAdapterName(rtsp->name); signal_status.SetSNR(0x1111 * rtsp->quality); signal_status.SetSignal(0x101 * rtsp->level); } }