Skip to content

Commit

Permalink
Implement multi-stream recordings
Browse files Browse the repository at this point in the history
Enable API changes for multiple recording stream . Allow optional Kodi extraction of thumbnails

Requires xbmc/xbmc#25867 to be merged.
  • Loading branch information
emveepee committed Oct 25, 2024
1 parent 9bf83f5 commit 5fe7193
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 42 deletions.
2 changes: 1 addition & 1 deletion pvr.nextpvr/addon.xml.in
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon
id="pvr.nextpvr"
version="22.3.0"
version="22.4.0"
name="NextPVR PVR Client"
provider-name="Graeme Blackley">
<requires>@ADDON_DEPENDS@
Expand Down
4 changes: 4 additions & 0 deletions pvr.nextpvr/changelog.txt
Original file line number Diff line number Diff line change
@@ -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

Expand Down
5 changes: 5 additions & 0 deletions pvr.nextpvr/resources/instance-settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@
<default>false</default>
<control type="toggle"/>
</setting>
<setting help="30719" id="poster" label="30219" type="boolean">
<level>1</level>
<default>true</default>
<control type="toggle"/>
</setting>
<setting help="30693" id="separateseasons" label="30193" type="boolean">
<level>2</level>
<default>false</default>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
6 changes: 6 additions & 0 deletions src/InstanceSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ void InstanceSettings::ReadFromAddon()

m_comskip = ReadBoolSetting("comskip", true);

m_recordingPoster = ReadBoolSetting("poster", true);

enum eHeartbeat m_heartbeat = ReadEnumSetting<eHeartbeat>("heartbeat", eHeartbeat::Default);

if (m_heartbeat == eHeartbeat::Default)
Expand Down Expand Up @@ -323,6 +325,10 @@ ADDON_STATUS InstanceSettings::SetValue(const std::string& settingName, const ko
return SetSetting<bool, ADDON_STATUS>(settingName, settingValue, m_separateSeasons, ADDON_STATUS_NEED_RESTART, ADDON_STATUS_OK);
else if (settingName == "showroot")
return SetSetting<bool, ADDON_STATUS>(settingName, settingValue, m_showRoot, ADDON_STATUS_NEED_RESTART, ADDON_STATUS_OK);
else if (settingName == "comskip")
return SetSetting<bool, ADDON_STATUS>(settingName, settingValue, m_comskip, ADDON_STATUS_NEED_RESTART, ADDON_STATUS_OK);
else if (settingName == "poster")
return SetSetting<bool, ADDON_STATUS>(settingName, settingValue, m_recordingPoster, ADDON_STATUS_NEED_RESTART, ADDON_STATUS_OK);
else if (settingName == "genrestring")
return SetSetting<bool, ADDON_STATUS>(settingName, settingValue, m_genreString, ADDON_STATUS_NEED_SETTINGS, ADDON_STATUS_OK);
else if (settingName == "host_mac")
Expand Down
1 change: 1 addition & 0 deletions src/InstanceSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/Recordings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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))
{
Expand Down
4 changes: 2 additions & 2 deletions src/buffers/RecordingBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/buffers/RecordingBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> m_isLive;

// recording start time
time_t m_recordingTime;

int m_streamId;
};
}
119 changes: 85 additions & 34 deletions src/pvrclient-nextpvr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ cPVRClientNextPVR::cPVRClientNextPVR(const CNextPVRAddon& base, const kodi::addo
m_supportsLiveTimeshift = false;
m_lastRecordingUpdateTime = std::numeric_limits<time_t>::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;
Expand All @@ -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<int64_t, timeshift::RecordingBuffer*>::iterator itr = m_multistreamRecording.begin();
while (itr != m_multistreamRecording.end())
{
CloseRecordedStream(itr->first);
itr = m_multistreamRecording.begin();
}
}
else
CloseLiveStream();
}
Expand All @@ -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();
Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand All @@ -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;
}
Expand All @@ -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);
}
Expand All @@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
9 changes: 7 additions & 2 deletions src/pvrclient-nextpvr.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<int64_t, timeshift::RecordingBuffer*> m_multistreamRecording;
mutable std::recursive_mutex m_mutexMulti;
int64_t m_streamCount = -1;

//Matrix changes
std::shared_ptr<InstanceSettings> m_settings;
Expand Down

0 comments on commit 5fe7193

Please sign in to comment.