From dda66ef083e873afcdfe663cf453c62e30d7bb2d Mon Sep 17 00:00:00 2001 From: ksooo <3226626+ksooo@users.noreply.github.com> Date: Mon, 21 Oct 2024 22:58:44 +0200 Subject: [PATCH] PVR Add-on API 9.2.0 (multiple recorded streams, title extra info). --- pvr.hts/addon.xml.in | 2 +- pvr.hts/changelog.txt | 5 + src/Tvheadend.cpp | 268 +++++++++++++++++++++++++------------- src/Tvheadend.h | 27 ++-- src/tvheadend/HTSPVFS.cpp | 3 +- src/tvheadend/HTSPVFS.h | 4 + 6 files changed, 197 insertions(+), 112 deletions(-) diff --git a/pvr.hts/addon.xml.in b/pvr.hts/addon.xml.in index c383d67d..40f1e04f 100644 --- a/pvr.hts/addon.xml.in +++ b/pvr.hts/addon.xml.in @@ -1,7 +1,7 @@ @ADDON_DEPENDS@ diff --git a/pvr.hts/changelog.txt b/pvr.hts/changelog.txt index 837121c5..f6aaf04f 100644 --- a/pvr.hts/changelog.txt +++ b/pvr.hts/changelog.txt @@ -1,3 +1,8 @@ +v22.5.0 +- PVR Add-on API v9.2.0 +- Add support for multiple recorded streams at a time (used by Kodi for thumbnail extraction) +- Add support for title extra information; do not misuse episode name field for this data + v22.4.2 - Fix pointless "Not all properties handled" log spam introduced with v22.4.1 diff --git a/src/Tvheadend.cpp b/src/Tvheadend.cpp index 1af01fda..6f648f76 100644 --- a/src/Tvheadend.cpp +++ b/src/Tvheadend.cpp @@ -36,14 +36,12 @@ CTvheadend::CTvheadend(const kodi::addon::IInstanceInfo& instance) m_customTimerProps( {CUSTOM_PROP_ID_DVR_CONFIGURATION, CUSTOM_PROP_ID_DVR_COMMENT}, *m_conn, m_dvrConfigs), m_streamchange(false), - m_vfs(new HTSPVFS(m_settings, *m_conn)), m_queue(static_cast(-1)), m_asyncState(m_settings->GetResponseTimeout()), m_timeRecordings(*m_conn, m_dvrConfigs), m_autoRecordings(m_settings, *m_conn, m_dvrConfigs), m_epgMaxDays(EpgMaxFutureDays()), - m_playingLiveStream(false), - m_playingRecording(nullptr) + m_playingLiveStream(false) { m_dmx.reserve(m_settings->GetTotalTuners()); for (int i = 0; i < 1 || i < m_settings->GetTotalTuners(); i++) @@ -61,7 +59,6 @@ CTvheadend::~CTvheadend() delete dmx; delete m_conn; - delete m_vfs; } void CTvheadend::Start() @@ -123,6 +120,7 @@ PVR_ERROR CTvheadend::GetCapabilities(kodi::addon::PVRCapabilities& capabilities capabilities.SetSupportsRecordingSize(m_conn->GetProtocol() >= 35); capabilities.SetSupportsProviders(m_conn->GetProtocol() >= 38); + capabilities.SetSupportsMultipleRecordedStreams(true); return PVR_ERROR_NO_ERROR; } @@ -611,7 +609,12 @@ PVR_ERROR CTvheadend::GetRecordings(bool deleted, kodi::addon::PVRRecordingsResu rec.SetTitle(recording.GetTitle()); /* Subtitle */ - rec.SetEpisodeName(recording.GetSubtitle()); + rec.SetTitleExtraInfo(recording.GetSubtitle()); + + /* Episode name - not directly supported by TVH. Assume subtitle containing episode name */ + /* if episode number is present. */ + if (recording.GetEpisode() > 0) + rec.SetEpisodeName(recording.GetSubtitle()); /* season/episode (tvh 4.3+) */ rec.SetSeriesNumber(recording.GetSeason()); @@ -1513,9 +1516,11 @@ void CTvheadend::CreateEvent(const Event& event, kodi::addon::PVREPGTag& epg) epg.SetParentalRatingSource(event.GetRatingSource()); epg.SetStarRating(event.GetStars()); epg.SetSeriesNumber(event.GetSeason()); + epg.SetTitleExtraInfo(event.GetSubtitle()); + if (event.GetEpisode() > 0) + epg.SetEpisodeName(event.GetSubtitle()); epg.SetEpisodeNumber(event.GetEpisode()); epg.SetEpisodePartNumber(event.GetPart()); - epg.SetEpisodeName(event.GetSubtitle()); epg.SetFlags(EPG_TAG_FLAG_UNDEFINED); epg.SetSeriesLink(event.GetSeriesLink()); } @@ -1723,54 +1728,128 @@ PVR_ERROR CTvheadend::OnSystemWake() * VFS * *************************************************************************/ -bool CTvheadend::OpenRecordedStream(const kodi::addon::PVRRecording& rec) +bool CTvheadend::OpenRecordedStream(const kodi::addon::PVRRecording& recording, int64_t& streamId) { if (!m_asyncState.WaitForState(ASYNC_EPG)) + return PVR_ERROR_SERVER_TIMEOUT; + + const auto vfs{std::make_shared(m_settings, *m_conn)}; + if (!vfs->Open(recording)) return false; - bool ret = m_vfs->Open(rec); + streamId = vfs->GetFileId(); - if (ret) + std::lock_guard lock(m_mutex); + m_vfs.insert({streamId, vfs}); + return true; +} + +void CTvheadend::CloseRecordedStream(int64_t streamId) +{ + std::shared_ptr vfs; { std::lock_guard lock(m_mutex); - const auto& it = m_recordings.find(std::stoul(rec.GetRecordingId())); - if (it != m_recordings.end()) - { - m_playingRecording = &(it->second); - } + auto it = m_vfs.find(streamId); + if (it == m_vfs.end()) + return; + + vfs = (*it).second; + m_vfs.erase(it); } - return ret; + vfs->Close(); } -void CTvheadend::CloseRecordedStream() +int CTvheadend::ReadRecordedStream(int64_t streamId, unsigned char* buffer, unsigned int size) { - m_vfs->Close(); + std::shared_ptr vfs; + bool isRecordingInProgress{false}; + { + std::lock_guard lock(m_mutex); - std::lock_guard lock(m_mutex); - m_playingRecording = nullptr; -} + auto it = m_vfs.find(streamId); + if (it == m_vfs.end()) + return 0; -int CTvheadend::ReadRecordedStream(unsigned char* buf, unsigned int len) -{ - return m_vfs->Read(buf, len, VfsIsActiveRecording()); + vfs = (*it).second; + + auto it2 = m_recordings.find(std::stoul(vfs->GetRecordingId())); + if (it2 == m_recordings.end()) + return 0; + + isRecordingInProgress = ((*it2).second.GetState() == PVR_TIMER_STATE_RECORDING); + } + + const int bytesRead = vfs->Read(buffer, size, isRecordingInProgress); + return bytesRead < 0 ? 0 : bytesRead; } -int64_t CTvheadend::SeekRecordedStream(int64_t position, int whence) +int64_t CTvheadend::SeekRecordedStream(int64_t streamId, int64_t position, int whence) { - return m_vfs->Seek(position, whence, VfsIsActiveRecording()); + std::shared_ptr vfs; + bool isRecordingInProgress{false}; + { + std::lock_guard lock(m_mutex); + + auto it = m_vfs.find(streamId); + if (it == m_vfs.end()) + return 0; + + vfs = (*it).second; + + auto it2 = m_recordings.find(std::stoul(vfs->GetRecordingId())); + if (it2 == m_recordings.end()) + return 0; + + isRecordingInProgress = ((*it2).second.GetState() == PVR_TIMER_STATE_RECORDING); + } + + const int64_t newPos = vfs->Seek(position, whence, isRecordingInProgress); + return newPos < 0 ? 0 : newPos; } -int64_t CTvheadend::LengthRecordedStream() +int64_t CTvheadend::LengthRecordedStream(int64_t streamId) { - return m_vfs->Size(); + std::shared_ptr vfs; + { + std::lock_guard lock(m_mutex); + + auto it = m_vfs.find(streamId); + if (it == m_vfs.end()) + return 0; + + vfs = (*it).second; + } + + const int64_t size = vfs->Size(); + return size < 0 ? 0 : size; } -void CTvheadend::PauseStream(bool paused) +PVR_ERROR CTvheadend::PauseRecordedStream(int64_t streamId, bool paused) { - if (VfsIsActiveRecording()) - m_vfs->PauseStream(paused); + std::shared_ptr vfs; + bool isRecordingInProgress{false}; + { + std::lock_guard lock(m_mutex); + + auto it = m_vfs.find(streamId); + if (it == m_vfs.end()) + return PVR_ERROR_INVALID_PARAMETERS; + + vfs = (*it).second; + + auto it2 = m_recordings.find(std::stoul(vfs->GetRecordingId())); + if (it2 == m_recordings.end()) + return PVR_ERROR_INVALID_PARAMETERS; + + isRecordingInProgress = ((*it2).second.GetState() == PVR_TIMER_STATE_RECORDING); + } + + if (isRecordingInProgress) + vfs->PauseStream(paused); + + return PVR_ERROR_NO_ERROR; } PVR_ERROR CTvheadend::GetStreamReadChunkSize(int& chunksize) @@ -1784,10 +1863,24 @@ PVR_ERROR CTvheadend::GetStreamReadChunkSize(int& chunksize) bool CTvheadend::IsRealTimeStream() { - if (m_playingRecording) - return m_vfs->IsRealTimeStream(); - else - return m_dmx_active->IsRealTimeStream(); + return m_dmx_active->IsRealTimeStream(); +} + +PVR_ERROR CTvheadend::IsRecordedStreamRealTime(int64_t streamId, bool& isRealTime) +{ + std::shared_ptr vfs; + { + std::lock_guard lock(m_mutex); + + auto it = m_vfs.find(streamId); + if (it == m_vfs.end()) + return PVR_ERROR_INVALID_PARAMETERS; + + vfs = (*it).second; + } + + isRealTime = vfs->IsRealTimeStream(); + return PVR_ERROR_NO_ERROR; } /* ************************************************************************** @@ -2048,7 +2141,8 @@ void CTvheadend::SyncInitCompleted() for (auto* dmx : m_dmx) dmx->RebuildState(); - m_vfs->RebuildState(); + for (const auto& vfs : m_vfs) + vfs.second->RebuildState(); } /* check state engine */ @@ -2113,22 +2207,8 @@ void CTvheadend::SyncDvrCompleted() return; /* Recordings */ - { - std::lock_guard lock(m_mutex); - - // save id of currently playing recording, if any - uint32_t id = m_playingRecording ? m_playingRecording->GetId() : 0; - - utilities::erase_if(m_recordings, - [](const RecordingMapEntry& entry) { return entry.second.IsDirty(); }); - - if (m_playingRecording) - { - const auto& it = m_recordings.find(id); - if (it == m_recordings.end()) - m_playingRecording = nullptr; - } - } + utilities::erase_if(m_recordings, + [](const RecordingMapEntry& entry) { return entry.second.IsDirty(); }); /* Time-based repeating timers */ m_timeRecordings.SyncDvrCompleted(); @@ -2500,13 +2580,6 @@ void CTvheadend::ParseRecordingAddOrUpdate(htsmsg_t* msg, bool bAdd) rec.SetId(id); rec.SetDirty(false); - { - std::lock_guard lock(m_mutex); - - if (m_playingRecording && m_playingRecording->GetId() == id) - m_playingRecording = &rec; - } - // Set the time the recording was scheduled to start. This may differ from the actual start. int64_t start = 0; if (!htsmsg_get_s64(msg, "start", &start)) @@ -2876,10 +2949,6 @@ void CTvheadend::ParseRecordingDelete(htsmsg_t* msg) /* Erase */ { std::lock_guard lock(m_mutex); - - if (m_playingRecording && m_playingRecording->GetId() == u32) - m_playingRecording = nullptr; - m_recordings.erase(u32); } @@ -3348,43 +3417,56 @@ PVR_ERROR CTvheadend::GetStreamTimes(kodi::addon::PVRStreamTimes& times) return m_dmx_active->GetStreamTimes(times); } - std::lock_guard lock(m_mutex); + return PVR_ERROR_INVALID_PARAMETERS; +} - if (m_playingRecording) +PVR_ERROR CTvheadend::GetRecordedStreamTimes(int64_t streamId, kodi::addon::PVRStreamTimes& times) +{ + Recording recording; { - if (m_playingRecording->GetState() == PVR_TIMER_STATE_RECORDING) + std::lock_guard lock(m_mutex); + + auto it = m_vfs.find(streamId); + if (it == m_vfs.end()) + return PVR_ERROR_INVALID_PARAMETERS; + + auto it2 = m_recordings.find(std::stoul((*it).second->GetRecordingId())); + if (it2 == m_recordings.end()) + return PVR_ERROR_INVALID_PARAMETERS; + + recording = (*it2).second; + } + + if (recording.GetState() == PVR_TIMER_STATE_RECORDING) + { + if (recording.GetFilesStart() > 0) { - if (m_playingRecording->GetFilesStart() > 0) - { - times.SetPTSEnd((std::time(nullptr) - m_playingRecording->GetFilesStart()) * - STREAM_TIME_BASE); - } - else - { - // Older tvh versions do not expose real recording start/stop time. - // Remark: Following calculation does not always work. Returned end time might be to large, as the - // recording might actually have started later than scheduled start time (server came up too late etc). - times.SetPTSEnd((m_playingRecording->GetStartExtra() * 60 + std::time(nullptr) - - m_playingRecording->GetStart()) * - STREAM_TIME_BASE); - } + times.SetPTSEnd((std::time(nullptr) - recording.GetFilesStart()) * STREAM_TIME_BASE); } else { - if (m_playingRecording->GetFilesStart() > 0 && m_playingRecording->GetFilesStop() > 0) - { - times.SetPTSEnd((m_playingRecording->GetFilesStop() - m_playingRecording->GetFilesStart()) * - STREAM_TIME_BASE); - } - else - { - // Older tvh versions do not expose real recording start/stop time. - // Remark: Kodi is handling finished recording's times very well on its own - in difference to - // in-progress recording's times. Returning not implemented will make Kodi handle the stream times. - return PVR_ERROR_NOT_IMPLEMENTED; - } + // Older tvh versions do not expose real recording start/stop time. + // Remark: Following calculation does not always work. Returned end time might be to large, + // as the recording might actually have started later than scheduled start time (server came + // up too late etc). + times.SetPTSEnd((recording.GetStartExtra() * 60 + std::time(nullptr) - recording.GetStart()) * + STREAM_TIME_BASE); } - return PVR_ERROR_NO_ERROR; } - return PVR_ERROR_INVALID_PARAMETERS; + else + { + if (recording.GetFilesStart() > 0 && recording.GetFilesStop() > 0) + { + times.SetPTSEnd((recording.GetFilesStop() - recording.GetFilesStart()) * STREAM_TIME_BASE); + } + else + { + // Older tvh versions do not expose real recording start/stop time. + // Remark: Kodi is handling finished recording's times very well on its own - in difference to + // in-progress recording's times. Returning not implemented will make Kodi handle the stream + // times. + return PVR_ERROR_NOT_IMPLEMENTED; + } + } + return PVR_ERROR_NO_ERROR; } diff --git a/src/Tvheadend.h b/src/Tvheadend.h index e77a188a..e70d62dd 100644 --- a/src/Tvheadend.h +++ b/src/Tvheadend.h @@ -209,14 +209,6 @@ class ATTR_DLL_LOCAL CTvheadend : public kodi::addon::CInstancePVRClient, void ParseEventDelete(htsmsg_t* m); bool ParseEvent(htsmsg_t* msg, bool bAdd, tvheadend::entity::Event& evt); - /* - * VFS - */ - bool VfsIsActiveRecording() const - { - return m_playingRecording && m_playingRecording->GetState() == PVR_TIMER_STATE_RECORDING; - } - public: /* * Connection (pass-thru) @@ -252,14 +244,16 @@ class ATTR_DLL_LOCAL CTvheadend : public kodi::addon::CInstancePVRClient, void CloseExpiredSubscriptions(); /* - * VFS (pass-thru) + * VFS */ - bool OpenRecordedStream(const kodi::addon::PVRRecording& rec) override; - void CloseRecordedStream() override; - int ReadRecordedStream(unsigned char* buf, unsigned int len) override; - int64_t SeekRecordedStream(int64_t position, int whence) override; - int64_t LengthRecordedStream() override; - void PauseStream(bool paused) override; + bool OpenRecordedStream(const kodi::addon::PVRRecording& recording, int64_t& streamId) override; + void CloseRecordedStream(int64_t streamId) override; + int ReadRecordedStream(int64_t streamId, unsigned char* buf, unsigned int len) override; + int64_t SeekRecordedStream(int64_t streamId, int64_t position, int whence) override; + int64_t LengthRecordedStream(int64_t streamId) override; + PVR_ERROR IsRecordedStreamRealTime(int64_t streamId, bool& isRealTime) override; + PVR_ERROR PauseRecordedStream(int64_t streamId, bool paused) override; + PVR_ERROR GetRecordedStreamTimes(int64_t streamId, kodi::addon::PVRStreamTimes& times) override; PVR_ERROR GetStreamReadChunkSize(int& chunksize) override; /* @@ -288,7 +282,7 @@ class ATTR_DLL_LOCAL CTvheadend : public kodi::addon::CInstancePVRClient, std::vector m_dmx; tvheadend::HTSPDemuxer* m_dmx_active; bool m_streamchange; - tvheadend::HTSPVFS* m_vfs; + std::map> m_vfs; bool m_stateRebuilt{false}; HTSPMessageQueue m_queue; @@ -311,5 +305,4 @@ class ATTR_DLL_LOCAL CTvheadend : public kodi::addon::CInstancePVRClient, int m_epgMaxDays; bool m_playingLiveStream; - tvheadend::entity::Recording* m_playingRecording; }; diff --git a/src/tvheadend/HTSPVFS.cpp b/src/tvheadend/HTSPVFS.cpp index 1292fe38..737a1351 100644 --- a/src/tvheadend/HTSPVFS.cpp +++ b/src/tvheadend/HTSPVFS.cpp @@ -71,7 +71,8 @@ bool HTSPVFS::Open(const kodi::addon::PVRRecording& rec) Close(); /* Cache details */ - m_path = kodi::tools::StringUtils::Format("dvr/%s", rec.GetRecordingId().c_str()); + m_recordingId = rec.GetRecordingId(); + m_path = kodi::tools::StringUtils::Format("dvr/%s", m_recordingId.c_str()); m_fileStart = rec.GetRecordingTime(); /* Send open */ diff --git a/src/tvheadend/HTSPVFS.h b/src/tvheadend/HTSPVFS.h index 2cd91717..1fbf9d18 100644 --- a/src/tvheadend/HTSPVFS.h +++ b/src/tvheadend/HTSPVFS.h @@ -33,6 +33,9 @@ class HTSPVFS HTSPVFS(const std::shared_ptr& settings, HTSPConnection& conn); ~HTSPVFS(); + uint32_t GetFileId() const { return m_fileId; } + const std::string& GetRecordingId() const { return m_recordingId; } + void RebuildState(); bool Open(const kodi::addon::PVRRecording& rec); @@ -51,6 +54,7 @@ class HTSPVFS std::shared_ptr m_settings; HTSPConnection& m_conn; + std::string m_recordingId; std::string m_path; uint32_t m_fileId; int64_t m_offset;