Change add-on to use inputstream.ffmpegdirect which also enables timeshifting

This commit is contained in:
phunkyfish 2024-02-25 13:53:19 +00:00
parent 7e3a8bd8ed
commit 5622e10c72
11 changed files with 92 additions and 1609 deletions

View File

@ -12,14 +12,10 @@ include_directories(${KODI_INCLUDE_DIR}/.. # Hack way with "/..", need bigger Ko
set(DEPLIBS ${JSONCPP_LIBRARIES})
set(OCTONET_SOURCES src/addon.cpp
src/OctonetData.cpp
src/Socket.cpp
src/rtsp_client.cpp)
src/OctonetData.cpp)
set(OCTONET_HEADERS src/addon.h
src/OctonetData.h
src/Socket.h
src/rtsp_client.hpp)
src/OctonetData.h)
addon_version(pvr.octonet OCTONET)
add_definitions(-DOCTONET_VERSION=${OCTONET_VERSION})

View File

@ -4,7 +4,9 @@
version="20.3.0"
name="Digital Devices Octopus NET Client"
provider-name="digitaldevices">
<requires>@ADDON_DEPENDS@</requires>
<requires>@ADDON_DEPENDS@
<import addon="inputstream.ffmpegdirect" minversion="21.0.0"/>
</requires>
<extension
point="kodi.pvrclient"
library_@PLATFORM@="@LIBRARY_FILENAME@"/>

View File

@ -27,3 +27,41 @@ msgstr ""
msgctxt "#30001"
msgid "Could not load chanellist"
msgstr ""
msgctxt "#30002"
msgid "Timeshift"
msgstr ""
msgctxt "#30003"
msgid "Enable timeshift"
msgstr ""
msgctxt "#30004"
msgid "- Modify inputstream.ffmpegdirect settings..."
msgstr ""
msgctxt "#30005"
msgid "Inputstream settings"
msgstr ""
#empty strings from id 30006 to 30599
msgctxt "#30600"
msgid "The IP/address of the octonet server. Note that in case of an address, the protocol (http://) has to be omitted."
msgstr ""
msgctxt "#30601"
msgid "General settings required by the add-on."
msgstr ""
msgctxt "#30602"
msgid "Timeshift settings for pausing/rewinding and fast-forwarding live streams."
msgstr ""
msgctxt "#30603"
msgid "Enable the timeshift feature."
msgstr ""
msgctxt "#30604"
msgid "Open settings dialog for inputstream.ffmpegdirect for modification of timeshift and other settings."
msgstr ""

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings version="1">
<section id="pvr.octonet">
<category id="main" label="128" help="-1">
<category id="main" label="128" help="30601">
<group id="1" label="-1">
<!-- Octonet Server Address -->
<setting id="octonetAddress" type="string" label="30000" help="-1">
<setting id="octonetAddress" type="string" label="30000" help="30600">
<level>0</level>
<default></default>
<constraints>
@ -14,5 +14,26 @@
</setting>
</group>
</category>
<category id="timeshift" label="30002" help="30602">
<group id="1" label="30002">
<setting id="timeshiftEnabled" type="boolean" label="30003" help="30603">
<level>0</level>
<default>false</default>
<control type="toggle" />
</setting>
</group>
<group id="2" label="30005">
<setting id="ffmpegdirectSettings" type="action" label="30004" help="30604">
<level>0</level>
<data>Addon.OpenSettings(inputstream.ffmpegdirect)</data>
<dependencies>
<dependency type="enable" setting="timeshiftEnabled" operator="is">true</dependency>
</dependencies>
<control type="button" format="action">
<close>true</close>
</control>
</setting>
</group>
</category>
</section>
</settings>

View File

