/* * Copyright (c) 2006 elupus (Joakim Plate) * Copyright (C) 2006-2018 Team Kodi * This file is part of Kodi - https://kodi.tv * * SPDX-License-Identifier: GPL-2.0-or-later * See LICENSES/README.md for more information. */ #include "UPnPPlayer.h" #include "FileItem.h" #include "ServiceBroker.h" #include "ThumbLoader.h" #include "UPnP.h" #include "UPnPInternal.h" #include "application/Application.h" #include "cores/DataCacheCore.h" #include "dialogs/GUIDialogBusy.h" #include "input/actions/Action.h" #include "input/actions/ActionIDs.h" #include "messaging/ApplicationMessenger.h" #include "messaging/helpers/DialogHelper.h" #include "music/MusicThumbLoader.h" #include "threads/Event.h" #include "utils/StringUtils.h" #include "utils/TimeUtils.h" #include "utils/Variant.h" #include "utils/log.h" #include "video/VideoThumbLoader.h" #include "windowing/WinSystem.h" #include #include #include #include using namespace KODI::MESSAGING; using KODI::MESSAGING::HELPERS::DialogResponse; using namespace std::chrono_literals; NPT_SET_LOCAL_LOGGER("xbmc.upnp.player") namespace UPNP { class CUPnPPlayerController : public PLT_MediaControllerDelegate { public: CUPnPPlayerController(PLT_MediaController* control, PLT_DeviceDataReference& device, IPlayerCallback& callback) : m_control(control), m_transport(NULL), m_device(device), m_instance(0), m_callback(callback), m_postime(0), m_logger(CServiceBroker::GetLogging().GetLogger("CUPnPPlayerController")) { m_posinfo = {}; m_device->FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", m_transport); } void OnSetAVTransportURIResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override { if(NPT_FAILED(res)) m_logger->error("OnSetAVTransportURIResult failed"); m_resstatus = res; m_resevent.Set(); } void OnPlayResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override { if(NPT_FAILED(res)) m_logger->error("OnPlayResult failed"); m_resstatus = res; m_resevent.Set(); } void OnStopResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override { if(NPT_FAILED(res)) m_logger->error("OnStopResult failed"); m_resstatus = res; m_resevent.Set(); } void OnGetMediaInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_MediaInfo* info, void* userdata) override { if(NPT_FAILED(res) || info == NULL) m_logger->error("OnGetMediaInfoResult failed"); } void OnGetTransportInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_TransportInfo* info, void* userdata) override { std::unique_lock lock(m_section); if(NPT_FAILED(res)) { m_logger->error("OnGetTransportInfoResult failed"); m_trainfo.cur_speed = "0"; m_trainfo.cur_transport_state = "STOPPED"; m_trainfo.cur_transport_status = "ERROR_OCCURED"; } else m_trainfo = *info; m_traevnt.Set(); } void UpdatePositionInfo() { if(m_postime == 0 || m_postime > CTimeUtils::GetFrameTime()) return; m_control->GetTransportInfo(m_device, m_instance, this); m_control->GetPositionInfo(m_device, m_instance, this); m_postime = 0; } void OnGetPositionInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_PositionInfo* info, void* userdata) override { std::unique_lock lock(m_section); if(NPT_FAILED(res) || info == NULL) { m_logger->error("OnGetMediaInfoResult failed"); m_posinfo = PLT_PositionInfo(); } else m_posinfo = *info; m_postime = CTimeUtils::GetFrameTime() + 500; m_posevnt.Set(); } ~CUPnPPlayerController() override = default; PLT_MediaController* m_control; PLT_Service * m_transport; PLT_DeviceDataReference m_device; NPT_UInt32 m_instance; IPlayerCallback& m_callback; NPT_Result m_resstatus; CEvent m_resevent; CCriticalSection m_section; unsigned int m_postime; CEvent m_posevnt; PLT_PositionInfo m_posinfo; CEvent m_traevnt; PLT_TransportInfo m_trainfo; Logger m_logger; }; CUPnPPlayer::CUPnPPlayer(IPlayerCallback& callback, const char* uuid) : IPlayer(callback), m_control(NULL), m_delegate(NULL), m_started(false), m_stopremote(false), m_logger(CServiceBroker::GetLogging().GetLogger(StringUtils::Format("CUPnPPlayer[{}]", uuid))) { m_control = CUPnP::GetInstance()->m_MediaController; PLT_DeviceDataReference device; if(NPT_SUCCEEDED(m_control->FindRenderer(uuid, device))) { m_delegate = new CUPnPPlayerController(m_control, device, callback); CUPnP::RegisterUserdata(m_delegate); } else m_logger->error("couldn't find device as {}", uuid); CServiceBroker::GetWinSystem()->RegisterRenderLoop(this); } CUPnPPlayer::~CUPnPPlayer() { CServiceBroker::GetWinSystem()->UnregisterRenderLoop(this); CloseFile(); CUPnP::UnregisterUserdata(m_delegate); delete m_delegate; } static NPT_Result WaitOnEvent(CEvent& event, XbmcThreads::EndTime<>& timeout) { if (event.Wait(0ms)) return NPT_SUCCESS; if (!CGUIDialogBusy::WaitOnEvent(event)) return NPT_FAILURE; return NPT_SUCCESS; } int CUPnPPlayer::PlayFile(const CFileItem& file, const CPlayerOptions& options, XbmcThreads::EndTime<>& timeout) { CFileItem item(file); NPT_Reference thumb_loader; NPT_Reference obj; NPT_String path(file.GetPath().c_str()); NPT_String tmp, resource; EMediaControllerQuirks quirks = EMEDIACONTROLLERQUIRKS_NONE; NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed); if (file.IsVideoDb()) thumb_loader = NPT_Reference(new CVideoThumbLoader()); else if (item.IsMusicDb()) thumb_loader = NPT_Reference(new CMusicThumbLoader()); obj = BuildObject(item, path, false, thumb_loader, NULL, CUPnP::GetServer(), UPnPPlayer); if(obj.IsNull()) goto failed; NPT_CHECK_LABEL_SEVERE(PLT_Didl::ToDidl(*obj, "", tmp), failed_todidl); tmp.Insert(didl_header, 0); tmp.Append(didl_footer); quirks = GetMediaControllerQuirks(m_delegate->m_device.AsPointer()); if (quirks & EMEDIACONTROLLERQUIRKS_X_MKV) { for (NPT_Cardinal i=0; i< obj->m_Resources.GetItemCount(); i++) { if (obj->m_Resources[i].m_ProtocolInfo.GetContentType().Compare("video/x-matroska") == 0) { m_logger->debug("PlayFile({}): applying video/x-mkv quirk", file.GetPath()); NPT_String protocolInfo = obj->m_Resources[i].m_ProtocolInfo.ToString(); protocolInfo.Replace(":video/x-matroska:", ":video/x-mkv:"); obj->m_Resources[i].m_ProtocolInfo = PLT_ProtocolInfo(protocolInfo); } } } /* The resource uri's are stored in the Didl. We must choose the best resource * for the playback device */ NPT_Cardinal res_index; NPT_CHECK_LABEL_SEVERE(m_control->FindBestResource(m_delegate->m_device, *obj, res_index), failed_findbestresource); // get the transport info to evaluate the TransportState to be able to // determine whether we first need to call Stop() timeout.Set(timeout.GetInitialTimeoutValue()); NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device , m_delegate->m_instance , m_delegate), failed_gettransportinfo); NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed_gettransportinfo); if (m_delegate->m_trainfo.cur_transport_state != "NO_MEDIA_PRESENT" && m_delegate->m_trainfo.cur_transport_state != "STOPPED") { timeout.Set(timeout.GetInitialTimeoutValue()); NPT_CHECK_LABEL_SEVERE(m_control->Stop(m_delegate->m_device , m_delegate->m_instance , m_delegate), failed_stop); NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_stop); NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_stop); } timeout.Set(timeout.GetInitialTimeoutValue()); NPT_CHECK_LABEL_SEVERE(m_control->SetAVTransportURI(m_delegate->m_device , m_delegate->m_instance , obj->m_Resources[res_index].m_Uri , (const char*)tmp , m_delegate), failed_setavtransporturi); NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_setavtransporturi); NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_setavtransporturi); timeout.Set(timeout.GetInitialTimeoutValue()); NPT_CHECK_LABEL_SEVERE(m_control->Play(m_delegate->m_device , m_delegate->m_instance , "1" , m_delegate), failed_play); NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_play); NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_play); /* wait for PLAYING state */ timeout.Set(timeout.GetInitialTimeoutValue()); do { NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device , m_delegate->m_instance , m_delegate), failed_waitplaying); { std::unique_lock lock(m_delegate->m_section); if(m_delegate->m_trainfo.cur_transport_state == "PLAYING" || m_delegate->m_trainfo.cur_transport_state == "PAUSED_PLAYBACK") break; if(m_delegate->m_trainfo.cur_transport_state == "STOPPED" && m_delegate->m_trainfo.cur_transport_status != "OK") { m_logger->error("OpenFile({}): remote player signalled error", file.GetPath()); return NPT_FAILURE; } } NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed_waitplaying); } while(!timeout.IsTimePast()); if(options.starttime > 0) { /* many upnp units won't load file properly until after play (including xbmc) */ NPT_CHECK_LABEL(m_control->Seek(m_delegate->m_device , m_delegate->m_instance , "REL_TIME" , PLT_Didl::FormatTimeStamp((NPT_UInt32)options.starttime) , m_delegate), failed_seek); } return NPT_SUCCESS; failed_todidl: m_logger->error("PlayFile({}) failed to serialize item into DIDL-Lite", file.GetPath()); return NPT_FAILURE; failed_findbestresource: m_logger->error("PlayFile({}) failed to find a matching resource", file.GetPath()); return NPT_FAILURE; failed_gettransportinfo: m_logger->error("PlayFile({}): call to GetTransportInfo failed", file.GetPath()); return NPT_FAILURE; failed_stop: m_logger->error("PlayFile({}) failed to stop current playback", file.GetPath()); return NPT_FAILURE; failed_setavtransporturi: m_logger->error("PlayFile({}) failed to set the playback URI", file.GetPath()); return NPT_FAILURE; failed_play: m_logger->error("PlayFile({}) failed to start playback", file.GetPath()); return NPT_FAILURE; failed_waitplaying: m_logger->error("PlayFile({}) failed to wait for PLAYING state", file.GetPath()); return NPT_FAILURE; failed_seek: m_logger->error("PlayFile({}) failed to seek to start offset", file.GetPath()); return NPT_FAILURE; failed: m_logger->error("PlayFile({}) failed", file.GetPath()); return NPT_FAILURE; } bool CUPnPPlayer::OpenFile(const CFileItem& file, const CPlayerOptions& options) { XbmcThreads::EndTime<> timeout(10s); /* if no path we want to attach to a already playing player */ if(file.GetPath() == "") { NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device , m_delegate->m_instance , m_delegate), failed); NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed); /* make sure the attached player is actually playing */ { std::unique_lock lock(m_delegate->m_section); if(m_delegate->m_trainfo.cur_transport_state != "PLAYING" && m_delegate->m_trainfo.cur_transport_state != "PAUSED_PLAYBACK") goto failed; } } else NPT_CHECK_LABEL_SEVERE(PlayFile(file, options, timeout), failed); m_stopremote = true; m_started = true; m_callback.OnPlayBackStarted(file); m_callback.OnAVStarted(file); NPT_CHECK_LABEL_SEVERE(m_control->GetPositionInfo(m_delegate->m_device , m_delegate->m_instance , m_delegate), failed); NPT_CHECK_LABEL_SEVERE(m_control->GetMediaInfo(m_delegate->m_device , m_delegate->m_instance , m_delegate), failed); m_updateTimer.Set(0ms); return true; failed: m_logger->error("OpenFile({}) failed to open file", file.GetPath()); return false; } bool CUPnPPlayer::QueueNextFile(const CFileItem& file) { CFileItem item(file); NPT_Reference thumb_loader; NPT_Reference obj; NPT_String path(file.GetPath().c_str()); NPT_String tmp; if (file.IsVideoDb()) thumb_loader = NPT_Reference(new CVideoThumbLoader()); else if (item.IsMusicDb()) thumb_loader = NPT_Reference(new CMusicThumbLoader()); obj = BuildObject(item, path, false, thumb_loader, NULL, CUPnP::GetServer(), UPnPPlayer); if(!obj.IsNull()) { NPT_CHECK_LABEL_SEVERE(PLT_Didl::ToDidl(*obj, "", tmp), failed); tmp.Insert(didl_header, 0); tmp.Append(didl_footer); } NPT_CHECK_LABEL_WARNING(m_control->SetNextAVTransportURI(m_delegate->m_device , m_delegate->m_instance , file.GetPath().c_str() , (const char*)tmp , m_delegate), failed); if (!m_delegate->m_resevent.Wait(10000ms)) goto failed; NPT_CHECK_LABEL_WARNING(m_delegate->m_resstatus, failed); return true; failed: m_logger->error("QueueNextFile({}) failed to queue file", file.GetPath()); return false; } bool CUPnPPlayer::CloseFile(bool reopen) { NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed); if(m_stopremote) { NPT_CHECK_LABEL(m_control->Stop(m_delegate->m_device , m_delegate->m_instance , m_delegate), failed); if (!m_delegate->m_resevent.Wait(10000ms)) goto failed; NPT_CHECK_LABEL(m_delegate->m_resstatus, failed); } if(m_started) { m_started = false; m_callback.OnPlayBackStopped(); } return true; failed: m_logger->error("CloseFile - unable to stop playback"); return false; } void CUPnPPlayer::Pause() { if(IsPaused()) { NPT_CHECK_LABEL(m_control->Play(m_delegate->m_device , m_delegate->m_instance , "1" , m_delegate), failed); CDataCacheCore::GetInstance().SetSpeed(1.0, 1.0); } else { NPT_CHECK_LABEL(m_control->Pause(m_delegate->m_device , m_delegate->m_instance , m_delegate), failed); CDataCacheCore::GetInstance().SetSpeed(1.0, 0.0); } return; failed: m_logger->error("CloseFile - unable to pause/unpause playback"); } void CUPnPPlayer::SeekTime(int64_t ms) { NPT_CHECK_LABEL(m_control->Seek(m_delegate->m_device , m_delegate->m_instance , "REL_TIME", PLT_Didl::FormatTimeStamp((NPT_UInt32)(ms / 1000)) , m_delegate), failed); CDataCacheCore::GetInstance().SeekFinished(0); return; failed: m_logger->error("SeekTime - unable to seek playback"); } float CUPnPPlayer::GetPercentage() { int64_t tot = GetTotalTime(); if(tot) return 100.0f * GetTime() / tot; else return 0.0f; } void CUPnPPlayer::SeekPercentage(float percent) { int64_t tot = GetTotalTime(); if (tot) SeekTime((int64_t)(tot * percent / 100)); } void CUPnPPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride) { } void CUPnPPlayer::DoAudioWork() { NPT_String data; NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed); m_delegate->UpdatePositionInfo(); if(m_started) { NPT_String uri, meta; NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("CurrentTrackURI", uri), failed); NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("CurrentTrackMetadata", meta), failed); if(m_current_uri != (const char*)uri || m_current_meta != (const char*)meta) { m_current_uri = (const char*)uri; m_current_meta = (const char*)meta; CFileItemPtr item = GetFileItem(uri, meta); g_application.CurrentFileItem() = *item; CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 0, -1, static_cast(new CFileItem(*item))); } NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed); if(data == "STOPPED") { m_started = false; m_callback.OnPlayBackEnded(); } } return; failed: return; } bool CUPnPPlayer::IsPlaying() const { NPT_String data; NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed); NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed); return data != "STOPPED"; failed: return false; } bool CUPnPPlayer::IsPaused() const { NPT_String data; NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed); NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed); return data == "PAUSED_PLAYBACK"; failed: return false; } void CUPnPPlayer::SetVolume(float volume) { NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed); NPT_CHECK_LABEL(m_control->SetVolume(m_delegate->m_device , m_delegate->m_instance , "Master", (int)(volume * 100) , m_delegate), failed); return; failed: m_logger->error("- unable to set volume"); } int64_t CUPnPPlayer::GetTime() { NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed); return m_delegate->m_posinfo.rel_time.ToMillis(); failed: return 0; } int64_t CUPnPPlayer::GetTotalTime() { NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed); return m_delegate->m_posinfo.track_duration.ToMillis(); failed: return 0; }; bool CUPnPPlayer::OnAction(const CAction &action) { switch (action.GetID()) { case ACTION_STOP: if(IsPlaying()) { //stop on remote system m_stopremote = HELPERS::ShowYesNoDialogText(CVariant{37022}, CVariant{37023}) == DialogResponse::CHOICE_YES; return false; /* let normal code handle the action */ } [[fallthrough]]; default: return false; } } void CUPnPPlayer::SetSpeed(float speed) { } void CUPnPPlayer::FrameMove() { if (m_updateTimer.IsTimePast()) { CDataCacheCore::GetInstance().SetPlayTimes(0, GetTime(), 0, GetTotalTime()); m_updateTimer.Set(500ms); } } } /* namespace UPNP */