From a1a544a7731085544be42c749e85983347b94669 Mon Sep 17 00:00:00 2001 From: Martin Vallevand Date: Wed, 23 Oct 2024 14:49:42 -0400 Subject: [PATCH] Implement multi-stream recordings Enable API changes for multiple recording stream . Allow optional Kodi extraction of thumbnails Requires xbmc/xbmc#25867 to be merged. --- pvr.nextpvr/addon.xml.in | 2 +- pvr.nextpvr/changelog.txt | 4 + pvr.nextpvr/resources/instance-settings.xml | 5 + .../resource.language.en_gb/strings.po | 8 ++ src/InstanceSettings.cpp | 6 + src/InstanceSettings.h | 1 + src/Recordings.cpp | 6 +- src/buffers/RecordingBuffer.cpp | 4 +- src/buffers/RecordingBuffer.h | 4 +- src/pvrclient-nextpvr.cpp | 119 +++++++++++++----- src/pvrclient-nextpvr.h | 9 +- 11 files changed, 126 insertions(+), 42 deletions(-) diff --git a/pvr.nextpvr/addon.xml.in b/pvr.nextpvr/addon.xml.in index b17cd18d..6eba7c4e 100644 --- a/pvr.nextpvr/addon.xml.in +++ b/pvr.nextpvr/addon.xml.in @@ -1,7 +1,7 @@ @ADDON_DEPENDS@ diff --git a/pvr.nextpvr/changelog.txt b/pvr.nextpvr/changelog.txt index b8361d24..3b8265eb 100644 --- a/pvr.nextpvr/changelog.txt +++ b/pvr.nextpvr/changelog.txt @@ -1,3 +1,7 @@ +v22.4.0 +- Implement multi-stream recording +- Allow optional Kodi thumbnail extraction + v22.3.0 - PVR Add-on API v9.2.0 diff --git a/pvr.nextpvr/resources/instance-settings.xml b/pvr.nextpvr/resources/instance-settings.xml index cc122d14..5fde0523 100644 --- a/pvr.nextpvr/resources/instance-settings.xml +++ b/pvr.nextpvr/resources/instance-settings.xml @@ -220,6 +220,11 @@ false + + 1 + true + + 2 false diff --git a/pvr.nextpvr/resources/language/resource.language.en_gb/strings.po b/pvr.nextpvr/resources/language/resource.language.en_gb/strings.po index 02049f21..838d290f 100644 --- a/pvr.nextpvr/resources/language/resource.language.en_gb/strings.po +++ b/pvr.nextpvr/resources/language/resource.language.en_gb/strings.po @@ -441,3 +441,11 @@ msgstr "" msgctxt "#30218" msgid "Repeating (all episodes)" msgstr "" + +msgctxt "#30219" +msgid "Download recording poster" +msgstr "" + +msgctxt "#30719" +msgid "Download backend poster or extract thumbnail from recording" +msgstr "" diff --git a/src/InstanceSettings.cpp b/src/InstanceSettings.cpp index 67b181d7..50753dfd 100644 --- a/src/InstanceSettings.cpp +++ b/src/InstanceSettings.cpp @@ -125,6 +125,8 @@ void InstanceSettings::ReadFromAddon() m_comskip = ReadBoolSetting("comskip", true); + m_recordingPoster = ReadBoolSetting("poster", true); + enum eHeartbeat m_heartbeat = ReadEnumSetting("heartbeat", eHeartbeat::Default); if (m_heartbeat == eHeartbeat::Default) @@ -323,6 +325,10 @@ ADDON_STATUS InstanceSettings::SetValue(const std::string& settingName, const ko return SetSetting(settingName, settingValue, m_separateSeasons, ADDON_STATUS_NEED_RESTART, ADDON_STATUS_OK); else if (settingName == "showroot") return SetSetting(settingName, settingValue, m_showRoot, ADDON_STATUS_NEED_RESTART, ADDON_STATUS_OK); + else if (settingName == "comskip") + return SetSetting(settingName, settingValue, m_comskip, ADDON_STATUS_NEED_RESTART, ADDON_STATUS_OK); + else if (settingName == "poster") + return SetSetting(settingName, settingValue, m_recordingPoster, ADDON_STATUS_NEED_RESTART, ADDON_STATUS_OK); else if (settingName == "genrestring") return SetSetting(settingName, settingValue, m_genreString, ADDON_STATUS_NEED_SETTINGS, ADDON_STATUS_OK); else if (settingName == "host_mac") diff --git a/src/InstanceSettings.h b/src/InstanceSettings.h index fe2d37dc..59c0d9b3 100644 --- a/src/InstanceSettings.h +++ b/src/InstanceSettings.h @@ -113,6 +113,7 @@ namespace NextPVR bool m_showRoot = false; int m_chunkRecording = 32; bool m_comskip = true; + bool m_recordingPoster = true; //Timers int m_defaultPrePadding = 0; diff --git a/src/Recordings.cpp b/src/Recordings.cpp index d7a27da3..6a43c22d 100644 --- a/src/Recordings.cpp +++ b/src/Recordings.cpp @@ -320,8 +320,9 @@ bool Recordings::UpdatePvrRecording(const tinyxml2::XMLNode* pRecordingNode, kod buffer.clear(); XMLUtils::GetString(pRecordingNode, "id", buffer); tag.SetRecordingId(buffer); + bool series = ParseNextPVRSubtitle(pRecordingNode, tag); - if (ParseNextPVRSubtitle(pRecordingNode, tag)) + if (series) { if (m_settings->m_separateSeasons && multipleSeasons && tag.GetSeriesNumber() != PVR_RECORDING_INVALID_SERIES_EPISODE) { @@ -439,7 +440,8 @@ bool Recordings::UpdatePvrRecording(const tinyxml2::XMLNode* pRecordingNode, kod else artworkPath = kodi::tools::StringUtils::Format("%s/service?method=channel.show.artwork&name=%s", m_settings->m_urlBase, name.c_str()); tag.SetFanartPath(artworkPath + "&prefer=fanart"); - tag.SetThumbnailPath(artworkPath + "&prefer=poster"); + if (m_settings->m_recordingPoster || status == "Failed" || tag.GetSizeInBytes() == 0) + tag.SetThumbnailPath(artworkPath + "&prefer=poster"); } if (XMLUtils::GetAdditiveString(pRecordingNode->FirstChildElement("genres"), "genre", EPG_STRING_TOKEN_SEPARATOR, buffer, true)) { diff --git a/src/buffers/RecordingBuffer.cpp b/src/buffers/RecordingBuffer.cpp index 9d1ca3e9..daca8877 100644 --- a/src/buffers/RecordingBuffer.cpp +++ b/src/buffers/RecordingBuffer.cpp @@ -70,11 +70,11 @@ int RecordingBuffer::Duration(void) } } -bool RecordingBuffer::Open(const std::string inputUrl, const kodi::addon::PVRRecording& recording) +bool RecordingBuffer::Open(const std::string inputUrl, const kodi::addon::PVRRecording& recording, int64_t streamId) { m_Duration = recording.GetDuration(); - kodi::Log(ADDON_LOG_DEBUG, "RecordingBuffer::Open %d %lld", recording.GetDuration(), recording.GetRecordingTime()); + kodi::Log(ADDON_LOG_DEBUG, "RecordingBuffer::Open %d %lld streamId %d", recording.GetDuration(), recording.GetRecordingTime(), streamId); if (recording.GetDuration() + recording.GetRecordingTime() > time(nullptr)) { m_recordingTime = recording.GetRecordingTime() + m_settings->m_serverTimeOffset; diff --git a/src/buffers/RecordingBuffer.h b/src/buffers/RecordingBuffer.h index 36e17352..77c1d867 100644 --- a/src/buffers/RecordingBuffer.h +++ b/src/buffers/RecordingBuffer.h @@ -74,11 +74,13 @@ namespace timeshift { return PVR_ERROR_NO_ERROR; } - bool Open(const std::string inputUrl, const kodi::addon::PVRRecording& recording); + bool Open(const std::string inputUrl, const kodi::addon::PVRRecording& recording, int64_t streamId); std::atomic m_isLive; // recording start time time_t m_recordingTime; + + int m_streamId; }; } diff --git a/src/pvrclient-nextpvr.cpp b/src/pvrclient-nextpvr.cpp index d5542294..55cbd4bc 100644 --- a/src/pvrclient-nextpvr.cpp +++ b/src/pvrclient-nextpvr.cpp @@ -109,7 +109,6 @@ cPVRClientNextPVR::cPVRClientNextPVR(const CNextPVRAddon& base, const kodi::addo m_supportsLiveTimeshift = false; m_lastRecordingUpdateTime = std::numeric_limits::max(); // time of last recording check - force forever m_timeshiftBuffer = new timeshift::DummyBuffer(m_settings, m_request); - m_recordingBuffer = new timeshift::RecordingBuffer(m_settings, m_request); m_realTimeBuffer = new timeshift::DummyBuffer(m_settings, m_request); m_livePlayer = nullptr; m_nowPlaying = NotPlaying; @@ -123,7 +122,14 @@ cPVRClientNextPVR::~cPVRClientNextPVR() { // this is likley only needed for transcoding but include all cases if (m_nowPlaying == Recording) - CloseRecordedStream(-1); + { + std::map::iterator itr = m_multistreamRecording.begin(); + while (itr != m_multistreamRecording.end()) + { + CloseRecordedStream(itr->first); + itr = m_multistreamRecording.begin(); + } + } else CloseLiveStream(); } @@ -136,7 +142,6 @@ cPVRClientNextPVR::~cPVRClientNextPVR() if (m_bConnected) Disconnect(); delete m_timeshiftBuffer; - delete m_recordingBuffer; delete m_realTimeBuffer; m_recordings.m_hostFilenames.clear(); m_channels.m_channelDetails.clear(); @@ -705,6 +710,7 @@ PVR_ERROR cPVRClientNextPVR::GetSignalStatus(int channelUid, kodi::addon::PVRSig bool cPVRClientNextPVR::CanPauseStream(void) { + // not called for recordings if (IsServerStreaming()) { if (m_nowPlaying == Recording) @@ -720,12 +726,25 @@ void cPVRClientNextPVR::PauseStream(bool bPaused) if (IsServerStreaming()) { if (m_nowPlaying == Recording) - m_recordingBuffer->PauseStream(bPaused); + m_multistreamRecording[m_streamCount]->PauseStream(bPaused); else m_livePlayer->PauseStream(bPaused); } } + +PVR_ERROR cPVRClientNextPVR::PauseRecordedStream(int64_t streamId, bool bPaused) +{ + if (IsServerStreaming()) + { + if (m_nowPlaying == Recording) + m_multistreamRecording[streamId]->PauseStream(bPaused); + else + m_livePlayer->PauseStream(bPaused); + } + return PVR_ERROR_NO_ERROR; +} + bool cPVRClientNextPVR::CanSeekStream(void) { if (IsServerStreamingLive()) @@ -743,45 +762,58 @@ bool cPVRClientNextPVR::CanSeekStream(void) bool cPVRClientNextPVR::OpenRecordedStream(const kodi::addon::PVRRecording& recording, int64_t& streamId) { kodi::addon::PVRRecording copyRecording = recording; - m_nowPlaying = Recording; copyRecording.SetDirectory(m_recordings.m_hostFilenames[recording.GetRecordingId()]); const std::string line = kodi::tools::StringUtils::Format("%s/live?recording=%s&client=XBMC-%s", m_settings->m_urlBase, recording.GetRecordingId().c_str(), m_request.GetSID()); - return m_recordingBuffer->Open(line, copyRecording); + m_mutexMulti.lock(); + m_nowPlaying = Recording; + m_multistreamRecording.emplace(++m_streamCount, new timeshift::RecordingBuffer(m_settings, m_request)); + streamId = m_streamCount; + bool ret = m_multistreamRecording[streamId]->Open(line, copyRecording, streamId); + if (!ret) + { + CloseRecordedStream(streamId); + } + m_mutexMulti.unlock(); + return ret; } void cPVRClientNextPVR::CloseRecordedStream(int64_t streamId) { - if (IsServerStreamingRecording()) + if (IsServerStreamingRecording(streamId)) { - m_recordingBuffer->Close(); - m_recordingBuffer->SetDuration(0); + m_mutexMulti.lock(); + m_multistreamRecording[streamId]->Close(); + m_multistreamRecording.erase(streamId); + m_mutexMulti.unlock(); } - m_nowPlaying = NotPlaying; + if (m_multistreamRecording.size() == 0) + m_nowPlaying = NotPlaying; + kodi::Log(ADDON_LOG_DEBUG, "Closed streamId %d remaining %d", streamId, m_multistreamRecording.size()); } int cPVRClientNextPVR::ReadRecordedStream(int64_t streamId, unsigned char* pBuffer, unsigned int iBufferSize) { - if (IsServerStreamingRecording()) + if (IsServerStreamingRecording(streamId)) { - return m_recordingBuffer->Read(pBuffer, iBufferSize); + return m_multistreamRecording[streamId]->Read(pBuffer, iBufferSize); } return -1; } int64_t cPVRClientNextPVR::SeekRecordedStream(int64_t streamId, int64_t iPosition, int iWhence) { - if (IsServerStreamingRecording()) + if (IsServerStreamingRecording(streamId)) { - return m_recordingBuffer->Seek(iPosition, iWhence); + return m_multistreamRecording[streamId]->Seek(iPosition, iWhence); } return -1; } int64_t cPVRClientNextPVR::LengthRecordedStream(int64_t streamId) { - if (IsServerStreamingRecording()) + if (IsServerStreamingRecording(streamId)) { - return m_recordingBuffer->Length(); + return m_multistreamRecording[streamId]->Length(); } return -1; } @@ -800,19 +832,45 @@ bool cPVRClientNextPVR::IsRealTimeStream() if (IsServerStreaming()) { if (m_nowPlaying == Recording) - return m_recordingBuffer->IsRealTimeStream(); + return m_multistreamRecording[m_streamCount]->IsRealTimeStream(); else return m_livePlayer->IsRealTimeStream(); } return false; } - PVR_ERROR cPVRClientNextPVR::GetStreamTimes(kodi::addon::PVRStreamTimes& stimes) { if (IsServerStreaming()) { if (m_nowPlaying == Recording) - return m_recordingBuffer->GetStreamTimes(stimes); + return m_multistreamRecording[m_streamCount]->GetStreamTimes(stimes); + else + return m_livePlayer->GetStreamTimes(stimes); + } + return PVR_ERROR_UNKNOWN; +} + + +PVR_ERROR cPVRClientNextPVR::IsRecordedStreamRealTime(int64_t streamId, bool& isRealTime) +{ + if (IsServerStreaming()) + { + if (m_nowPlaying == Recording) + { + isRealTime = m_multistreamRecording[streamId]->IsRealTimeStream(); + } + else + return PVR_ERROR_INVALID_PARAMETERS; + } + return PVR_ERROR_NO_ERROR; +} + +PVR_ERROR cPVRClientNextPVR::GetRecordedStreamTimes(int64_t streamId, kodi::addon::PVRStreamTimes& stimes) +{ + if (IsServerStreaming()) + { + if (m_nowPlaying == Recording) + return m_multistreamRecording[streamId]->GetStreamTimes(stimes); else return m_livePlayer->GetStreamTimes(stimes); } @@ -836,11 +894,11 @@ PVR_ERROR cPVRClientNextPVR::GetStreamReadChunkSize(int& chunksize) bool cPVRClientNextPVR::IsServerStreaming() { - if (IsServerStreamingLive(false) || IsServerStreamingRecording(false)) + if (IsServerStreamingLive(false) || m_multistreamRecording.size() > 0) { return true; } - kodi::Log(ADDON_LOG_ERROR, "Unknown streaming state %d %d %d", m_nowPlaying, m_recordingBuffer->GetDuration(), !m_livePlayer); + kodi::Log(ADDON_LOG_ERROR, "Unknown streaming state %d %d %d", m_nowPlaying, m_multistreamRecording.size(), !m_livePlayer); return false; } @@ -851,29 +909,21 @@ bool cPVRClientNextPVR::IsServerStreamingLive(bool log) return true; } if (log) - kodi::Log(ADDON_LOG_ERROR, "Unknown live streaming state %d %d %d", m_nowPlaying, m_recordingBuffer->GetDuration(), !m_livePlayer); + kodi::Log(ADDON_LOG_ERROR, "Unknown live streaming state %d %d %d", m_nowPlaying, m_multistreamRecording.size(), !m_livePlayer); return false; } -bool cPVRClientNextPVR::IsServerStreamingRecording(bool log) +bool cPVRClientNextPVR::IsServerStreamingRecording(int64_t streamId, bool log) { - if (m_nowPlaying == Recording && m_recordingBuffer->GetDuration() > 0) + if (m_nowPlaying == Recording && m_multistreamRecording.size() > 0) { - return true; + return m_multistreamRecording.find(streamId) != m_multistreamRecording.end(); } if (log) - kodi::Log(ADDON_LOG_ERROR, "Unknown recording streaming state %d %d %d", m_nowPlaying, m_recordingBuffer->GetDuration(), !m_livePlayer); + kodi::Log(ADDON_LOG_ERROR, "Unknown recording streaming state %d %d %d", m_nowPlaying, m_multistreamRecording.size(), !m_livePlayer); return false; } -/* -PVR_ERROR cPVRClientNextPVR::GetBackendName(std::string& name) -{ - name = m_settings->m_hostname; - return PVR_ERROR_NO_ERROR; -} -*/ - PVR_ERROR cPVRClientNextPVR::CallChannelMenuHook(const kodi::addon::PVRMenuhook& menuhook, const kodi::addon::PVRChannel& item) { return m_menuhook.CallChannelMenuHook(menuhook, item); @@ -1047,5 +1097,6 @@ PVR_ERROR cPVRClientNextPVR::GetCapabilities(kodi::addon::PVRCapabilities& capab capabilities.SetSupportsDescrambleInfo(false); capabilities.SetSupportsRecordingPlayCount(m_settings->m_backendResume); capabilities.SetSupportsProviders(false); + capabilities.SetSupportsMultipleRecordedStreams(!m_settings->m_recordingPoster); return PVR_ERROR_NO_ERROR; } diff --git a/src/pvrclient-nextpvr.h b/src/pvrclient-nextpvr.h index 02aa0284..bb102770 100644 --- a/src/pvrclient-nextpvr.h +++ b/src/pvrclient-nextpvr.h @@ -76,15 +76,18 @@ class ATTR_DLL_LOCAL cPVRClientNextPVR : public kodi::addon::CInstancePVRClient int64_t LengthLiveStream() override; bool CanPauseStream() override; void PauseStream(bool paused) override; + PVR_ERROR PauseRecordedStream(int64_t streamId, bool paused) override; bool CanSeekStream() override; bool IsTimeshifting(); bool IsRealTimeStream() override; PVR_ERROR GetStreamTimes(kodi::addon::PVRStreamTimes& times) override; + PVR_ERROR IsRecordedStreamRealTime(int64_t streamId, bool& isRealTime) override; + PVR_ERROR GetRecordedStreamTimes(int64_t streamId, kodi::addon::PVRStreamTimes& times) override; PVR_ERROR GetStreamReadChunkSize(int& chunksize) override; bool IsRadio() { return m_nowPlaying == Radio; }; bool IsServerStreaming(); bool IsServerStreamingLive(bool log = true); - bool IsServerStreamingRecording(bool log = true); + bool IsServerStreamingRecording(int64_t streamId, bool log = true); /* Record stream handling */ bool OpenRecordedStream(const kodi::addon::PVRRecording& recinfo, int64_t& streamId) override; @@ -147,7 +150,9 @@ class ATTR_DLL_LOCAL cPVRClientNextPVR : public kodi::addon::CInstancePVRClient timeshift::Buffer* m_timeshiftBuffer; timeshift::Buffer* m_livePlayer; timeshift::Buffer* m_realTimeBuffer; - timeshift::RecordingBuffer* m_recordingBuffer; + std::map m_multistreamRecording; + mutable std::recursive_mutex m_mutexMulti; + int64_t m_streamCount = -1; //Matrix changes std::shared_ptr m_settings;