@ -10,8 +10,6 @@
#include "OctonetData.h"
#include "rtsp_client.hpp"
#include <json/json.h>
#include <kodi/Filesystem.h>
#include <kodi/General.h>
@ -23,10 +21,12 @@
#endif
OctonetData::OctonetData(const std::string& octonetAddress,
bool enableTimeshift,
const kodi::addon::IInstanceInfo& instance)
: kodi::addon::CInstancePVRClient(instance)
{
m_serverAddress = octonetAddress;
m_enableTimeshift = enableTimeshift;
m_channels.clear();
m_groups.clear();
m_lastEpgLoad = 0;
@ -273,6 +273,25 @@ PVR_ERROR OctonetData::GetChannels(bool radio, kodi::addon::PVRChannelsResultSet
return PVR_ERROR_NO_ERROR;
}
PVR_ERROR OctonetData::GetChannelStreamProperties(const kodi::addon::PVRChannel& channelinfo, std::vector<kodi::addon::PVRStreamProperty>& properties)
{
properties.emplace_back(PVR_STREAM_PROPERTY_INPUTSTREAM, "inputstream.ffmpegdirect");
properties.emplace_back("inputstream.ffmpegdirect.is_realtime_stream", "true");
properties.emplace_back("inputstream.ffmpegdirect.open_mode", "ffmpeg");
if (m_enableTimeshift)
{
// This property is required to support timeshifting for Radio channels
properties.emplace_back("inputstream-player", "videodefaultplayer");
properties.emplace_back("inputstream.ffmpegdirect.stream_mode", "timeshift");
}
properties.emplace_back(PVR_STREAM_PROPERTY_MIMETYPE, "video/x-mpegts");
properties.emplace_back(PVR_STREAM_PROPERTY_STREAMURL, GetUrl(channelinfo.GetUniqueId()));
kodi::Log(ADDON_LOG_INFO, "Playing channel - name: %s, url: %s, and using inputstream.ffmpegdirect", GetName(channelinfo.GetUniqueId()).c_str(), GetUrl(channelinfo.GetUniqueId()).c_str());
return PVR_ERROR_NO_ERROR;
}
PVR_ERROR OctonetData::GetEPGForChannel(int channelUid,
time_t start,
time_t end,
@ -426,20 +445,3 @@ OctonetGroup* OctonetData::FindGroup(const std::string& name)
return nullptr;
}
/* PVR stream handling */
/* entirely unused, as we use standard RTSP+TS mux, which can be handlded by
* Kodi core */
bool OctonetData::OpenLiveStream(const kodi::addon::PVRChannel& channelinfo)
{
return rtsp_open(GetName(channelinfo.GetUniqueId()), GetUrl(channelinfo.GetUniqueId()));
}
int OctonetData::ReadLiveStream(unsigned char* pBuffer, unsigned int iBufferSize)
{
return rtsp_read(pBuffer, iBufferSize);
}
void OctonetData::CloseLiveStream()
{
rtsp_close();
}

View File

@ -47,6 +47,7 @@ class ATTR_DLL_LOCAL OctonetData : public kodi::addon::CInstancePVRClient
{
public:
OctonetData(const std::string& octonetAddress,
bool enableTimeshift,
const kodi::addon::IInstanceInfo& instance);
~OctonetData() override;
@ -66,16 +67,13 @@ public:
PVR_ERROR GetChannelGroups(bool radio, kodi::addon::PVRChannelGroupsResultSet& results) override;
PVR_ERROR GetChannelGroupMembers(const kodi::addon::PVRChannelGroup& group,
kodi::addon::PVRChannelGroupMembersResultSet& results) override;
PVR_ERROR GetChannelStreamProperties(const kodi::addon::PVRChannel& channel, std::vector<kodi::addon::PVRStreamProperty>& properties) override;
PVR_ERROR GetEPGForChannel(int channelUid,
time_t start,
time_t end,
kodi::addon::PVREPGTagsResultSet& results) override;
bool OpenLiveStream(const kodi::addon::PVRChannel& channelinfo) override;
int ReadLiveStream(unsigned char* buffer, unsigned int size) override;
void CloseLiveStream() override;
protected:
const std::string& GetUrl(int id) const;
const std::string& GetName(int id) const;
@ -89,6 +87,7 @@ protected:
private:
std::string m_serverAddress;
bool m_enableTimeshift = false;
std::vector<OctonetChannel> m_channels;
std::vector<OctonetGroup> m_groups;

View File

@ -1,728 +0,0 @@
/*
* Copyright (C) 2005-2021 Team Kodi (https://kodi.tv)
*
* SPDX-License-Identifier: GPL-2.0-or-later
* See LICENSE.md for more information.
*/
#include "Socket.h"
#include <cstdio>
#include <kodi/General.h>
#include <string>
using namespace std;
namespace OCTO
{
/* Master defines for client control */
#define RECEIVE_TIMEOUT 6 //sec
Socket::Socket(const enum SocketFamily family,
const enum SocketDomain domain,
const enum SocketType type,
const enum SocketProtocol protocol)
{
m_sd = INVALID_SOCKET;
m_family = family;
m_domain = domain;
m_type = type;
m_protocol = protocol;
m_port = 0;
memset(&m_sockaddr, 0, sizeof(m_sockaddr));
}
Socket::Socket()
{
// Default constructor, default settings
m_sd = INVALID_SOCKET;
m_family = af_inet;
m_domain = pf_inet;
m_type = sock_stream;
m_protocol = tcp;
m_port = 0;
memset(&m_sockaddr, 0, sizeof(m_sockaddr));
}
Socket::~Socket()
{
close();
osCleanup();
}
bool Socket::setHostname(const std::string& host)
{
m_hostname = host;
return true;
}
bool Socket::close()
{
if (is_valid())
{
if (m_sd != SOCKET_ERROR)
closesocket(m_sd);
m_sd = INVALID_SOCKET;
return true;
}
return false;
}
bool Socket::create()
{
close();
if (!osInit())
{
return false;
}
return true;
}
bool Socket::bind(const unsigned short port)
{
if (is_valid())
{
close();
}
m_sd = socket(m_family, m_type, m_protocol);
m_port = port;
m_sockaddr.sin_family = (sa_family_t)m_family;
m_sockaddr.sin_addr.s_addr = INADDR_ANY; //listen to all
m_sockaddr.sin_port = htons(m_port);
int bind_return = ::bind(m_sd, (sockaddr*)(&m_sockaddr), sizeof(m_sockaddr));
if (bind_return == -1)
{
errormessage(getLastError(), "Socket::bind");
return false;
}
return true;
}
bool Socket::listen() const
{
if (!is_valid())
{
return false;
}
int listen_return = ::listen(m_sd, SOMAXCONN);
//This is defined as 5 in winsock.h, and 0x7FFFFFFF in winsock2.h.
//linux 128//MAXCONNECTIONS =1
if (listen_return == -1)
{
errormessage(getLastError(), "Socket::listen");
return false;
}
return true;
}
bool Socket::accept(Socket& new_socket) const
{
if (!is_valid())
{
return false;
}
socklen_t addr_length = sizeof(m_sockaddr);
new_socket.m_sd =
::accept(m_sd, const_cast<sockaddr*>((const sockaddr*)&m_sockaddr), &addr_length);
#ifdef TARGET_WINDOWS
if (new_socket.m_sd == INVALID_SOCKET)
#else
if (new_socket.m_sd <= 0)
#endif
{
errormessage(getLastError(), "Socket::accept");
return false;
}
return true;
}
int Socket::send(const std::string& data)
{
return Socket::send((const char*)data.c_str(), (const unsigned int)data.size());
}
int Socket::send(const char* data, const unsigned int len)
{
fd_set set_w, set_e;
struct timeval tv;
int result;
if (!is_valid())
{
return 0;
}
// fill with new data
tv.tv_sec = 0;
tv.tv_usec = 0;
FD_ZERO(&set_w);
FD_ZERO(&set_e);
FD_SET(m_sd, &set_w);
FD_SET(m_sd, &set_e);
result = select(FD_SETSIZE, &set_w, nullptr, &set_e, &tv);
if (result < 0)
{
kodi::Log(ADDON_LOG_ERROR, "Socket::send - select failed");
close();
return 0;
}
if (FD_ISSET(m_sd, &set_w))
{
kodi::Log(ADDON_LOG_ERROR, "Socket::send - failed to send data");
close();
return 0;
}
int status = ::send(m_sd, data, len, 0);
if (status == -1)
{
errormessage(getLastError(), "Socket::send");
kodi::Log(ADDON_LOG_ERROR, "Socket::send - failed to send data");
close();
return 0;
}
return status;
}
int Socket::sendto(const char* data, unsigned int size, bool sendcompletebuffer)
{
int sentbytes = 0;
int i;
do
{
i = ::sendto(m_sd, data, size, 0, (const struct sockaddr*)&m_sockaddr, sizeof(m_sockaddr));
if (i <= 0)
{
errormessage(getLastError(), "Socket::sendto");
osCleanup();
return i;
}
sentbytes += i;
} while ((sentbytes < (int)size) && (sendcompletebuffer == true));
return i;
}
int Socket::receive(std::string& data, unsigned int minpacketsize) const
{
char* buf = nullptr;
int status = 0;
if (!is_valid())
{
return 0;
}
buf = new char[minpacketsize + 1];
memset(buf, 0, minpacketsize + 1);
status = receive(buf, minpacketsize, minpacketsize);
data = buf;
delete[] buf;
return status;
}
//Receive until error or \n
bool Socket::ReadLine(string& line)
{
fd_set set_r, set_e;
timeval timeout;
int retries = 6;
char buffer[2048];
if (!is_valid())
return false;
while (true)
{
size_t pos1 = line.find("\r\n", 0);
if (pos1 != std::string::npos)
{
line.erase(pos1, string::npos);
return true;
}
timeout.tv_sec = RECEIVE_TIMEOUT;
timeout.tv_usec = 0;
// fill with new data
FD_ZERO(&set_r);
FD_ZERO(&set_e);
FD_SET(m_sd, &set_r);
FD_SET(m_sd, &set_e);
int result = select(FD_SETSIZE, &set_r, nullptr, &set_e, &timeout);
if (result < 0)
{
kodi::Log(ADDON_LOG_DEBUG, "%s: select failed", __func__);
errormessage(getLastError(), __func__);
close();
return false;
}
if (result == 0)
{
if (retries != 0)
{
kodi::Log(ADDON_LOG_DEBUG, "%s: timeout waiting for response, retrying... (%i)", __func__,
retries);
retries--;
continue;
}
else
{
kodi::Log(ADDON_LOG_DEBUG, "%s: timeout waiting for response. Aborting after 10 retries.",
__func__);
return false;
}
}
result = recv(m_sd, buffer, sizeof(buffer) - 1, 0);
if (result < 0)
{
kodi::Log(ADDON_LOG_DEBUG, "%s: recv failed", __func__);
errormessage(getLastError(), __func__);
close();
return false;
}
buffer[result] = 0;
line.append(buffer);
}
return true;
}
int Socket::receive(std::string& data) const
{
char buf[MAXRECV + 1];
int status = 0;
if (!is_valid())
{
return 0;
}
memset(buf, 0, MAXRECV + 1);
status = receive(buf, MAXRECV, 0);
data = buf;
return status;
}
int Socket::receive(char* data,
const unsigned int buffersize,
const unsigned int minpacketsize) const
{
unsigned int receivedsize = 0;
if (!is_valid())
{
return 0;
}
while ((receivedsize <= minpacketsize) && (receivedsize < buffersize))
{
int status = ::recv(m_sd, data + receivedsize, (buffersize - receivedsize), 0);
if (status == SOCKET_ERROR)
{
errormessage(getLastError(), "Socket::receive");
return status;
}
receivedsize += status;
}
return receivedsize;
}
int Socket::recvfrom(char* data,
const int buffersize,
struct sockaddr* from,
socklen_t* fromlen) const
{
int status = ::recvfrom(m_sd, data, buffersize, 0, from, fromlen);
return status;
}
bool Socket::connect(const std::string& host, const unsigned short port)
{
close();
if (!setHostname(host))
{
kodi::Log(ADDON_LOG_ERROR, "Socket::setHostname(%s) failed.\n", host.c_str());
return false;
}
m_port = port;
char strPort[15];
snprintf(strPort, 15, "%hu", port);
struct addrinfo hints;
struct addrinfo* result = nullptr;
struct addrinfo* address = nullptr;
memset(&hints, 0, sizeof(hints));
hints.ai_family = m_family;
hints.ai_socktype = m_type;
hints.ai_protocol = m_protocol;
int retval = getaddrinfo(host.c_str(), strPort, &hints, &result);
if (retval != 0)
{
errormessage(getLastError(), "Socket::connect");
return false;
}
for (address = result; address != nullptr; address = address->ai_next)
{
// Create the socket
m_sd = socket(address->ai_family, address->ai_socktype, address->ai_protocol);
if (m_sd == INVALID_SOCKET)
{
errormessage(getLastError(), "Socket::create");
continue;
}
int status = ::connect(m_sd, address->ai_addr, address->ai_addrlen);
if (status == SOCKET_ERROR)
{
close();
continue;
}
// We have a conection
break;
}
freeaddrinfo(result);
if (address == nullptr)
{
kodi::Log(ADDON_LOG_ERROR, "Socket::connect %s:%u\n", host.c_str(), port);
errormessage(getLastError(), "Socket::connect");
close();
return false;
}
return true;
}
bool Socket::reconnect()
{
if (is_valid())
{
return true;
}
return connect(m_hostname, m_port);
}
bool Socket::is_valid() const
{
return (m_sd != INVALID_SOCKET);
}
#if defined(TARGET_WINDOWS)
bool Socket::set_non_blocking(const bool b)
{
u_long iMode;
if (b)
iMode = 1; // enable non_blocking
else
iMode = 0; // disable non_blocking
if (ioctlsocket(m_sd, FIONBIO, &iMode) == -1)
{
kodi::Log(ADDON_LOG_ERROR, "Socket::set_non_blocking - Can't set socket condition to: %i",
iMode);
return false;
}
return true;
}
void Socket::errormessage(int errnum, const char* functionname) const
{
const char* errmsg = nullptr;
switch (errnum)
{
case WSANOTINITIALISED:
errmsg = "A successful WSAStartup call must occur before using this function.";
break;
case WSAENETDOWN:
errmsg = "The network subsystem or the associated service provider has failed";
break;
case WSA_NOT_ENOUGH_MEMORY:
errmsg = "Insufficient memory available";
break;
case WSA_INVALID_PARAMETER:
errmsg = "One or more parameters are invalid";
break;
case WSA_OPERATION_ABORTED:
errmsg = "Overlapped operation aborted";
break;
case WSAEINTR:
errmsg = "Interrupted function call";
break;
case WSAEBADF:
errmsg = "File handle is not valid";
break;
case WSAEACCES:
errmsg = "Permission denied";
break;
case WSAEFAULT:
errmsg = "Bad address";
break;
case WSAEINVAL:
errmsg = "Invalid argument";
break;
case WSAENOTSOCK:
errmsg = "Socket operation on nonsocket";
break;
case WSAEDESTADDRREQ:
errmsg = "Destination address required";
break;
case WSAEMSGSIZE:
errmsg = "Message too long";
break;
case WSAEPROTOTYPE:
errmsg = "Protocol wrong type for socket";
break;
case WSAENOPROTOOPT:
errmsg = "Bad protocol option";
break;
case WSAEPFNOSUPPORT:
errmsg = "Protocol family not supported";
break;
case WSAEAFNOSUPPORT:
errmsg = "Address family not supported by protocol family";
break;
case WSAEADDRINUSE:
errmsg = "Address already in use";
break;
case WSAECONNRESET:
errmsg = "Connection reset by peer";
break;
case WSAHOST_NOT_FOUND:
errmsg = "Authoritative answer host not found";
break;
case WSATRY_AGAIN:
errmsg = "Nonauthoritative host not found, or server failure";
break;
case WSAEISCONN:
errmsg = "Socket is already connected";
break;
case WSAETIMEDOUT:
errmsg = "Connection timed out";
break;
case WSAECONNREFUSED:
errmsg = "Connection refused";
break;
case WSANO_DATA:
errmsg = "Valid name, no data record of requested type";
break;
default:
errmsg = "WSA Error";
}
kodi::Log(ADDON_LOG_ERROR, "%s: (Winsock error=%i) %s\n", functionname, errnum, errmsg);
}
int Socket::getLastError() const
{
return WSAGetLastError();
}
int Socket::win_usage_count = 0; //Declared static in Socket class
bool Socket::osInit()
{
win_usage_count++;
// initialize winsock:
if (WSAStartup(MAKEWORD(2, 2), &m_wsaData) != 0)
{
return false;
}
WORD wVersionRequested = MAKEWORD(2, 2);
// check version
if (m_wsaData.wVersion != wVersionRequested)
{
return false;
}
return true;
}
void Socket::osCleanup()
{
win_usage_count--;
if (win_usage_count == 0)
{
WSACleanup();
}
}
#elif defined TARGET_LINUX || defined TARGET_DARWIN || defined TARGET_FREEBSD
bool Socket::set_non_blocking(const bool b)
{
int opts;
opts = fcntl(m_sd, F_GETFL);
if (opts < 0)
{
return false;
}
if (b)
opts = (opts | O_NONBLOCK);
else
opts = (opts & ~O_NONBLOCK);
if (fcntl(m_sd, F_SETFL, opts) == -1)
{
kodi::Log(ADDON_LOG_ERROR, "Socket::set_non_blocking - Can't set socket flags to: %i", opts);
return false;
}
return true;
}
void Socket::errormessage(int errnum, const char* functionname) const
{
const char* errmsg = nullptr;
switch (errnum)
{
case EAGAIN: //same as EWOULDBLOCK
errmsg = "EAGAIN: The socket is marked non-blocking and the requested operation would block";
break;
case EBADF:
errmsg = "EBADF: An invalid descriptor was specified";
break;
case ECONNRESET:
errmsg = "ECONNRESET: Connection reset by peer";
break;
case EDESTADDRREQ:
errmsg = "EDESTADDRREQ: The socket is not in connection mode and no peer address is set";
break;
case EFAULT:
errmsg = "EFAULT: An invalid userspace address was specified for a parameter";
break;
case EINTR:
errmsg = "EINTR: A signal occurred before data was transmitted";
break;
case EINVAL:
errmsg = "EINVAL: Invalid argument passed";
break;
case ENOTSOCK:
errmsg = "ENOTSOCK: The argument is not a valid socket";
break;
case EMSGSIZE:
errmsg = "EMSGSIZE: The socket requires that message be sent atomically, and the size of the "
"message to be sent made this impossible";
break;
case ENOBUFS:
errmsg = "ENOBUFS: The output queue for a network interface was full";
break;
case ENOMEM:
errmsg = "ENOMEM: No memory available";
break;
case EPIPE:
errmsg = "EPIPE: The local end has been shut down on a connection oriented socket";
break;
case EPROTONOSUPPORT:
errmsg = "EPROTONOSUPPORT: The protocol type or the specified protocol is not supported "
"within this domain";
break;
case EAFNOSUPPORT:
errmsg = "EAFNOSUPPORT: The implementation does not support the specified address family";
break;
case ENFILE:
errmsg = "ENFILE: Not enough kernel memory to allocate a new socket structure";
break;
case EMFILE:
errmsg = "EMFILE: Process file table overflow";
break;
case EACCES:
errmsg =
"EACCES: Permission to create a socket of the specified type and/or protocol is denied";
break;
case ECONNREFUSED:
errmsg = "ECONNREFUSED: A remote host refused to allow the network connection (typically "
"because it is not running the requested service)";
break;
case ENOTCONN:
errmsg = "ENOTCONN: The socket is associated with a connection-oriented protocol and has not "
"been connected";
break;
//case E:
// errmsg = "";
// break;
default:
break;
}
kodi::Log(ADDON_LOG_ERROR, "%s: (errno=%i) %s\n", functionname, errnum, errmsg);
}
int Socket::getLastError() const
{
return errno;
}
bool Socket::osInit()
{
// Not needed for Linux
return true;
}
void Socket::osCleanup()
{
// Not needed for Linux
}
#endif //TARGET_WINDOWS || TARGET_LINUX || TARGET_DARWIN || TARGET_FREEBSD
} //namespace OCTO

View File

@ -1,284 +0,0 @@
/*
* Copyright (C) 2005-2021 Team Kodi (https://kodi.tv)
*
* SPDX-License-Identifier: GPL-2.0-or-later
* See LICENSE.md for more information.
*/
#pragma once
//Include platform specific datatypes, header files, defines and constants:
#if defined TARGET_WINDOWS
#define WIN32_LEAN_AND_MEAN // Enable LEAN_AND_MEAN support
#pragma warning(disable : 4005) // Disable "warning C4005: '_WINSOCKAPI_' : macro redefinition"
#include <WS2tcpip.h>
#include <winsock2.h>
#pragma warning(default : 4005)
#include <windows.h>
#ifndef NI_MAXHOST
#define NI_MAXHOST 1025
#endif
#ifndef socklen_t
typedef int socklen_t;
#endif
#ifndef ipaddr_t
typedef unsigned long ipaddr_t;
#endif
#ifndef port_t
typedef unsigned short port_t;
#endif
#ifndef sa_family_t
#define sa_family_t ADDRESS_FAMILY
#endif
#elif defined TARGET_LINUX || defined TARGET_DARWIN || defined TARGET_FREEBSD
#ifdef SOCKADDR_IN
#undef SOCKADDR_IN
#endif
#include <arpa/inet.h> /* for inet_pton */
#include <errno.h>
#include <fcntl.h>
#include <netdb.h> /* for gethostbyname */
#include <netinet/in.h> /* for htons */
#include <sys/socket.h> /* for socket,connect */
#include <sys/types.h> /* for socket,connect */
#include <sys/un.h> /* for Unix socket */
#include <unistd.h> /* for read, write, close */
typedef int SOCKET;
typedef sockaddr SOCKADDR;
typedef sockaddr_in SOCKADDR_IN;
#ifndef INVALID_SOCKET
#define INVALID_SOCKET (-1)
#endif
#define SOCKET_ERROR (-1)
#define closesocket(sd) ::close(sd)
#else
#error Platform specific socket support is not yet available on this platform!
#endif
#include <string>
#include <vector>
namespace OCTO
{
#define MAXCONNECTIONS 1 ///< Maximum number of pending connections before "Connection refused"
#define MAXRECV 1500 ///< Maximum packet size
enum SocketFamily
{
af_unspec = AF_UNSPEC,
af_inet = AF_INET,
af_inet6 = AF_INET6
};
enum SocketDomain
{
#if defined TARGET_LINUX || defined TARGET_DARWIN || defined TARGET_FREEBSD
pf_unix = PF_UNIX,
pf_local = PF_LOCAL,
#endif
pf_inet = PF_INET
};
enum SocketType
{
sock_stream = SOCK_STREAM,
sock_dgram = SOCK_DGRAM
};
enum SocketProtocol
{
tcp = IPPROTO_TCP,
udp = IPPROTO_UDP
};
class Socket
{
public:
/*!
* An unconnected socket may be created directly on the local
* machine. The socket type (SOCK_STREAM, SOCK_DGRAM) and
* protocol may also be specified.
* If the socket cannot be created, an exception is thrown.
*
* \param family Socket family (IPv4 or IPv6)
* \param domain The domain parameter specifies a communications domain within which communication will take place;
* this selects the protocol family which should be used.
* \param type base type and protocol family of the socket.
* \param protocol specific protocol to apply.
*/
Socket(const enum SocketFamily family,
const enum SocketDomain domain,
const enum SocketType type,
const enum SocketProtocol protocol = tcp);
Socket(void);
virtual ~Socket();
//Socket settings
/*!
* Socket setFamily
* \param family Can be af_inet or af_inet6. Default: af_inet
*/
void setFamily(const enum SocketFamily family) { m_family = family; };
/*!
* Socket setDomain
* \param domain Can be pf_unix, pf_local, pf_inet or pf_inet6. Default: pf_inet
*/
void setDomain(const enum SocketDomain domain) { m_domain = domain; };
/*!
* Socket setType
* \param type Can be sock_stream or sock_dgram. Default: sock_stream.
*/
void setType(const enum SocketType type) { m_type = type; };
/*!
* Socket setProtocol
* \param protocol Can be tcp or udp. Default: tcp.
*/
void setProtocol(const enum SocketProtocol protocol) { m_protocol = protocol; };
/*!
* Socket setPort
* \param port port number for socket communication
*/
void setPort(const unsigned short port) { m_sockaddr.sin_port = htons(port); };
bool setHostname(const std::string& host);
// Server initialization
/*!
* Socket create
* Create a new socket
* \return True if succesful
*/
bool create();
/*!
* Socket close
* Close the socket
* \return True if succesful
*/
bool close();
/*!
* Socket bind
*/
bool bind(const unsigned short port);
bool listen() const;
bool accept(Socket& socket) const;
// Client initialization
bool connect(const std::string& host, const unsigned short port);
bool reconnect();
// Data Transmission
/*!
* Socket send function
*
* \param data Reference to a std::string with the data to transmit
* \return Number of bytes send or -1 in case of an error
*/
int send(const std::string& data);
/*!
* Socket send function
*
* \param data Pointer to a character array of size 'size' with the data to transmit
* \param size Length of the data to transmit
* \return Number of bytes send or -1 in case of an error
*/
int send(const char* data, const unsigned int size);
/*!
* Socket sendto function
*
* \param data Reference to a std::string with the data to transmit
* \param size Length of the data to transmit
* \param sendcompletebuffer If 'true': do not return until the complete buffer is transmitted
* \return Number of bytes send or -1 in case of an error
*/
int sendto(const char* data, unsigned int size, bool sendcompletebuffer = false);
// Data Receive
/*!
* Socket receive function
*
* \param data Reference to a std::string for storage of the received data.
* \param minpacketsize The minimum number of bytes that should be received before returning from this function
* \return Number of bytes received or SOCKET_ERROR
*/
int receive(std::string& data, unsigned int minpacketsize) const;
/*!
* Socket receive function
*
* \param data Reference to a std::string for storage of the received data.
* \return Number of bytes received or SOCKET_ERROR
*/
int receive(std::string& data) const;
/*!
* Socket receive function
*
* \param data Pointer to a character array of size buffersize. Used to store the received data.
* \param buffersize Size of the 'data' buffer
* \param minpacketsize Specifies the minimum number of bytes that need to be received before returning
* \return Number of bytes received or SOCKET_ERROR
*/
int receive(char* data, const unsigned int buffersize, const unsigned int minpacketsize) const;
/*!
* Socket recvfrom function
*
* \param data Pointer to a character array of size buffersize. Used to store the received data.
* \param buffersize Size of the 'data' buffer
* \param from Optional: pointer to a sockaddr struct that will get the address from which the data is received
* \param fromlen Optional, only required if 'from' is given: length of from struct
* \return Number of bytes received or SOCKET_ERROR
*/
int recvfrom(char* data,
const int buffersize,
struct sockaddr* from = nullptr,
socklen_t* fromlen = nullptr) const;
bool set_non_blocking(const bool);
bool ReadLine(std::string& line);
bool is_valid() const;
private:
SOCKET m_sd; ///< Socket Descriptor
SOCKADDR_IN m_sockaddr; ///< Socket Address
//struct addrinfo* m_addrinfo; ///< Socket address info
std::string m_hostname; ///< Hostname
unsigned short m_port; ///< Port number
enum SocketFamily m_family; ///< Socket Address Family
enum SocketProtocol m_protocol; ///< Socket Protocol
enum SocketType m_type; ///< Socket Type
enum SocketDomain m_domain; ///< Socket domain
#ifdef TARGET_WINDOWS
WSADATA m_wsaData; ///< Windows Socket data
static int
win_usage_count; ///< Internal Windows usage counter used to prevent a global WSACleanup when more than one Socket object is used
#endif
void errormessage(int errornum, const char* functionname = nullptr) const;
int getLastError(void) const;
bool osInit();
void osCleanup();
};
} //namespace OCTO

View File

@ -29,8 +29,9 @@ ADDON_STATUS COctonetAddon::CreateInstance(const kodi::addon::IInstanceInfo& ins
/* IP or hostname of the octonet to be connected to */
std::string octonetAddress = kodi::addon::GetSettingString("octonetAddress");
bool enableTimeshift = kodi::addon::GetSettingBoolean("timeshiftEnabled");
OctonetData* usedInstance = new OctonetData(octonetAddress, instance);
OctonetData* usedInstance = new OctonetData(octonetAddress, enableTimeshift, instance);
hdl = usedInstance;
m_usedInstances.emplace(instance.GetID(), usedInstance);

View File

@ -1,548 +0,0 @@
/*
* Copyright (C) 2005-2021 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 <algorithm>
#include <cctype>
#include <cstring>
#include <iterator>
#include <sstream>
#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<string>& 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<char*>(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<const rtcp_app*>(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<string> elems;
split_string(app_data, ';', elems);
if (elems.size() != 4)
{
return;
}
vector<string> 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);
}
}

View File

@ -1,16 +0,0 @@
/*
* Copyright (C) 2005-2021 Team Kodi (https://kodi.tv)
*
* SPDX-License-Identifier: GPL-2.0-or-later
* See LICENSE.md for more information.
*/
#pragma once
#include <kodi/addon-instance/pvr/Channels.h>
#include <string>
bool rtsp_open(const std::string& name, const std::string& url_str);
void rtsp_close();
int rtsp_read(void* buf, unsigned buf_size);
void rtsp_fill_signal_status(kodi::addon::PVRSignalStatus& signal_status);