diff --git a/lib/dvb/Makefile.inc b/lib/dvb/Makefile.inc index 1c7dc2968a4..8b5f02011a5 100644 --- a/lib/dvb/Makefile.inc +++ b/lib/dvb/Makefile.inc @@ -6,6 +6,7 @@ dvb_libenigma_dvb_a_SOURCES = \ dvb/crc32.cpp \ dvb/csaengine.cpp \ dvb/csasession.cpp \ + dvb/cwhandler.cpp \ dvb/db.cpp \ dvb/decoder.cpp \ dvb/demux.cpp \ @@ -49,6 +50,7 @@ dvbinclude_HEADERS = \ dvb/crc32.h \ dvb/csaengine.h \ dvb/csasession.h \ + dvb/cwhandler.h \ dvb/db.h \ dvb/decoder.h \ dvb/demux.h \ diff --git a/lib/dvb/cahandler.cpp b/lib/dvb/cahandler.cpp index 0ede5393614..ad2841632b6 100644 --- a/lib/dvb/cahandler.cpp +++ b/lib/dvb/cahandler.cpp @@ -12,9 +12,11 @@ #include #include +#include #include +#include -// Cache serviceId per DVB service reference to ensure OSCam always sees the same ID +// Cache serviceId per DVB service reference to ensure softcam always sees the same ID // This prevents CW delivery issues when switching between StreamRelay and Live-TV static std::map s_serviceId_cache; @@ -46,10 +48,10 @@ void ePMTClient::dataAvailable() { if (bytesAvailable() < 6) return; receivedLength = readBlock((char*)receivedHeader, 1); - // check OSCam protocol version -> version 3 starts with 0xA5 + // check softcam protocol version -> version 3 starts with 0xA5 if ((m_protocolVersion == 3 || m_protocolVersion == -1) && receivedHeader[0] == 0xA5) { - // OSCam protocol 3: read 4 byte msgid + first byte of tag + // Softcam protocol 3: read 4 byte msgid + first byte of tag readBlock((char*)receivedHeader, 5); receivedTag[0] = receivedHeader[4]; } @@ -164,7 +166,7 @@ bool ePMTClient::processCaSetDescrPacket() auto it = parent->m_service_caid.find(serviceId); if (it != parent->m_service_caid.end()) caid = it->second; - parent->receivedCw(service, descr.parity, (const char*)descr.cw, caid); + parent->receivedCw(service, descr.parity, (const char*)descr.cw, caid, serviceId); } return true; } @@ -365,15 +367,23 @@ eDVBCAHandler::~eDVBCAHandler() void eDVBCAHandler::newConnection(int socket) { - ePMTClient *client = new ePMTClient(this, socket); + // Route through eDVBCWHandler proxy for MainLoop-independent CW delivery + int client_fd = eDVBCWHandler::getInstance()->addConnection(socket); + if (client_fd < 0) + { + eWarning("[eDVBCAHandler] eDVBCWHandler proxy failed, rejecting connection"); + ::close(socket); + return; + } + ePMTClient *client = new ePMTClient(this, client_fd); clients.push_back(client); /* First distribute current CAPMTs in legacy format (works for all clients), * then send CLIENT_INFO to initiate Protocol-3 handshake. - * - OSCam: receives legacy CAPMTs, then CLIENT_INFO, responds with - * SERVER_INFO -> Protocol 3 for all subsequent CAPMTs. - * - CCcam: receives legacy CAPMTs (works!), then CLIENT_INFO causes - * disconnect, but CAPMTs were already delivered. */ + * - Protocol-3 softcams: receive legacy CAPMTs, then CLIENT_INFO, + * respond with SERVER_INFO -> Protocol 3 for all subsequent CAPMTs. + * - Legacy softcams: receive legacy CAPMTs (works!), then CLIENT_INFO + * causes disconnect, but CAPMTs were already delivered. */ distributeCAPMT(); client->sendClientInfo(); } @@ -462,21 +472,15 @@ int eDVBCAHandler::registerService(const eServiceReferenceDVB &ref, int adapter, if (cacheit != pmtCache.end() && cacheit->second) { // If streamserver was active and we're adding a different type (e.g. Live-TV), - // send CA PMT update immediately so OSCam knows about the new demux config + // we need to force the softcam to restart descrambling so it resends the CW. + // A simple LIST_UPDATE is not enough because the softcam's demux is already + // running and would just "continue processing" without resending the CW. + // We DEFER the restart to handlePMT() so the new CSA session is already + // activated and its engine registered with CWHandler when the CW arrives. if (had_streamserver && servicetype != 7 && servicetype != 8) { - caservice->resetBuildHash(); - if (caservice->buildCAPMT(cacheit->second) >= 0) - { - for (ePtrList::iterator client_it = clients.begin(); client_it != clients.end(); ++client_it) - { - if (client_it->state() == eSocket::Connection) - { - caservice->writeCAPMTObject(*client_it, LIST_UPDATE); - } - } - eDebug("[eDVBCAService] sent early CA PMT update (streamserver active, new type %d registering)", servicetype); - } + caservice->m_force_cw_send = true; + eDebug("[eDVBCAService] deferred softcam restart (streamserver->live, type %d)", servicetype); } else { @@ -576,12 +580,7 @@ void eDVBCAHandler::serviceGone() { if (!services.size()) { - eDebug("[DVBCAHandler] no more services"); - for (ePtrList::iterator it = clients.begin(); it != clients.end(); ) - { - delete *it; - it = clients.erase(it); - } + eDebug("[DVBCAHandler] no more services (keeping %zu client connections)", clients.size()); if (pmtCache.size() > 500) { pmtCache.clear(); @@ -600,14 +599,28 @@ void eDVBCAHandler::distributeCAPMT() { if (client_it->state() == eSocket::Connection) { - unsigned char list_management = LIST_FIRST; - for (CAServiceMap::iterator it = services.begin(); it != services.end(); ) + /* + * Collect services that have a valid CAPMT (buildCAPMT was called). + * Services with m_version == -1 have never had their PMT processed, + * so their m_capmt buffer contains uninitialized heap data. + * Sending that would corrupt the protocol stream. + */ + std::vector ready_services; + for (CAServiceMap::iterator it = services.begin(); it != services.end(); ++it) { - eDVBCAService *current = it->second; - ++it; - if (it == services.end()) list_management |= LIST_LAST; - current->writeCAPMTObject(*client_it, list_management); - list_management = LIST_MORE; + if (it->second->getCAPMTVersion() >= 0) + { + ready_services.push_back(it->second); + } + } + + if (ready_services.empty()) continue; + + for (size_t idx = 0; idx < ready_services.size(); ++idx) + { + unsigned char list_management = (idx == 0) ? LIST_FIRST : LIST_MORE; + if (idx == ready_services.size() - 1) list_management |= LIST_LAST; + ready_services[idx]->writeCAPMTObject(*client_it, list_management); } } } @@ -623,7 +636,28 @@ void eDVBCAHandler::processPMTForService(eDVBCAService *service, eTablesendCAPMT(); - if (isUpdate) + if (service->m_force_cw_send) + { + /* + * SR→Live transition: force the softcam to restart descrambling. + * Send CMD_NOT_SELECTED to stop the running demux, then LIST_ADD + * with CMD_OK_DESCRAMBLING to re-add. This makes the softcam treat + * it as a new service and immediately resend the CW from cache. + * At this point the new CSA session is already activated and its + * engine registered with CWHandler, so the CW won't be lost. + */ + service->m_force_cw_send = false; + for (ePtrList::iterator client_it = clients.begin(); client_it != clients.end(); ++client_it) + { + if (client_it->state() == eSocket::Connection) + { + service->writeCAPMTObject(*client_it, LIST_UPDATE, CMD_NOT_SELECTED); + service->writeCAPMTObject(*client_it, LIST_ADD, CMD_OK_DESCRAMBLING); + } + } + eDebug("[eDVBCAService] forced softcam restart for SR->Live transition"); + } + else if (isUpdate) { /* * this is a PMT update for an existing service, so we should @@ -703,9 +737,10 @@ int eDVBCAHandler::getServiceReference(eServiceReferenceDVB &service, uint32_t s } eDVBCAService::eDVBCAService(const eServiceReferenceDVB &service, uint32_t id) - : eUnixDomainSocket(eApp), m_service(service), m_adapter(0), m_service_type_mask(0), m_prev_build_hash(0), m_crc32(0), m_id(id), m_version(-1), m_retryTimer(eTimer::create(eApp)) + : eUnixDomainSocket(eApp), m_service(service), m_adapter(0), m_service_type_mask(0), m_prev_build_hash(0), m_crc32(0), m_id(id), m_version(-1), m_retryTimer(eTimer::create(eApp)), m_force_cw_send(false) { memset(m_used_demux, 0xFF, sizeof(m_used_demux)); + memset(m_capmt, 0, sizeof(m_capmt)); CONNECT(connectionClosed_, eDVBCAService::connectionLost); CONNECT(m_retryTimer->timeout, eDVBCAService::sendCAPMT); } @@ -847,7 +882,7 @@ int eDVBCAService::buildCAPMT(eTable *ptr) if ( i != ptr->getSections().end() ) { crc = (*i)->getCrc32(); - if (build_hash == m_prev_build_hash && crc == m_crc32) + if (build_hash == m_prev_build_hash && crc == m_crc32 && !m_force_cw_send) { eDebug("[eDVBCAService] don't build/send the same CA PMT twice"); return -1; @@ -1148,13 +1183,14 @@ void eDVBCAService::sendCAPMT() } } -int eDVBCAService::writeCAPMTObject(eSocket *socket, int list_management) +int eDVBCAService::writeCAPMTObject(eSocket *socket, int list_management, int cmd_id) { int wp = 0; + int lenbytes = 0; if (m_capmt[8] & 0x80) { int i=0; - int lenbytes = m_capmt[8] & ~0x80; + lenbytes = m_capmt[8] & ~0x80; while(i < lenbytes) wp = (wp << 8) | m_capmt[9 + i++]; wp += 4; @@ -1167,17 +1203,20 @@ int eDVBCAService::writeCAPMTObject(eSocket *socket, int list_management) wp += 4; if (list_management >= 0) m_capmt[9] = (unsigned char)list_management; } + // cmd_id is 6 bytes after list_management: list_mgmt(1) + program_number(2) + version(1) + prog_info_len(2) + if (cmd_id >= 0) m_capmt[9 + lenbytes + 6] = (unsigned char)cmd_id; return socket->writeBlock((const char*)(m_capmt + 5), wp); // skip extra header } -int eDVBCAService::writeCAPMTObject(ePMTClient *client, int list_management) +int eDVBCAService::writeCAPMTObject(ePMTClient *client, int list_management, int cmd_id) { int wp = 0; + int lenbytes = 0; if (m_capmt[8] & 0x80) { int i=0; - int lenbytes = m_capmt[8] & ~0x80; + lenbytes = m_capmt[8] & ~0x80; while(i < lenbytes) wp = (wp << 8) | m_capmt[9 + i++]; wp += 4; @@ -1190,6 +1229,8 @@ int eDVBCAService::writeCAPMTObject(ePMTClient *client, int list_management) wp += 4; if (list_management >= 0) m_capmt[9] = (unsigned char)list_management; } + // cmd_id is 6 bytes after list_management: list_mgmt(1) + program_number(2) + version(1) + prog_info_len(2) + if (cmd_id >= 0) m_capmt[9 + lenbytes + 6] = (unsigned char)cmd_id; return client->writeCAPMTObject((const char*)m_capmt, wp); } diff --git a/lib/dvb/cahandler.h b/lib/dvb/cahandler.h index 80cd8d499ba..11b9b3523d6 100644 --- a/lib/dvb/cahandler.h +++ b/lib/dvb/cahandler.h @@ -97,7 +97,7 @@ class ePMTClient : public eUnixDomainSocket eDVBCAHandler *parent; void connectionLost(); void dataAvailable(); - // OSCam Protocol 3 handlers + // Softcam Protocol 3 handlers bool processCaSetDescrPacket(); bool processServerInfoPacket(); bool processEcmInfoPacket(); @@ -109,6 +109,7 @@ class ePMTClient : public eUnixDomainSocket class eDVBCAService: public eUnixDomainSocket { + friend class eDVBCAHandler; eServiceReferenceDVB m_service; uint8_t m_used_demux[32]; uint8_t m_adapter; @@ -119,6 +120,7 @@ class eDVBCAService: public eUnixDomainSocket int m_version; unsigned char m_capmt[2048]; ePtr m_retryTimer; + bool m_force_cw_send; // force softcam CW resend on next handlePMT (SR→Live) public: eDVBCAService(const eServiceReferenceDVB &service, uint32_t id); ~eDVBCAService(); @@ -136,8 +138,8 @@ class eDVBCAService: public eUnixDomainSocket uint32_t getServiceTypeMask() const; void resetBuildHash() { m_prev_build_hash = 0; m_crc32 = 0; } void sendCAPMT(); - int writeCAPMTObject(eSocket *socket, int list_management = -1); - int writeCAPMTObject(ePMTClient *client, int list_management = -1); + int writeCAPMTObject(eSocket *socket, int list_management = -1, int cmd_id = -1); + int writeCAPMTObject(ePMTClient *client, int list_management = -1, int cmd_id = -1); int buildCAPMT(eTable *ptr); int buildCAPMT(ePtr &dvbservice); void connectionLost(); @@ -155,7 +157,7 @@ class iCryptoInfo : public iObject iCryptoInfo(); ~iCryptoInfo(); #endif - sigc::signal receivedCw; // service, parity, cw, caid + sigc::signal receivedCw; // service, parity, cw, caid, serviceId }; SWIG_TEMPLATE_TYPEDEF(ePtr, iCryptoInfoPtr); @@ -172,7 +174,7 @@ DECLARE_REF(eDVBCAHandler); ePtrList clients; ePtr serviceLeft; std::map > > pmtCache; - std::map m_service_caid; // serviceId -> CAID (from OSCam ECM_INFO) + std::map m_service_caid; // serviceId -> CAID (from softcam ECM_INFO) uint32_t serviceIdCounter; void newConnection(int socket); diff --git a/lib/dvb/csaengine.cpp b/lib/dvb/csaengine.cpp index f4def6a103d..c0179e43f81 100644 --- a/lib/dvb/csaengine.cpp +++ b/lib/dvb/csaengine.cpp @@ -53,7 +53,7 @@ bool csa_load_library() if (!g_csa_api.handle) { - eWarning("[CSAEngine] libdvbcsa not found (dlopen failed)"); + eWarning("[eDVBCSAEngine] libdvbcsa not found (dlopen failed)"); return false; } @@ -71,7 +71,7 @@ bool csa_load_library() !g_csa_api.batch_size || !g_csa_api.decrypt) { - eWarning("[CSAEngine] %s loaded but missing required symbols", loaded_name); + eWarning("[eDVBCSAEngine] %s loaded but missing required symbols", loaded_name); dlclose(g_csa_api.handle); g_csa_api.handle = 0; g_csa_api.available = false; @@ -80,7 +80,7 @@ bool csa_load_library() g_csa_api.available = true; - eDebug("[CSAEngine] %s successfully loaded, software CSA enabled", loaded_name); + eDebug("[eDVBCSAEngine] %s successfully loaded, software CSA enabled", loaded_name); return true; } @@ -113,7 +113,7 @@ bool eDVBCSAEngine::init() { if (!csa_load_library()) { - eWarning("[CSAEngine] init: csa_load_library failed"); + eWarning("[eDVBCSAEngine] init: csa_load_library failed"); return false; } @@ -123,7 +123,7 @@ bool eDVBCSAEngine::init() if (!m_key_even || !m_key_odd) { - eWarning("[CSAEngine] init: key_alloc failed"); + eWarning("[eDVBCSAEngine] init: key_alloc failed"); return false; } @@ -131,7 +131,7 @@ bool eDVBCSAEngine::init() m_batch_even.resize(m_batch_size + 1); m_batch_odd.resize(m_batch_size + 1); - eDebug("[CSAEngine] init: batch_size=%d", m_batch_size); + eDebug("[eDVBCSAEngine] init: batch_size=%d", m_batch_size); return true; } @@ -206,7 +206,7 @@ void eDVBCSAEngine::setKey(int parity, uint8_t ecm_mode, const uint8_t* cw) if (!g_csa_api.available || !g_csa_api.key_set_ecm) return; - CSA_LOG("[CSAEngine] setKey: parity=%d ecm_mode=%u CW=%02X %02X %02X %02X %02X %02X %02X %02X", + CSA_LOG("[eDVBCSAEngine] setKey: parity=%d ecm_mode=%u CW=%02X %02X %02X %02X %02X %02X %02X %02X", parity, ecm_mode, BYTE_HEX(cw[0]), BYTE_HEX(cw[1]), BYTE_HEX(cw[2]), BYTE_HEX(cw[3]), BYTE_HEX(cw[4]), BYTE_HEX(cw[5]), BYTE_HEX(cw[6]), BYTE_HEX(cw[7])); @@ -229,7 +229,7 @@ void eDVBCSAEngine::setKey(int parity, uint8_t ecm_mode, const uint8_t* cw) uint8_t table_used = g_csa_api.get_ecm_table(); if (table_used != last_logged_table) { - eDebug("[CSAEngine] libdvbcsa using table 0x%02X", table_used); + eDebug("[eDVBCSAEngine] libdvbcsa using table 0x%02X", table_used); last_logged_table = table_used; } } @@ -288,7 +288,7 @@ void eDVBCSAEngine::descramble(unsigned char* packets, int len) dvbcsa_bs_batch_s* pcks_even = m_batch_even.data(); dvbcsa_bs_batch_s* pcks_odd = m_batch_odd.data(); - CSA_LOG("[CSAEngine] descramble: len=%d batch_size=%d", len, m_batch_size); + CSA_LOG("[eDVBCSAEngine] descramble: len=%d batch_size=%d", len, m_batch_size); const bool even_set = m_key_even_set; const bool odd_set = m_key_odd_set; @@ -299,7 +299,7 @@ void eDVBCSAEngine::descramble(unsigned char* packets, int len) if (!isPacketValid(pkt)) { - CSA_LOG("[CSAEngine] decrypt sync error at offset=%d", i); + CSA_LOG("[eDVBCSAEngine] decrypt sync error at offset=%d", i); return; } @@ -349,7 +349,7 @@ void eDVBCSAEngine::descramble(unsigned char* packets, int len) pcks_even[even_cnt].data = NULL; if (even_set) g_csa_api.decrypt(m_key_even, pcks_even, 184); - CSA_LOG("[CSAEngine] decrypt even batch (%d)", m_batch_size); + CSA_LOG("[eDVBCSAEngine] decrypt even batch (%d)", m_batch_size); even_cnt = 0; } @@ -359,7 +359,7 @@ void eDVBCSAEngine::descramble(unsigned char* packets, int len) pcks_odd[odd_cnt].data = NULL; if (odd_set) g_csa_api.decrypt(m_key_odd, pcks_odd, 184); - CSA_LOG("[CSAEngine] decrypt odd batch (%d)", m_batch_size); + CSA_LOG("[eDVBCSAEngine] decrypt odd batch (%d)", m_batch_size); odd_cnt = 0; } @@ -372,7 +372,7 @@ void eDVBCSAEngine::descramble(unsigned char* packets, int len) pcks_even[even_cnt].data = NULL; if (even_set) g_csa_api.decrypt(m_key_even, pcks_even, 184); - CSA_LOG("[CSAEngine] decrypt remaining even packets=%d", even_cnt); + CSA_LOG("[eDVBCSAEngine] decrypt remaining even packets=%d", even_cnt); } // flush remaining odd @@ -381,8 +381,8 @@ void eDVBCSAEngine::descramble(unsigned char* packets, int len) pcks_odd[odd_cnt].data = NULL; if (odd_set) g_csa_api.decrypt(m_key_odd, pcks_odd, 184); - CSA_LOG("[CSAEngine] decrypt remaining odd packets=%d", odd_cnt); + CSA_LOG("[eDVBCSAEngine] decrypt remaining odd packets=%d", odd_cnt); } - CSA_LOG("[CSAEngine] descramble done"); + CSA_LOG("[eDVBCSAEngine] descramble done"); } diff --git a/lib/dvb/csasession.cpp b/lib/dvb/csasession.cpp index edf34615fcf..69d5159f17f 100644 --- a/lib/dvb/csasession.cpp +++ b/lib/dvb/csasession.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #ifdef DREAMNEXTGEN @@ -65,13 +66,20 @@ eDVBCSASession::eDVBCSASession(const eServiceReferenceDVB& ref) , m_ecm_mode_detected(false) , m_ecm_analyzed(false) , m_csa_alt(false) + , m_cw_service_id(0) + , m_cw_handler_registered(false) + , m_first_cw_signaled(false) + , m_pending_cw{} { - eDebug("[CSASession] Created for service %s", ref.toString().c_str()); + eDebug("[eDVBCSASession] Created for service %s", ref.toString().c_str()); } eDVBCSASession::~eDVBCSASession() { - eDebug("[CSASession] Destroyed for service %s", m_service_ref.toString().c_str()); + eDebug("[eDVBCSASession] Destroyed for service %s", m_service_ref.toString().c_str()); + + if (m_cw_handler_registered) + eDVBCWHandler::getInstance()->unregisterEngine(m_cw_service_id, m_engine); stopECMMonitor(); @@ -87,7 +95,7 @@ bool eDVBCSASession::init() m_engine = new eDVBCSAEngine(); if (!m_engine->init()) { - eWarning("[CSASession] Failed to initialize CSA engine"); + eWarning("[eDVBCSASession] Failed to initialize CSA engine"); m_engine = nullptr; return false; } @@ -96,13 +104,13 @@ bool eDVBCSASession::init() eDVBCAHandler* ca = eDVBCAHandler::getInstance(); if (!ca) { - eWarning("[CSASession] eDVBCAHandler not available"); + eWarning("[eDVBCSASession] eDVBCAHandler not available"); return false; } CONNECT(ca->receivedCw, eDVBCSASession::onCwReceived); - eDebug("[CSASession] Initialized - CSA-ALT detection via ECM analysis"); + eDebug("[eDVBCSASession] Initialized - CSA-ALT detection via ECM analysis"); return true; } @@ -124,7 +132,7 @@ void eDVBCSASession::startECMMonitor(iDVBDemux *demux, uint16_t ecm_pid, uint16_ if (cache_it != s_csa_cache.end() && cache_it->second.valid) { const ServiceCsaInfo& info = cache_it->second; - eDebug("[CSASession] ECM Monitor: Found cached info - CSA-ALT=%d, ecm_mode=0x%02X", + eDebug("[eDVBCSASession] ECM Monitor: Found cached info - CSA-ALT=%d, ecm_mode=0x%02X", info.is_csa_alt, info.ecm_mode); // Pre-load ecm_mode from cache @@ -133,7 +141,7 @@ void eDVBCSASession::startECMMonitor(iDVBDemux *demux, uint16_t ecm_pid, uint16_ if (info.is_csa_alt && !m_active) { - eDebug("[CSASession] ECM Monitor: Activating from cache (CSA-ALT)"); + eDebug("[eDVBCSASession] ECM Monitor: Activating from cache (CSA-ALT)"); m_ecm_analyzed = true; m_csa_alt = true; setActive(true); @@ -144,7 +152,7 @@ void eDVBCSASession::startECMMonitor(iDVBDemux *demux, uint16_t ecm_pid, uint16_ ePtr reader; if (demux->createSectionReader(eApp, reader) != 0 || !reader) { - eWarning("[CSASession] ECM Monitor: Failed to create section reader"); + eWarning("[eDVBCSASession] ECM Monitor: Failed to create section reader"); return; } @@ -163,12 +171,12 @@ void eDVBCSASession::startECMMonitor(iDVBDemux *demux, uint16_t ecm_pid, uint16_ if (m_ecm_reader->start(mask) != 0) { - eWarning("[CSASession] ECM Monitor: Failed to start filter on PID %d", ecm_pid); + eWarning("[eDVBCSASession] ECM Monitor: Failed to start filter on PID %d", ecm_pid); m_ecm_reader = nullptr; return; } - eDebug("[CSASession] ECM Monitor started on PID %d", ecm_pid); + eDebug("[eDVBCSASession] ECM Monitor started on PID %d", ecm_pid); } void eDVBCSASession::stopECMMonitor() @@ -177,7 +185,7 @@ void eDVBCSASession::stopECMMonitor() { m_ecm_reader->stop(); m_ecm_reader = nullptr; - eDebug("[CSASession] ECM Monitor stopped"); + eDebug("[eDVBCSASession] ECM Monitor stopped"); } m_ecm_conn = nullptr; } @@ -212,7 +220,7 @@ void eDVBCSASession::ecmDataReceived(const uint8_t *data) { bool is_csa_alt = detect_csa_alt_from_ecm(data, m_caid); - eDebug("[CSASession] ECM received (PMT): caid=0x%04X, ecm[2]=0x%02X, ecm[4]=0x%02X, ecm_mode=0x%02X, CSA-ALT=%d", + eDebug("[eDVBCSASession] ECM received (PMT): caid=0x%04X, ecm[2]=0x%02X, ecm[4]=0x%02X, ecm_mode=0x%02X, CSA-ALT=%d", m_caid, data[2], data[4], new_ecm_mode, is_csa_alt); // Update unified cache @@ -224,7 +232,7 @@ void eDVBCSASession::ecmDataReceived(const uint8_t *data) if (is_csa_alt) { - eDebug("[CSASession] CSA-ALT detected from ECM! Activating software descrambling"); + eDebug("[eDVBCSASession] CSA-ALT detected from ECM! Activating software descrambling"); if (!m_active) { setActive(true); @@ -232,7 +240,7 @@ void eDVBCSASession::ecmDataReceived(const uint8_t *data) } else { - eDebug("[CSASession] ECM analyzed: Not CSA-ALT, hardware descrambling will be used"); + eDebug("[eDVBCSASession] ECM analyzed: Not CSA-ALT, hardware descrambling will be used"); stopECMMonitor(); } } @@ -254,17 +262,32 @@ void eDVBCSASession::setActive(bool active) if (m_active) { - eDebug("[CSASession] ACTIVATED - CSA-ALT detected, SW-Descrambling active"); + eDebug("[eDVBCSASession] ACTIVATED - CSA-ALT detected, SW-Descrambling active"); #ifdef DREAMNEXTGEN eAlsaOutput::setSoftDecoderActive(1); #endif + // Replay buffered CW that arrived before activation + if (m_pending_cw.valid) + { + eDebug("[eDVBCSASession] Replaying buffered CW: parity=%d", m_pending_cw.parity); + onCwReceived(m_service_ref, m_pending_cw.parity, m_pending_cw.cw, + m_pending_cw.caid, m_pending_cw.serviceId); + m_pending_cw.valid = false; + } } else { - eDebug("[CSASession] DEACTIVATED - HW-Descrambling (passthrough)"); + eDebug("[eDVBCSASession] DEACTIVATED - HW-Descrambling (passthrough)"); #ifdef DREAMNEXTGEN eAlsaOutput::setSoftDecoderActive(0); #endif + if (m_cw_handler_registered) + { + eDVBCWHandler::getInstance()->unregisterEngine(m_cw_service_id, m_engine); + m_cw_handler_registered = false; + } + m_first_cw_signaled = false; + m_pending_cw.valid = false; if (m_engine) m_engine->clearKeys(); // Reset ECM analysis state @@ -278,24 +301,33 @@ void eDVBCSASession::setActive(bool active) activated(m_active); } -void eDVBCSASession::onCwReceived(eServiceReferenceDVB ref, int parity, const char* cw, uint16_t caid) +void eDVBCSASession::onCwReceived(eServiceReferenceDVB ref, int parity, const char* cw, uint16_t caid, uint32_t serviceId) { // Only for our service if (!matchesService(ref)) return; - eDebug("[CSASession] onCwReceived: parity=%d for service %s", parity, ref.toString().c_str()); + if (!m_cw_handler_registered) + eDebug("[eDVBCSASession] onCwReceived: parity=%d for service %s", parity, ref.toString().c_str()); - // Only process CWs when active + // Buffer CW if session not yet active (activation pending on ECM analysis) if (!m_active) + { + if (cw) + { + m_pending_cw.parity = parity; + memcpy(m_pending_cw.cw, cw, 8); + m_pending_cw.caid = caid; + m_pending_cw.serviceId = serviceId; + m_pending_cw.valid = true; + eDebug("[eDVBCSASession] CW buffered (session not yet active): parity=%d", parity); + } return; + } if (!cw || !m_engine) return; - // Check if this is the first CW (for signaling) - bool had_any_key = m_engine->hasAnyKey(); - // Get ecm_mode: prefer detected, then cached, then default uint8_t ecm_mode; const char *source = "default"; @@ -319,20 +351,35 @@ void eDVBCSASession::onCwReceived(eServiceReferenceDVB ref, int parity, const ch ecm_mode = DEFAULT_ECM_MODE; } } - eDebug("[CSASession] ECM Mode 0x%02X (%s, tail: %02X %02X %02X %02X)", - ecm_mode, source, m_ecm_tail[0], m_ecm_tail[1], m_ecm_tail[2], m_ecm_tail[3]); - const uint8_t* cw_bytes = (const uint8_t*)cw; - m_engine->setKey(parity, ecm_mode, cw_bytes); - char caid_str[20] = ""; - if (caid != 0) - snprintf(caid_str, sizeof(caid_str), "caid=0x%04X, ", caid); - eDebug("[CSASession] CW set: %sparity=%d, hasEven=%d, hasOdd=%d, CW=%02X", - caid_str, parity, m_engine->hasEvenKey(), m_engine->hasOddKey(), cw_bytes[0]); - - // If this is the first CW, signal to listeners - if (!had_any_key && m_engine->hasAnyKey()) + + // Register/update eDVBCWHandler - it handles setKey() directly from its thread + if (!m_cw_handler_registered) + { + m_cw_service_id = serviceId; + eDVBCWHandler::getInstance()->registerEngine(serviceId, m_engine, ecm_mode); + m_cw_handler_registered = true; + // The first CW packet was already intercepted by eDVBCWHandler BEFORE this + // registration, so the engine missed it. Apply it now to avoid waiting + // for the next CW cycle. + m_engine->setKey(parity, ecm_mode, (const uint8_t*)cw); + const uint8_t* cw_bytes = (const uint8_t*)cw; + eDebug("[eDVBCSASession] CW set: caid=0x%04X, parity=%d, hasEven=%d, hasOdd=%d, CW=%02X", + caid, parity, m_engine->hasEvenKey(), m_engine->hasOddKey(), cw_bytes[0]); + } + else + { + eDVBCWHandler::getInstance()->updateEcmMode(m_cw_service_id, m_engine, ecm_mode); + } + + if (m_ecm_mode != ecm_mode) + eDebug("[eDVBCSASession] ECM Mode 0x%02X (%s, tail: %02X %02X %02X %02X)", + ecm_mode, source, m_ecm_tail[0], m_ecm_tail[1], m_ecm_tail[2], m_ecm_tail[3]); + + // Signal firstCwReceived once (for SoftDecoder start) + if (!m_first_cw_signaled && m_engine->hasAnyKey()) { - eDebug("[CSASession] First CW received - signaling"); + eDebug("[eDVBCSASession] First CW received - signaling"); + m_first_cw_signaled = true; firstCwReceived(); } } diff --git a/lib/dvb/csasession.h b/lib/dvb/csasession.h index 65c6541aabf..00bed6144a0 100644 --- a/lib/dvb/csasession.h +++ b/lib/dvb/csasession.h @@ -12,7 +12,7 @@ class eDVBCAHandler; /** * eDVBCSASession - CW-Management per Service with ECM-based CSA-ALT Detection * - * - Receives CWs from OSCam via eDVBCAHandler signals + * - Receives CWs from softcam via eDVBCAHandler signals * - Filters by service reference * - Monitors ECM to detect CSA-ALT and ecm_mode * - ACTIVATES ITSELF when CSA-ALT is detected from ECM @@ -58,7 +58,7 @@ class eDVBCSASession : public iServiceScrambled, public sigc::trackable * @param caid CA System ID for CSA-ALT detection * * Reads ecm[len-1] and extracts lower nibble for ecm_mode. - * Also detects CSA-ALT from ECM using OSCam's select_csa_alt() logic: + * Also detects CSA-ALT from ECM using softcam's select_csa_alt() logic: * - CAID is VideoGuard (0x09xx) * - ecm[4] != 0 * - (ecm[2] - ecm[4]) == 4 @@ -114,11 +114,29 @@ class eDVBCSASession : public iServiceScrambled, public sigc::trackable ePtr m_cw_connection; // CW Handler (called from eDVBCAHandler signal) - void onCwReceived(eServiceReferenceDVB ref, int parity, const char* cw, uint16_t caid); + void onCwReceived(eServiceReferenceDVB ref, int parity, const char* cw, uint16_t caid, uint32_t serviceId); // Helper bool matchesService(const eServiceReferenceDVB& ref) const; void setActive(bool active); + + // eDVBCWHandler registration + uint32_t m_cw_service_id; // Softcam's serviceId (set on first CW) + bool m_cw_handler_registered; // true once registered with eDVBCWHandler + bool m_first_cw_signaled; // true once firstCwReceived signal was emitted + + // CW buffer for CWs arriving before activation + // When a CW arrives while m_active is false, we store it here. + // On setActive(true), the buffered CW is replayed immediately, + // avoiding a multi-second wait for the next CW cycle. + struct PendingCw { + int parity; + char cw[8]; + uint16_t caid; + uint32_t serviceId; + bool valid; + }; + PendingCw m_pending_cw; }; #endif // __dvbcsasession_h diff --git a/lib/dvb/cwhandler.cpp b/lib/dvb/cwhandler.cpp new file mode 100644 index 00000000000..4d1d74e7660 --- /dev/null +++ b/lib/dvb/cwhandler.cpp @@ -0,0 +1,422 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +eDVBCWHandler* eDVBCWHandler::instance = nullptr; + +eDVBCWHandler* eDVBCWHandler::getInstance() +{ + if (!instance) + instance = new eDVBCWHandler(); + return instance; +} + +eDVBCWHandler::eDVBCWHandler() + : m_running(false) + , m_thread(0) +{ + m_wake_pipe[0] = -1; + m_wake_pipe[1] = -1; + + if (pipe2(m_wake_pipe, O_NONBLOCK | O_CLOEXEC) != 0) + { + eWarning("[eDVBCWHandler] Failed to create wake pipe: %m"); + return; + } + + m_running = true; + if (pthread_create(&m_thread, nullptr, threadFunc, this) != 0) + { + eWarning("[eDVBCWHandler] Failed to create thread: %m"); + m_running = false; + return; + } + + eDebug("[eDVBCWHandler] Started"); +} + +eDVBCWHandler::~eDVBCWHandler() +{ + m_running = false; + + // Wake up poll + if (m_wake_pipe[1] >= 0) + { + char c = 'q'; + ::write(m_wake_pipe[1], &c, 1); + } + + if (m_thread) + pthread_join(m_thread, nullptr); + + // Close all connections + { + std::lock_guard lock(m_connections_mutex); + for (auto& conn : m_connections) + { + if (conn.softcam_fd >= 0) ::close(conn.softcam_fd); + if (conn.proxy_fd >= 0) ::close(conn.proxy_fd); + // client_fd is owned by ePMTClient + } + m_connections.clear(); + } + + if (m_wake_pipe[0] >= 0) ::close(m_wake_pipe[0]); + if (m_wake_pipe[1] >= 0) ::close(m_wake_pipe[1]); + + eDebug("[eDVBCWHandler] Stopped"); +} + +void eDVBCWHandler::registerEngine(uint32_t serviceId, eDVBCSAEngine* engine, uint8_t ecm_mode) +{ + std::lock_guard lock(m_targets_mutex); + + // Check if this exact engine is already registered for this serviceId + auto range = m_targets.equal_range(serviceId); + for (auto it = range.first; it != range.second; ++it) + { + if (it->second.engine == engine) + { + // Already registered - just update ecm_mode + it->second.ecm_mode = ecm_mode; + eDebug("[eDVBCWHandler] Registered engine for serviceId %u, ecm_mode=0x%02X", serviceId, ecm_mode); + return; + } + } + + // New engine for this serviceId (e.g. Timeshift alongside Live) + CwTarget target; + target.engine = engine; + target.ecm_mode = ecm_mode; + m_targets.insert({serviceId, target}); + eDebug("[eDVBCWHandler] Registered engine for serviceId %u, ecm_mode=0x%02X", serviceId, ecm_mode); +} + +void eDVBCWHandler::unregisterEngine(uint32_t serviceId, eDVBCSAEngine* engine) +{ + std::lock_guard lock(m_targets_mutex); + auto range = m_targets.equal_range(serviceId); + for (auto it = range.first; it != range.second; ++it) + { + if (it->second.engine == engine) + { + m_targets.erase(it); + eDebug("[eDVBCWHandler] Unregistered engine for serviceId %u", serviceId); + return; + } + } + eDebug("[eDVBCWHandler] Unregister: no engine found for serviceId %u", serviceId); +} + +void eDVBCWHandler::updateEcmMode(uint32_t serviceId, eDVBCSAEngine* engine, uint8_t ecm_mode) +{ + std::lock_guard lock(m_targets_mutex); + auto range = m_targets.equal_range(serviceId); + for (auto it = range.first; it != range.second; ++it) + { + if (it->second.engine == engine) + { + if (it->second.ecm_mode != ecm_mode) + { + eDebug("[eDVBCWHandler] Updated ecm_mode for serviceId %u to 0x%02X", serviceId, ecm_mode); + it->second.ecm_mode = ecm_mode; + } + return; + } + } +} + +int eDVBCWHandler::addConnection(int softcam_fd) +{ + int pair[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, pair) != 0) + { + eWarning("[eDVBCWHandler] socketpair failed: %m"); + return -1; + } + + // Set softcam_fd to non-blocking for our poll loop + int flags = fcntl(softcam_fd, F_GETFL, 0); + if (flags >= 0) + fcntl(softcam_fd, F_SETFL, flags | O_NONBLOCK); + + Connection conn; + conn.softcam_fd = softcam_fd; + conn.proxy_fd = pair[0]; // our end + conn.client_fd = pair[1]; // ePMTClient's end + + { + std::lock_guard lock(m_connections_mutex); + m_connections.push_back(conn); + } + + // Wake up poll loop to pick up new connection + char c = 'w'; + ::write(m_wake_pipe[1], &c, 1); + + eDebug("[eDVBCWHandler] Added connection: softcam_fd=%d, proxy_fd=%d, client_fd=%d", softcam_fd, pair[0], pair[1]); + return pair[1]; // return the fd for ePMTClient +} + +void eDVBCWHandler::removeConnection(int client_fd) +{ + std::lock_guard lock(m_connections_mutex); + for (auto it = m_connections.begin(); it != m_connections.end(); ++it) + { + if (it->client_fd == client_fd) + { + eDebug("[eDVBCWHandler] Removing connection: softcam_fd=%d, client_fd=%d", it->softcam_fd, client_fd); + if (it->softcam_fd >= 0) ::close(it->softcam_fd); + if (it->proxy_fd >= 0) ::close(it->proxy_fd); + m_connections.erase(it); + + // Wake up poll loop + char c = 'w'; + ::write(m_wake_pipe[1], &c, 1); + return; + } + } +} + +void* eDVBCWHandler::threadFunc(void* arg) +{ + static_cast(arg)->threadLoop(); + return nullptr; +} + +void eDVBCWHandler::threadLoop() +{ + char buf[4096]; + + while (m_running) + { + // Build poll fd list + std::vector pfds; + std::vector conns_snapshot; + + { + std::lock_guard lock(m_connections_mutex); + conns_snapshot = m_connections; + } + + // Wake pipe + struct pollfd wpfd; + wpfd.fd = m_wake_pipe[0]; + wpfd.events = POLLIN; + wpfd.revents = 0; + pfds.push_back(wpfd); + + // For each connection: poll softcam_fd and proxy_fd + for (const auto& conn : conns_snapshot) + { + struct pollfd pfd; + + pfd.fd = conn.softcam_fd; + pfd.events = POLLIN; + pfd.revents = 0; + pfds.push_back(pfd); + + pfd.fd = conn.proxy_fd; + pfd.events = POLLIN; + pfd.revents = 0; + pfds.push_back(pfd); + } + + int ret = poll(pfds.data(), pfds.size(), 1000); + if (ret < 0) + { + if (errno == EINTR) + continue; + eWarning("[eDVBCWHandler] poll error: %m"); + break; + } + + if (ret == 0) + continue; + + // Check wake pipe + if (pfds[0].revents & POLLIN) + { + // Drain pipe + while (::read(m_wake_pipe[0], buf, sizeof(buf)) > 0); + } + + // Process connections + for (size_t i = 0; i < conns_snapshot.size(); i++) + { + int softcam_idx = 1 + i * 2; + int proxy_idx = 2 + i * 2; + const Connection& conn = conns_snapshot[i]; + + // Softcam → proxy_fd (and intercept CWs) + if (pfds[softcam_idx].revents & POLLIN) + { + ssize_t n = ::read(conn.softcam_fd, buf, sizeof(buf)); + if (n > 0) + { + // Intercept CW packets before forwarding + processCwFromRawPacket((const uint8_t*)buf, n); + // Forward everything to ePMTClient via socketpair + ssize_t written = 0; + while (written < n) + { + ssize_t w = ::write(conn.proxy_fd, buf + written, n - written); + if (w < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + // Socketpair buffer full (MainLoop not reading) - drop this chunk + // CWs were already intercepted and setKey() called + eDebug("[eDVBCWHandler] Socketpair buffer full, dropping %zd bytes (CWs already processed)", n - written); + break; + } + break; // Error + } + written += w; + } + } + } + + // Handle softcam disconnect + if (pfds[softcam_idx].revents & (POLLHUP | POLLERR)) + { + if (!(pfds[softcam_idx].revents & POLLIN)) // Only if no data pending + { + eDebug("[eDVBCWHandler] Softcam disconnected on fd %d", conn.softcam_fd); + // Close proxy_fd so ePMTClient gets connectionLost + std::lock_guard lock(m_connections_mutex); + for (auto it = m_connections.begin(); it != m_connections.end(); ++it) + { + if (it->softcam_fd == conn.softcam_fd) + { + ::close(it->softcam_fd); + ::close(it->proxy_fd); + m_connections.erase(it); + break; + } + } + continue; + } + } + + // proxy_fd → softcam (CAPMT writes from ePMTClient) + if (pfds[proxy_idx].revents & POLLIN) + { + ssize_t n = ::read(conn.proxy_fd, buf, sizeof(buf)); + if (n > 0) + { + ssize_t written = 0; + while (written < n) + { + ssize_t w = ::write(conn.softcam_fd, buf + written, n - written); + if (w < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + // Brief spin-wait for softcam to catch up (small CAPMT packets) + usleep(1000); + continue; + } + break; // Error + } + written += w; + } + } + } + + // Handle ePMTClient disconnect + if (pfds[proxy_idx].revents & (POLLHUP | POLLERR)) + { + if (!(pfds[proxy_idx].revents & POLLIN)) + { + eDebug("[eDVBCWHandler] ePMTClient disconnected on proxy_fd %d", conn.proxy_fd); + std::lock_guard lock(m_connections_mutex); + for (auto it = m_connections.begin(); it != m_connections.end(); ++it) + { + if (it->proxy_fd == conn.proxy_fd) + { + ::close(it->softcam_fd); + ::close(it->proxy_fd); + m_connections.erase(it); + break; + } + } + } + } + } + } +} + +void eDVBCWHandler::processCwFromRawPacket(const uint8_t* data, int len) +{ + /* + * Softcam Protocol 3 packet format: + * [0xA5] [msgid: 4 bytes] [tag: 4 bytes] [data...] + * + * CA_SET_DESCR tag: 0x40 0x10 0x6F 0x86 + * CA_SET_DESCR data: 1 byte padding + ca_descr_t (16 bytes) = 17 bytes + * + * Total CA_SET_DESCR packet: 1 + 4 + 4 + 17 = 26 bytes + * + * ca_descr_t: { uint32_t index, uint32_t parity, uint8_t cw[8] } + * + * Note: No residual buffering needed - Unix domain sockets deliver + * small packets atomically, and ePMTClient handles reassembly for + * all packet types via its state machine on the socketpair end. + */ + static const uint8_t CW_TAG[] = { 0x40, 0x10, 0x6F, 0x86 }; + static const int CW_PACKET_SIZE = 26; // 0xA5 + 4 msgid + 4 tag + 17 data + + int pos = 0; + while (pos <= len - CW_PACKET_SIZE) + { + // Find next 0xA5 frame start + if (data[pos] != 0xA5) + { + pos++; + continue; + } + + // Check if this is a CA_SET_DESCR packet + // Tag is at offset 5 (after 0xA5 + 4 byte msgid) + if (memcmp(&data[pos + 5], CW_TAG, 4) != 0) + { + // Not a CW packet - skip past this frame start + pos++; + continue; + } + + // Extract serviceId from msgid (bytes 1-4, big-endian) + uint32_t serviceId; + memcpy(&serviceId, &data[pos + 1], 4); + serviceId = ntohl(serviceId); + + // Extract ca_descr_t from data (offset 10 = 1+4+4+1 padding) + ca_descr_t descr; + memcpy(&descr, &data[pos + 10], sizeof(ca_descr_t)); + descr.index = ntohl(descr.index); + descr.parity = ntohl(descr.parity); + + // Deliver CW to all engines registered for this serviceId (Live + Timeshift) + { + std::lock_guard lock(m_targets_mutex); + auto range = m_targets.equal_range(serviceId); + for (auto it = range.first; it != range.second; ++it) + { + it->second.engine->setKey(descr.parity, it->second.ecm_mode, descr.cw); + eDebug("[eDVBCWHandler] CW set: parity=%d, hasEven=%d, hasOdd=%d, CW=%02X", + descr.parity, it->second.engine->hasEvenKey(), it->second.engine->hasOddKey(), descr.cw[0]); + } + } + + pos += CW_PACKET_SIZE; + } +} diff --git a/lib/dvb/cwhandler.h b/lib/dvb/cwhandler.h new file mode 100644 index 00000000000..caa6d727e43 --- /dev/null +++ b/lib/dvb/cwhandler.h @@ -0,0 +1,104 @@ +#ifndef __dvbcwhandler_h +#define __dvbcwhandler_h + +#include +#include +#include +#include // for m_running +#include +#include +#include + +/** + * eDVBCWHandler - Socketpair proxy for MainLoop-independent CW delivery + * + * Sits between softcam socket and ePMTClient: + * - Takes ownership of the original softcam fd + * - Creates a socketpair, gives one end to ePMTClient + * - Runs a poll() loop in a dedicated thread + * - Forwards ALL data bidirectionally (ePMTClient sees no difference) + * - Additionally intercepts CA_SET_DESCR packets and calls setKey() directly + * + * This ensures CW delivery continues even when the MainLoop is blocked. + * The MainLoop still receives all packets (including CWs) for signal handling, + * but setKey() is ONLY called from this thread, never from the MainLoop. + */ +class eDVBCWHandler +{ +public: + static eDVBCWHandler* getInstance(); + + /** + * Register a CSA engine for direct CW delivery + * Called from CSASession::onCwReceived() on first CW (MainLoop context) + * @param serviceId Softcam's internal service ID (from CA_SET_DESCR msgid) + * @param engine The CSA engine to set keys on + * @param ecm_mode Current ecm_mode for key setting + */ + void registerEngine(uint32_t serviceId, eDVBCSAEngine* engine, uint8_t ecm_mode); + + /** + * Unregister a specific engine (called from CSASession destructor) + * Uses engine pointer to identify which registration to remove, + * so Live and Timeshift sessions don't interfere with each other. + */ + void unregisterEngine(uint32_t serviceId, eDVBCSAEngine* engine); + + /** + * Update ecm_mode for a specific registered engine + * Called from CSASession when ecm_mode changes + */ + void updateEcmMode(uint32_t serviceId, eDVBCSAEngine* engine, uint8_t ecm_mode); + + /** + * Take ownership of a softcam connection fd + * Creates socketpair, starts proxying + * @param softcam_fd The accepted socket fd from the softcam + * @return The fd for ePMTClient to use (socketpair end), or -1 on error + */ + int addConnection(int softcam_fd); + + /** + * Remove a connection (called when ePMTClient disconnects) + * @param client_fd The fd that was given to ePMTClient + */ + void removeConnection(int client_fd); + +private: + eDVBCWHandler(); + ~eDVBCWHandler(); + + static eDVBCWHandler* instance; + + // Engine registry (all access protected by m_targets_mutex) + // Multiple engines can be registered per serviceId (e.g. Live + Timeshift) + struct CwTarget { + eDVBCSAEngine* engine; + uint8_t ecm_mode; + }; + std::multimap m_targets; + std::mutex m_targets_mutex; + + // Connection tracking + struct Connection { + int softcam_fd; // Original softcam socket + int proxy_fd; // Our end of socketpair (proxy_fd ↔ client_fd) + int client_fd; // ePMTClient's end of socketpair + }; + std::vector m_connections; + std::mutex m_connections_mutex; + + // Pipe for waking up poll() when connections change + int m_wake_pipe[2]; + + // Thread + std::atomic m_running; + pthread_t m_thread; + static void* threadFunc(void* arg); + void threadLoop(); + + // Protocol parsing for CW interception + void processCwFromRawPacket(const uint8_t* data, int len); +}; + +#endif // __dvbcwhandler_h diff --git a/lib/dvb/pmt.cpp b/lib/dvb/pmt.cpp index 7357f19ada5..1190dfd41a9 100644 --- a/lib/dvb/pmt.cpp +++ b/lib/dvb/pmt.cpp @@ -203,6 +203,8 @@ void eDVBServicePMTHandler::PMTready(int error) { eDVBCIInterfaces::getInstance()->recheckPMTHandlers(); eDVBCIInterfaces::getInstance()->gotPMT(this); + if (isCiConnected()) + serviceEvent(eventCIConnected); } } if (m_ca_servicePtr) diff --git a/lib/dvb/pmt.h b/lib/dvb/pmt.h index 56a9ff39140..274c498e677 100644 --- a/lib/dvb/pmt.h +++ b/lib/dvb/pmt.h @@ -145,6 +145,7 @@ class eDVBServicePMTHandler: public eDVBPMTParser eventStartPvrDescramble, // start PVR Descramble Convert eventChannelAllocated, eventStreamCorrupt, + eventCIConnected, // a CI slot was assigned to this service after recheckPMTHandlers }; #ifndef SWIG sigc::signal serviceEvent; diff --git a/lib/service/servicedvb.cpp b/lib/service/servicedvb.cpp index 535b8945fbe..8de97551e1a 100644 --- a/lib/service/servicedvb.cpp +++ b/lib/service/servicedvb.cpp @@ -1384,6 +1384,14 @@ void eDVBServicePlay::serviceEvent(int event) case eDVBServicePMTHandler::eventHBBTVInfo: m_event((iPlayableService*)this, evHBBTVInfo); break; + case eDVBServicePMTHandler::eventCIConnected: + if (m_csa_session && m_csa_session->isActive()) + { + eDebug("[eDVBServicePlay] CI module connected - deactivating SoftCSA to save resources"); + m_csa_session->stopECMMonitor(); + m_csa_session->forceDeactivate(); + } + break; } } @@ -4440,12 +4448,13 @@ void eDVBServicePlay::onSessionActivated(bool active) eDebug("[eDVBServicePlay] SoftDecoder takeover complete"); - // Notify listeners (skin converters) that service info has changed (IsSoftCSA icon display) - m_event((iPlayableService*)this, evUpdatedInfo); + // Connect decoder-ready signal: SoftDecoder fires this after decoder PLAY, + // when video info is actually queryable. We defer evUpdatedInfo until then + // to avoid the skin querying -1 values before the decoder exists. + m_soft_decoder->m_decoder_ready.connect( + sigc::mem_fun(*this, &eDVBServicePlay::onSoftDecoderReady)); - // Reset video info flag - a second evUpdatedInfo will be sent when first video event arrives - // This is needed because some skins query video resolution only on evUpdatedInfo - // and the decoder hasn't analyzed any frames yet at this point + // Reset video info flag - will be set on first video size event from decoder m_soft_decoder_video_info_valid = false; } else if (!active && m_soft_decoder) @@ -4460,6 +4469,12 @@ void eDVBServicePlay::onSessionActivated(bool active) } } +void eDVBServicePlay::onSoftDecoderReady() +{ + eDebug("[eDVBServicePlay] SoftDecoder decoder ready - notifying skin"); + m_event((iPlayableService*)this, evUpdatedInfo); +} + void eDVBServicePlay::onSoftDecoderAudioPidSelected(int pid) { // SoftDecoder selected an audio track - update our tracking variable diff --git a/lib/service/servicedvb.h b/lib/service/servicedvb.h index 7bb7b772260..307eca5ef31 100644 --- a/lib/service/servicedvb.h +++ b/lib/service/servicedvb.h @@ -355,6 +355,7 @@ class eDVBServicePlay : public eDVBServiceBase, // Software descrambling virtual void setupSpeculativeDescrambling(); void onSessionActivated(bool active); + void onSoftDecoderReady(); void onSoftDecoderAudioPidSelected(int pid); void cleanupSoftwareDescrambling(); diff --git a/lib/service/servicedvbsoftdecoder.cpp b/lib/service/servicedvbsoftdecoder.cpp index 542b2bf13ab..4fce1e37fb4 100644 --- a/lib/service/servicedvbsoftdecoder.cpp +++ b/lib/service/servicedvbsoftdecoder.cpp @@ -22,8 +22,9 @@ eDVBSoftDecoder::eDVBSoftDecoder(eDVBServicePMTHandler& source_handler, , m_stall_count(0) , m_stream_stalled(false) , m_paused(false) + , m_last_health_check(0) { - eDebug("[SoftDecoder] Created for decoder %d", decoder_index); + eDebug("[eDVBSoftDecoder] Created for decoder %d", decoder_index); } eDVBSoftDecoder::~eDVBSoftDecoder() @@ -37,7 +38,7 @@ eDVBSoftDecoder::~eDVBSoftDecoder() m_first_cw_conn.disconnect(); stop(); - eDebug("[SoftDecoder] Destroyed"); + eDebug("[eDVBSoftDecoder] Destroyed"); } void eDVBSoftDecoder::setSession(ePtr session) @@ -60,14 +61,14 @@ void eDVBSoftDecoder::setSession(ePtr session) void eDVBSoftDecoder::onSessionActivated(bool active) { - eDebug("[SoftDecoder] Session activated: %d", active); + eDebug("[eDVBSoftDecoder] Session activated: %d", active); // Note: Don't start here automatically! // eDVBServicePlay::onSessionActivated will call start() after stopping // the old hardware decoder to ensure correct ordering. if (!active && m_running) { - eDebug("[SoftDecoder] Session deactivated - stopping decoder"); + eDebug("[eDVBSoftDecoder] Session deactivated - stopping decoder"); stop(); } } @@ -77,7 +78,7 @@ void eDVBSoftDecoder::onFirstCwReceived() if (m_decoder_started) return; // Already started - eDebug("[SoftDecoder] First CW received - starting decoder with DVR wait"); + eDebug("[eDVBSoftDecoder] First CW received - starting decoder with DVR wait"); // Stop timer if (m_start_timer) @@ -95,7 +96,7 @@ void eDVBSoftDecoder::onWaitForFirstDataTimeout() if (m_decoder_started) return; // Already started - eWarning("[SoftDecoder] CW timeout - starting decoder with DVR wait anyway"); + eWarning("[eDVBSoftDecoder] CW timeout - starting decoder with DVR wait anyway"); // Disconnect signal if (m_first_cw_conn.connected()) @@ -112,21 +113,21 @@ void eDVBSoftDecoder::startDecoderWithDvrWait() // Safety check: m_record must exist if (!m_record) { - eWarning("[SoftDecoder] startDecoderWithDvrWait: m_record is NULL!"); + eWarning("[eDVBSoftDecoder] startDecoderWithDvrWait: m_record is NULL!"); return; } // Wait for DVR data (blocking) int wait_timeout = eSimpleConfig::getInt("config.softcsa.waitForDataTimeout", 800); - eDebug("[SoftDecoder] Waiting for DVR data (timeout=%dms)", wait_timeout); + eDebug("[eDVBSoftDecoder] Waiting for DVR data (timeout=%dms)", wait_timeout); if (!m_record->waitForFirstData(wait_timeout)) { - eWarning("[SoftDecoder] DVR timeout - starting decoder anyway"); + eWarning("[eDVBSoftDecoder] DVR timeout - starting decoder anyway"); } // Start decoder - eDebug("[SoftDecoder] Starting decoder"); + eDebug("[eDVBSoftDecoder] Starting decoder"); updatePids(true); m_decoder_started = true; @@ -139,6 +140,7 @@ void eDVBSoftDecoder::startDecoderWithDvrWait() m_stall_count = 0; m_stream_stalled = false; m_paused = false; + m_last_health_check = 0; m_health_timer->start(500, false); } @@ -147,7 +149,7 @@ int eDVBSoftDecoder::start() if (m_running) return 0; - eDebug("[SoftDecoder] Starting"); + eDebug("[eDVBSoftDecoder] Starting"); // Connect to source PMT handler for program info updates (e.g. new audio tracks) m_source_event_conn = m_source_handler.serviceEvent.connect( @@ -156,7 +158,7 @@ int eDVBSoftDecoder::start() int ret = setupRecorder(); if (ret < 0) { - eWarning("[SoftDecoder] setupRecorder failed"); + eWarning("[eDVBSoftDecoder] setupRecorder failed"); m_source_event_conn.disconnect(); return ret; } @@ -171,7 +173,7 @@ void eDVBSoftDecoder::stop() if (!m_running) return; - eDebug("[SoftDecoder] Stopping"); + eDebug("[eDVBSoftDecoder] Stopping"); m_stopping = true; // Stop timers and disconnect signals @@ -196,7 +198,7 @@ void eDVBSoftDecoder::stop() // allowing the thread to exit cleanly. if (m_dvr_fd >= 0) { - eDebug("[SoftDecoder] Closing DVR fd %d (before stopping thread)", m_dvr_fd); + eDebug("[eDVBSoftDecoder] Closing DVR fd %d (before stopping thread)", m_dvr_fd); ::close(m_dvr_fd); m_dvr_fd = -1; } @@ -205,7 +207,7 @@ void eDVBSoftDecoder::stop() // Must stop before setDescrambler(nullptr) to prevent race condition if (m_record) { - eDebug("[SoftDecoder] Stopping recorder thread"); + eDebug("[eDVBSoftDecoder] Stopping recorder thread"); m_record->stop(); m_record->setDescrambler(nullptr); m_record = nullptr; @@ -214,14 +216,14 @@ void eDVBSoftDecoder::stop() // Release decode demux if (m_decode_demux) { - eDebug("[SoftDecoder] Releasing decode demux"); + eDebug("[eDVBSoftDecoder] Releasing decode demux"); m_decode_demux = nullptr; } // Stop decoder - release video/audio devices if (m_decoder) { - eDebug("[SoftDecoder] Stopping decoder"); + eDebug("[eDVBSoftDecoder] Stopping decoder"); m_decoder->pause(); m_decoder->setVideoPID(-1, -1); m_decoder->setAudioPID(-1, -1); @@ -230,7 +232,7 @@ void eDVBSoftDecoder::stop() } // Free PVR handler last - eDebug("[SoftDecoder] Freeing PVR handler"); + eDebug("[eDVBSoftDecoder] Freeing PVR handler"); m_pvr_handler.free(); m_pids_active.clear(); @@ -240,26 +242,27 @@ void eDVBSoftDecoder::stop() m_stall_count = 0; m_stream_stalled = false; m_paused = false; - eDebug("[SoftDecoder] Stop complete"); + m_last_health_check = 0; + eDebug("[eDVBSoftDecoder] Stop complete"); } int eDVBSoftDecoder::setupRecorder() { - eDebug("[SoftDecoder] setupRecorder"); + eDebug("[eDVBSoftDecoder] setupRecorder"); if (!m_record) { ePtr demux; if (m_source_handler.getDataDemux(demux)) { - eDebug("[SoftDecoder] NO DEMUX available"); + eDebug("[eDVBSoftDecoder] NO DEMUX available"); return -1; } // Debug: Show data demux ID uint8_t data_demux_id = 0; demux->getCADemuxID(data_demux_id); - eDebug("[SoftDecoder] Data demux ID: %d (reads from tuner)", data_demux_id); + eDebug("[eDVBSoftDecoder] Data demux ID: %d (reads from tuner)", data_demux_id); // Use streaming=false to get ScrambledThread (supports descrambling) // sync_mode is configurable via GUI: @@ -267,24 +270,24 @@ int eDVBSoftDecoder::setupRecorder() // 1 - "Synchronous": force sync (poll + write) int sync_mode_cfg = eSimpleConfig::getInt("config.softcsa.syncMode", 0); bool sync_mode = (sync_mode_cfg == 1); // 1 = Synchronous forced - eDebug("[SoftDecoder] Using %s mode (config=%d)", sync_mode ? "synchronous" : "automatic", sync_mode_cfg); + eDebug("[eDVBSoftDecoder] Using %s mode (config=%d)", sync_mode ? "synchronous" : "automatic", sync_mode_cfg); demux->createTSRecorder(m_record, 188, false, sync_mode); if (!m_record) { - eDebug("[SoftDecoder] no ts recorder available."); + eDebug("[eDVBSoftDecoder] no ts recorder available."); return -1; } // Allocate separate PVR channel for decode demux (critical!) // This ensures we have a different demux for PVR playback m_pvr_handler.allocatePVRChannel(); - eDebug("[SoftDecoder] PVR channel allocated"); + eDebug("[eDVBSoftDecoder] PVR channel allocated"); // Get decode demux from PVR handler (NOT from source_handler!) m_pvr_handler.getDecodeDemux(m_decode_demux); if (!m_decode_demux) { - eWarning("[SoftDecoder] No decode demux from PVR handler - aborting!"); + eWarning("[eDVBSoftDecoder] No decode demux from PVR handler - aborting!"); m_record = nullptr; return -2; } @@ -292,14 +295,14 @@ int eDVBSoftDecoder::setupRecorder() // Get demux ID uint8_t demux_id = 0; m_decode_demux->getCADemuxID(demux_id); - eDebug("[SoftDecoder] Decode demux ID: %d (from PVR handler)", demux_id); + eDebug("[eDVBSoftDecoder] Decode demux ID: %d (from PVR handler)", demux_id); // Set demux source to PVR (critical for decoder to read from DVR) eDVBDemux *demux_raw = (eDVBDemux*)m_decode_demux.operator->(); if (demux_raw) { demux_raw->setSourcePVR(demux_id); - eDebug("[SoftDecoder] Set demux %d source to PVR (DVR%d)", demux_id, demux_id); + eDebug("[eDVBSoftDecoder] Set demux %d source to PVR (DVR%d)", demux_id, demux_id); } int fd = m_decode_demux->openDVR(O_WRONLY); @@ -307,11 +310,11 @@ int eDVBSoftDecoder::setupRecorder() { m_dvr_fd = fd; // Save for closing before thread stop m_record->setTargetFD(fd); - eDebug("[SoftDecoder] DVR opened for writing (fd=%d)", fd); + eDebug("[eDVBSoftDecoder] DVR opened for writing (fd=%d)", fd); } else { - eWarning("[SoftDecoder] Failed to open DVR for writing - aborting!"); + eWarning("[eDVBSoftDecoder] Failed to open DVR for writing - aborting!"); m_decode_demux = nullptr; m_record = nullptr; return -3; @@ -324,11 +327,11 @@ int eDVBSoftDecoder::setupRecorder() // Attach session as descrambler if (m_session) { - eDebug("[SoftDecoder] Attaching session as descrambler (active=%d)", m_session->isActive()); + eDebug("[eDVBSoftDecoder] Attaching session as descrambler (active=%d)", m_session->isActive()); m_record->setDescrambler(ePtr(m_session.operator->())); } else - eWarning("[SoftDecoder] No session attached!"); + eWarning("[eDVBSoftDecoder] No session attached!"); updatePids(false); // Add PIDs only, no decoder yet @@ -339,7 +342,7 @@ int eDVBSoftDecoder::setupRecorder() // Check if CW is already available (e.g. fast channel switch) if (m_session && m_session->hasKeys()) { - eDebug("[SoftDecoder] First CW already available - starting decoder with DVR wait"); + eDebug("[eDVBSoftDecoder] First CW already available - starting decoder with DVR wait"); m_record->start(); startDecoderWithDvrWait(); return 0; @@ -354,7 +357,7 @@ int eDVBSoftDecoder::setupRecorder() // Start timeout timer for CW int wait_timeout = eSimpleConfig::getInt("config.softcsa.waitForDataTimeout", 800); - eDebug("[SoftDecoder] Waiting for first CW (timeout=%dms)", wait_timeout); + eDebug("[eDVBSoftDecoder] Waiting for first CW (timeout=%dms)", wait_timeout); m_start_timer = eTimer::create(eApp); CONNECT(m_start_timer->timeout, eDVBSoftDecoder::onWaitForFirstDataTimeout); @@ -374,10 +377,10 @@ void eDVBSoftDecoder::recordEvent(int event) switch (event) { case iDVBTSRecorder::eventWriteError: - eDebug("[SoftDecoder] TS write error"); + eDebug("[eDVBSoftDecoder] TS write error"); break; default: - eDebug("[SoftDecoder] Unhandled record event %d", event); + eDebug("[eDVBSoftDecoder] Unhandled record event %d", event); break; } } @@ -391,6 +394,30 @@ void eDVBSoftDecoder::streamHealthCheck() if (m_paused) return; + // Detect MainLoop hangs: if this timer callback comes much later than + // expected (>2s instead of 500ms), the MainLoop was blocked. + // In that case, skip the stall check - the stream kept running fine + // via eDVBCWHandler thread, only our monitoring was paused. + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + int64_t now = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + if (m_last_health_check > 0) + { + int64_t elapsed = now - m_last_health_check; + if (elapsed > 2000) + { + eDebug("[eDVBSoftDecoder] MainLoop was blocked for %lldms, skipping stall check", elapsed); + m_stall_count = 0; + m_stream_stalled = false; + m_last_pts = 0; + m_last_health_check = now; + // Restart timer to discard all queued callbacks from the freeze + m_health_timer->start(500, false); + return; + } + } + m_last_health_check = now; + pts_t current_pts = 0; if (m_decoder->getPTS(0, current_pts) != 0) return; @@ -405,12 +432,12 @@ void eDVBSoftDecoder::streamHealthCheck() m_stall_count++; if (m_stall_count == 3) { - eWarning("[SoftDecoder] Stream stalled (PTS=%lld)", current_pts); + eWarning("[eDVBSoftDecoder] Stream stalled (PTS=%lld)", current_pts); m_stream_stalled = true; } else if (m_stall_count == 6) { - eWarning("[SoftDecoder] Stream stalled too long - attempting recovery"); + eWarning("[eDVBSoftDecoder] Stream stalled too long - attempting recovery"); m_decoder->pause(); m_decoder->play(); m_stall_count = 0; @@ -420,7 +447,7 @@ void eDVBSoftDecoder::streamHealthCheck() else { if (m_stream_stalled) - eDebug("[SoftDecoder] Stream recovered (PTS: %lld -> %lld)", m_last_pts, current_pts); + eDebug("[eDVBSoftDecoder] Stream recovered (PTS: %lld -> %lld)", m_last_pts, current_pts); m_stall_count = 0; m_stream_stalled = false; } @@ -434,7 +461,7 @@ void eDVBSoftDecoder::serviceEventSource(int event) switch (event) { case eDVBServicePMTHandler::eventNewProgramInfo: - eDebug("[SoftDecoder] Source: eventNewProgramInfo"); + eDebug("[eDVBSoftDecoder] Source: eventNewProgramInfo"); if (m_running) updatePids(true); // Decoder already running, update it break; @@ -452,7 +479,7 @@ void eDVBSoftDecoder::updatePids(bool withDecoder) eDVBServicePMTHandler::program program; if (m_source_handler.getProgramInfo(program)) { - eDebug("[SoftDecoder] getting program info failed."); + eDebug("[eDVBSoftDecoder] getting program info failed."); return; } @@ -462,7 +489,7 @@ void eDVBSoftDecoder::updatePids(bool withDecoder) if (program.pmtPid != -1) pids_to_record.insert(program.pmtPid); // PMT - eDebugNoNewLineStart("[SoftDecoder] have %zd video stream(s)", program.videoStreams.size()); + eDebugNoNewLineStart("[eDVBSoftDecoder] have %zd video stream(s)", program.videoStreams.size()); if (!program.videoStreams.empty()) { eDebugNoNewLine(" ("); @@ -574,22 +601,22 @@ void eDVBSoftDecoder::updateDecoder(int vpid, int vpidtype, int pcrpid) m_pvr_handler.getDecodeDemux(m_decode_demux); if (!m_decode_demux) { - eWarning("[SoftDecoder] updateDecoder: No decode demux available!"); + eWarning("[eDVBSoftDecoder] updateDecoder: No decode demux available!"); return; } uint8_t demux_id = 0; m_decode_demux->getCADemuxID(demux_id); - eDebug("[SoftDecoder] Getting decoder from demux %d", demux_id); + eDebug("[eDVBSoftDecoder] Getting decoder from demux %d", demux_id); m_decode_demux->getMPEGDecoder(m_decoder, m_decoder_index); if (!m_decoder) { - eWarning("[SoftDecoder] updateDecoder: getMPEGDecoder failed!"); + eWarning("[eDVBSoftDecoder] updateDecoder: getMPEGDecoder failed!"); return; } - eDebug("[SoftDecoder] Decoder created on demux %d", demux_id); + eDebug("[eDVBSoftDecoder] Decoder created on demux %d", demux_id); // Connect to video events to forward them to parent m_decoder->connectVideoEvent(sigc::mem_fun(*this, &eDVBSoftDecoder::videoEvent), m_video_event_conn); mustPlay = true; @@ -597,7 +624,7 @@ void eDVBSoftDecoder::updateDecoder(int vpid, int vpidtype, int pcrpid) if (m_decoder) { - eDebug("[SoftDecoder] Setting decoder: vpid=%04x vpidtype=%d pcrpid=%04x", vpid, vpidtype, pcrpid); + eDebug("[eDVBSoftDecoder] Setting decoder: vpid=%04x vpidtype=%d pcrpid=%04x", vpid, vpidtype, pcrpid); m_decoder->setVideoPID(vpid, vpidtype); // Select audio stream - first check service cache, then fall back to language preferences @@ -625,7 +652,7 @@ void eDVBSoftDecoder::updateDecoder(int vpid, int vpidtype, int pcrpid) apid = cached_apid; atype = program.audioStreams[s].type; audio_index = s; - eDebug("[SoftDecoder] Using cached audio: apid=%04x atype=%d (stream %u)", apid, atype, audio_index); + eDebug("[eDVBSoftDecoder] Using cached audio: apid=%04x atype=%d (stream %u)", apid, atype, audio_index); break; } } @@ -644,7 +671,7 @@ void eDVBSoftDecoder::updateDecoder(int vpid, int vpidtype, int pcrpid) apid = program.audioStreams[audio_index].pid; atype = program.audioStreams[audio_index].type; - eDebug("[SoftDecoder] Using default audio: apid=%04x atype=%d (stream %u of %zu)", + eDebug("[eDVBSoftDecoder] Using default audio: apid=%04x atype=%d (stream %u of %zu)", apid, atype, audio_index, program.audioStreams.size()); } @@ -660,7 +687,8 @@ void eDVBSoftDecoder::updateDecoder(int vpid, int vpidtype, int pcrpid) if (mustPlay) { m_decoder->play(); - eDebug("[SoftDecoder] Decoder PLAY with vpid=%04x vpidtype=%d", vpid, vpidtype); + eDebug("[eDVBSoftDecoder] Decoder PLAY with vpid=%04x vpidtype=%d", vpid, vpidtype); + m_decoder_ready(); } else { @@ -733,13 +761,13 @@ int eDVBSoftDecoder::selectAudioTrack(unsigned int i) eDVBServicePMTHandler::program program; if (m_source_handler.getProgramInfo(program)) { - eDebug("[SoftDecoder] selectAudioTrack: getProgramInfo failed"); + eDebug("[eDVBSoftDecoder] selectAudioTrack: getProgramInfo failed"); return -1; } if (i >= program.audioStreams.size()) { - eDebug("[SoftDecoder] selectAudioTrack: invalid track %u (have %zu)", + eDebug("[eDVBSoftDecoder] selectAudioTrack: invalid track %u (have %zu)", i, program.audioStreams.size()); return -2; } @@ -747,7 +775,7 @@ int eDVBSoftDecoder::selectAudioTrack(unsigned int i) int pid = program.audioStreams[i].pid; int type = program.audioStreams[i].type; - eDebug("[SoftDecoder] selectAudioTrack(%u): pid=%04x type=%d", i, pid, type); + eDebug("[eDVBSoftDecoder] selectAudioTrack(%u): pid=%04x type=%d", i, pid, type); // Set audio PID on our decoder int ret = setAudioPID(pid, type); diff --git a/lib/service/servicedvbsoftdecoder.h b/lib/service/servicedvbsoftdecoder.h index 1b7b7aae687..0e3c862e507 100644 --- a/lib/service/servicedvbsoftdecoder.h +++ b/lib/service/servicedvbsoftdecoder.h @@ -89,6 +89,9 @@ class eDVBSoftDecoder : public iObject, public sigc::trackable // Audio track selection signal (notifies parent when SoftDecoder selects audio) sigc::signal m_audio_pid_selected; + // Decoder ready signal (fired after decoder PLAY, video info now queryable) + sigc::signal m_decoder_ready; + private: eDVBServicePMTHandler& m_source_handler; eDVBServicePMTHandler m_pvr_handler; // Separate PVR handler for decode demux @@ -119,6 +122,7 @@ class eDVBSoftDecoder : public iObject, public sigc::trackable int m_stall_count; bool m_stream_stalled; bool m_paused; + int64_t m_last_health_check; void streamHealthCheck(); // Event Handlers