diff options
Diffstat (limited to 'xbmc/peripherals/devices/PeripheralCecAdapter.cpp')
-rw-r--r-- | xbmc/peripherals/devices/PeripheralCecAdapter.cpp | 1861 |
1 files changed, 1861 insertions, 0 deletions
diff --git a/xbmc/peripherals/devices/PeripheralCecAdapter.cpp b/xbmc/peripherals/devices/PeripheralCecAdapter.cpp new file mode 100644 index 0000000..bc62bea --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralCecAdapter.cpp @@ -0,0 +1,1861 @@ +/* + * Copyright (C) 2005-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 "PeripheralCecAdapter.h" + +#include "ServiceBroker.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationEnums.h" +#include "application/ApplicationPlayer.h" +#include "application/ApplicationPowerHandling.h" +#include "application/ApplicationVolumeHandling.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "input/remote/IRRemote.h" +#include "messaging/ApplicationMessenger.h" +#include "pictures/GUIWindowSlideShow.h" +#include "utils/JobManager.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "xbmc/interfaces/AnnouncementManager.h" + +#include <mutex> + +#include <libcec/cec.h> + +using namespace PERIPHERALS; +using namespace CEC; +using namespace ANNOUNCEMENT; +using namespace std::chrono_literals; + +#define CEC_LIB_SUPPORTED_VERSION LIBCEC_VERSION_TO_UINT(4, 0, 0) + +/* time in seconds to ignore standby commands from devices after the screensaver has been activated + */ +#define SCREENSAVER_TIMEOUT 20 +#define VOLUME_CHANGE_TIMEOUT 250 +#define VOLUME_REFRESH_TIMEOUT 100 + +#define LOCALISED_ID_TV 36037 +#define LOCALISED_ID_AVR 36038 +#define LOCALISED_ID_TV_AVR 36039 +#define LOCALISED_ID_STOP 36044 +#define LOCALISED_ID_PAUSE 36045 +#define LOCALISED_ID_POWEROFF 13005 +#define LOCALISED_ID_SUSPEND 13011 +#define LOCALISED_ID_HIBERNATE 13010 +#define LOCALISED_ID_QUIT 13009 +#define LOCALISED_ID_IGNORE 36028 +#define LOCALISED_ID_RECORDING_DEVICE 36051 +#define LOCALISED_ID_PLAYBACK_DEVICE 36052 +#define LOCALISED_ID_TUNER_DEVICE 36053 + +#define LOCALISED_ID_NONE 231 + +/* time in seconds to suppress source activation after receiving OnStop */ +#define CEC_SUPPRESS_ACTIVATE_SOURCE_AFTER_ON_STOP 2 + +CPeripheralCecAdapter::CPeripheralCecAdapter(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheralHID(manager, scanResult, bus), CThread("CECAdapter"), m_cecAdapter(NULL) +{ + ResetMembers(); + m_features.push_back(FEATURE_CEC); + m_strComPort = scanResult.m_strLocation; +} + +CPeripheralCecAdapter::~CPeripheralCecAdapter(void) +{ + { + std::unique_lock<CCriticalSection> lock(m_critSection); + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + m_bStop = true; + } + + StopThread(true); + delete m_queryThread; + + if (m_cecAdapter) + { + CECDestroy(m_cecAdapter); + m_cecAdapter = NULL; + } +} + +void CPeripheralCecAdapter::ResetMembers(void) +{ + if (m_cecAdapter) + CECDestroy(m_cecAdapter); + m_cecAdapter = NULL; + m_bStarted = false; + m_bHasButton = false; + m_bIsReady = false; + m_bHasConnectedAudioSystem = false; + m_strMenuLanguage = "???"; + m_lastKeypress = {}; + m_lastChange = VOLUME_CHANGE_NONE; + m_iExitCode = EXITCODE_QUIT; + + //! @todo fetch the correct initial value when system audiostatus is + //! implemented in libCEC + m_bIsMuted = false; + + m_bGoingToStandby = false; + m_bIsRunning = false; + m_bDeviceRemoved = false; + m_bActiveSourcePending = false; + m_bStandbyPending = false; + m_bActiveSourceBeforeStandby = false; + m_bOnPlayReceived = false; + m_bPlaybackPaused = false; + m_queryThread = NULL; + m_bPowerOnScreensaver = false; + m_bUseTVMenuLanguage = false; + m_bSendInactiveSource = false; + m_bPowerOffScreensaver = false; + m_bShutdownOnStandby = false; + + m_currentButton.iButton = 0; + m_currentButton.iDuration = 0; + m_standbySent.SetValid(false); + m_configuration.Clear(); +} + +void CPeripheralCecAdapter::Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) +{ + if (flag == ANNOUNCEMENT::System && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnQuit" && m_bIsReady) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iExitCode = static_cast<int>(data["exitcode"].asInteger(EXITCODE_QUIT)); + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + StopThread(false); + } + else if (flag == ANNOUNCEMENT::GUI && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnScreensaverDeactivated" && m_bIsReady) + { + bool bIgnoreDeactivate(false); + if (data["shuttingdown"].isBoolean()) + { + // don't respond to the deactivation if we are just going to suspend/shutdown anyway + // the tv will not have time to switch on before being told to standby and + // may not action the standby command. + bIgnoreDeactivate = data["shuttingdown"].asBoolean(); + if (bIgnoreDeactivate) + CLog::Log(LOGDEBUG, "{} - ignoring OnScreensaverDeactivated for power action", + __FUNCTION__); + } + if (m_bPowerOnScreensaver && !bIgnoreDeactivate && m_configuration.bActivateSource) + { + ActivateSource(); + } + } + else if (flag == ANNOUNCEMENT::GUI && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnScreensaverActivated" && m_bIsReady) + { + // Don't put devices to standby if application is currently playing + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (!appPlayer->IsPlaying() && m_bPowerOffScreensaver) + { + // only power off when we're the active source + if (m_cecAdapter->IsLibCECActiveSource()) + StandbyDevices(); + } + } + else if (flag == ANNOUNCEMENT::System && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnSleep") + { + // this will also power off devices when we're the active source + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bGoingToStandby = true; + } + StopThread(); + } + else if (flag == ANNOUNCEMENT::System && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnWake") + { + CLog::Log(LOGDEBUG, "{} - reconnecting to the CEC adapter after standby mode", __FUNCTION__); + if (ReopenConnection()) + { + bool bActivate(false); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bActivate = m_bActiveSourceBeforeStandby; + m_bActiveSourceBeforeStandby = false; + } + if (bActivate) + ActivateSource(); + } + } + else if (flag == ANNOUNCEMENT::Player && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnStop") + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_preventActivateSourceOnPlay = CDateTime::GetCurrentDateTime(); + m_bOnPlayReceived = false; + } + else if (flag == ANNOUNCEMENT::Player && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + (message == "OnPlay" || message == "OnResume")) + { + // activate the source when playback started, and the option is enabled + bool bActivateSource(false); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bActivateSource = (m_configuration.bActivateSource && !m_bOnPlayReceived && + !m_cecAdapter->IsLibCECActiveSource() && + (!m_preventActivateSourceOnPlay.IsValid() || + CDateTime::GetCurrentDateTime() - m_preventActivateSourceOnPlay > + CDateTimeSpan(0, 0, 0, CEC_SUPPRESS_ACTIVATE_SOURCE_AFTER_ON_STOP))); + m_bOnPlayReceived = true; + } + if (bActivateSource) + ActivateSource(); + } +} + +bool CPeripheralCecAdapter::InitialiseFeature(const PeripheralFeature feature) +{ + if (feature == FEATURE_CEC && !m_bStarted && GetSettingBool("enabled")) + { + // hide settings that have an override set + if (!GetSettingString("wake_devices_advanced").empty()) + SetSettingVisible("wake_devices", false); + if (!GetSettingString("standby_devices_advanced").empty()) + SetSettingVisible("standby_devices", false); + + SetConfigurationFromSettings(); + m_callbacks.Clear(); + m_callbacks.logMessage = &CecLogMessage; + m_callbacks.keyPress = &CecKeyPress; + m_callbacks.commandReceived = &CecCommand; + m_callbacks.configurationChanged = &CecConfiguration; + m_callbacks.alert = &CecAlert; + m_callbacks.sourceActivated = &CecSourceActivated; + m_configuration.callbackParam = this; + m_configuration.callbacks = &m_callbacks; + + m_cecAdapter = CECInitialise(&m_configuration); + + if (m_configuration.serverVersion < CEC_LIB_SUPPORTED_VERSION) + { + /* unsupported libcec version */ + CLog::Log( + LOGERROR, + "Detected version of libCEC interface ({0:x}) is lower than the supported version {1:x}", + m_cecAdapter ? m_configuration.serverVersion : -1, CEC_LIB_SUPPORTED_VERSION); + + // display warning: incompatible libCEC + std::string strMessage = StringUtils::Format( + g_localizeStrings.Get(36040), m_cecAdapter ? m_configuration.serverVersion : -1, + CEC_LIB_SUPPORTED_VERSION); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(36000), + strMessage); + m_bError = true; + if (m_cecAdapter) + CECDestroy(m_cecAdapter); + m_cecAdapter = NULL; + + m_features.clear(); + return false; + } + else + { + CLog::Log(LOGDEBUG, "{} - using libCEC v{}", __FUNCTION__, + m_cecAdapter->VersionToString(m_configuration.serverVersion)); + SetVersionInfo(m_configuration); + } + + m_bStarted = true; + Create(); + } + + return CPeripheral::InitialiseFeature(feature); +} + +void CPeripheralCecAdapter::SetVersionInfo(const libcec_configuration& configuration) +{ + m_strVersionInfo = StringUtils::Format("libCEC {} - firmware v{}", + m_cecAdapter->VersionToString(configuration.serverVersion), + configuration.iFirmwareVersion); + + // append firmware build date + if (configuration.iFirmwareBuildDate != CEC_FW_BUILD_UNKNOWN) + { + CDateTime dt((time_t)configuration.iFirmwareBuildDate); + m_strVersionInfo += StringUtils::Format(" ({})", dt.GetAsDBDate()); + } +} + +bool CPeripheralCecAdapter::OpenConnection(void) +{ + bool bIsOpen(false); + + if (!GetSettingBool("enabled")) + { + CLog::Log(LOGDEBUG, "{} - CEC adapter is disabled in peripheral settings", __FUNCTION__); + m_bStarted = false; + return bIsOpen; + } + + // open the CEC adapter + CLog::Log(LOGDEBUG, "{} - opening a connection to the CEC adapter: {}", __FUNCTION__, + m_strComPort); + + // scanning the CEC bus takes about 5 seconds, so display a notification to inform users that + // we're busy + std::string strMessage = + StringUtils::Format(g_localizeStrings.Get(21336), g_localizeStrings.Get(36000)); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000), + strMessage); + + bool bConnectionFailedDisplayed(false); + + while (!m_bStop && !bIsOpen) + { + if ((bIsOpen = m_cecAdapter->Open(m_strComPort.c_str(), 10000)) == false) + { + // display warning: couldn't initialise libCEC + CLog::Log(LOGERROR, "{} - could not opening a connection to the CEC adapter", __FUNCTION__); + if (!bConnectionFailedDisplayed) + CGUIDialogKaiToast::QueueNotification( + CGUIDialogKaiToast::Error, g_localizeStrings.Get(36000), g_localizeStrings.Get(36012)); + bConnectionFailedDisplayed = true; + + CThread::Sleep(10000ms); + } + } + + if (bIsOpen) + { + CLog::Log(LOGDEBUG, "{} - connection to the CEC adapter opened", __FUNCTION__); + + // read the configuration + libcec_configuration config; + if (m_cecAdapter->GetCurrentConfiguration(&config)) + { + // update the local configuration + std::unique_lock<CCriticalSection> lock(m_critSection); + SetConfigurationFromLibCEC(config); + } + } + + return bIsOpen; +} + +void CPeripheralCecAdapter::Process(void) +{ + if (!OpenConnection()) + return; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iExitCode = EXITCODE_QUIT; + m_bGoingToStandby = false; + m_bIsRunning = true; + m_bActiveSourceBeforeStandby = false; + } + + CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this); + + m_queryThread = new CPeripheralCecAdapterUpdateThread(this, &m_configuration); + m_queryThread->Create(false); + + while (!m_bStop) + { + if (!m_bStop) + ProcessVolumeChange(); + + if (!m_bStop) + ProcessActivateSource(); + + if (!m_bStop) + ProcessStandbyDevices(); + + if (!m_bStop) + CThread::Sleep(5ms); + } + + m_queryThread->StopThread(true); + + bool bSendStandbyCommands(false); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bSendStandbyCommands = m_iExitCode != EXITCODE_REBOOT && m_iExitCode != EXITCODE_RESTARTAPP && + !m_bDeviceRemoved && + (!m_bGoingToStandby || GetSettingBool("standby_tv_on_pc_standby")) && + GetSettingBool("enabled"); + + if (m_bGoingToStandby) + m_bActiveSourceBeforeStandby = m_cecAdapter->IsLibCECActiveSource(); + } + + if (bSendStandbyCommands) + { + if (m_cecAdapter->IsLibCECActiveSource()) + { + if (!m_configuration.powerOffDevices.IsEmpty()) + { + CLog::Log(LOGDEBUG, "{} - sending standby commands", __FUNCTION__); + m_standbySent = CDateTime::GetCurrentDateTime(); + m_cecAdapter->StandbyDevices(); + } + else if (m_bSendInactiveSource) + { + CLog::Log(LOGDEBUG, "{} - sending inactive source commands", __FUNCTION__); + m_cecAdapter->SetInactiveView(); + } + } + else + { + CLog::Log(LOGDEBUG, "{} - XBMC is not the active source, not sending any standby commands", + __FUNCTION__); + } + } + + m_cecAdapter->Close(); + + CLog::Log(LOGDEBUG, "{} - CEC adapter processor thread ended", __FUNCTION__); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bStarted = false; + m_bIsRunning = false; + } +} + +bool CPeripheralCecAdapter::HasAudioControl(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bHasConnectedAudioSystem; +} + +void CPeripheralCecAdapter::SetAudioSystemConnected(bool bSetTo) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bHasConnectedAudioSystem = bSetTo; +} + +void CPeripheralCecAdapter::ProcessVolumeChange(void) +{ + bool bSendRelease(false); + CecVolumeChange pendingVolumeChange = VOLUME_CHANGE_NONE; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + auto now = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastKeypress); + if (!m_volumeChangeQueue.empty()) + { + /* get the first change from the queue */ + pendingVolumeChange = m_volumeChangeQueue.front(); + m_volumeChangeQueue.pop(); + + /* remove all dupe entries */ + while (!m_volumeChangeQueue.empty() && m_volumeChangeQueue.front() == pendingVolumeChange) + m_volumeChangeQueue.pop(); + + /* send another keypress after VOLUME_REFRESH_TIMEOUT ms */ + + bool bRefresh(duration.count() > VOLUME_REFRESH_TIMEOUT); + + /* only send the keypress when it hasn't been sent yet */ + if (pendingVolumeChange != m_lastChange) + { + m_lastKeypress = std::chrono::steady_clock::now(); + m_lastChange = pendingVolumeChange; + } + else if (bRefresh) + { + m_lastKeypress = std::chrono::steady_clock::now(); + pendingVolumeChange = m_lastChange; + } + else + pendingVolumeChange = VOLUME_CHANGE_NONE; + } + else if (m_lastKeypress.time_since_epoch().count() > 0 && + duration.count() > VOLUME_CHANGE_TIMEOUT) + { + /* send a key release */ + m_lastKeypress = {}; + bSendRelease = true; + m_lastChange = VOLUME_CHANGE_NONE; + } + } + + switch (pendingVolumeChange) + { + case VOLUME_CHANGE_UP: + m_cecAdapter->SendKeypress(CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_VOLUME_UP, false); + break; + case VOLUME_CHANGE_DOWN: + m_cecAdapter->SendKeypress(CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_VOLUME_DOWN, false); + break; + case VOLUME_CHANGE_MUTE: + m_cecAdapter->SendKeypress(CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_MUTE, false); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bIsMuted = !m_bIsMuted; + } + break; + case VOLUME_CHANGE_NONE: + if (bSendRelease) + m_cecAdapter->SendKeyRelease(CECDEVICE_AUDIOSYSTEM, false); + break; + } +} + +void CPeripheralCecAdapter::VolumeUp(void) +{ + if (HasAudioControl()) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_volumeChangeQueue.push(VOLUME_CHANGE_UP); + } +} + +void CPeripheralCecAdapter::VolumeDown(void) +{ + if (HasAudioControl()) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_volumeChangeQueue.push(VOLUME_CHANGE_DOWN); + } +} + +void CPeripheralCecAdapter::ToggleMute(void) +{ + if (HasAudioControl()) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_volumeChangeQueue.push(VOLUME_CHANGE_MUTE); + } +} + +bool CPeripheralCecAdapter::IsMuted(void) +{ + if (HasAudioControl()) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bIsMuted; + } + return false; +} + +void CPeripheralCecAdapter::SetMenuLanguage(const char* strLanguage) +{ + if (StringUtils::EqualsNoCase(m_strMenuLanguage, strLanguage)) + return; + + std::string strGuiLanguage; + + if (!strcmp(strLanguage, "bul")) + strGuiLanguage = "bg_bg"; + else if (!strcmp(strLanguage, "hrv")) + strGuiLanguage = "hr_hr"; + else if (!strcmp(strLanguage, "cze")) + strGuiLanguage = "cs_cz"; + else if (!strcmp(strLanguage, "dan")) + strGuiLanguage = "da_dk"; + else if (!strcmp(strLanguage, "deu")) + strGuiLanguage = "de_de"; + else if (!strcmp(strLanguage, "dut")) + strGuiLanguage = "nl_nl"; + else if (!strcmp(strLanguage, "eng")) + strGuiLanguage = "en_gb"; + else if (!strcmp(strLanguage, "fin")) + strGuiLanguage = "fi_fi"; + else if (!strcmp(strLanguage, "fre")) + strGuiLanguage = "fr_fr"; + else if (!strcmp(strLanguage, "ger")) + strGuiLanguage = "de_de"; + else if (!strcmp(strLanguage, "gre")) + strGuiLanguage = "el_gr"; + else if (!strcmp(strLanguage, "hun")) + strGuiLanguage = "hu_hu"; + else if (!strcmp(strLanguage, "ita")) + strGuiLanguage = "it_it"; + else if (!strcmp(strLanguage, "nor")) + strGuiLanguage = "nb_no"; + else if (!strcmp(strLanguage, "pol")) + strGuiLanguage = "pl_pl"; + else if (!strcmp(strLanguage, "por")) + strGuiLanguage = "pt_pt"; + else if (!strcmp(strLanguage, "rum")) + strGuiLanguage = "ro_ro"; + else if (!strcmp(strLanguage, "rus")) + strGuiLanguage = "ru_ru"; + else if (!strcmp(strLanguage, "srp")) + strGuiLanguage = "sr_rs@latin"; + else if (!strcmp(strLanguage, "slo")) + strGuiLanguage = "sk_sk"; + else if (!strcmp(strLanguage, "slv")) + strGuiLanguage = "sl_si"; + else if (!strcmp(strLanguage, "spa")) + strGuiLanguage = "es_es"; + else if (!strcmp(strLanguage, "swe")) + strGuiLanguage = "sv_se"; + else if (!strcmp(strLanguage, "tur")) + strGuiLanguage = "tr_tr"; + + if (!strGuiLanguage.empty()) + { + strGuiLanguage = "resource.language." + strGuiLanguage; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SETLANGUAGE, -1, -1, nullptr, strGuiLanguage); + CLog::Log(LOGDEBUG, "{} - language set to '{}'", __FUNCTION__, strGuiLanguage); + } + else + CLog::Log(LOGWARNING, "{} - TV menu language set to unknown value '{}'", __FUNCTION__, + strLanguage); +} + +void CPeripheralCecAdapter::OnTvStandby(void) +{ + int iActionOnTvStandby = GetSettingInt("standby_pc_on_tv_standby"); + switch (iActionOnTvStandby) + { + case LOCALISED_ID_POWEROFF: + m_bStarted = false; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SYSTEM_POWERDOWN, TMSG_SHUTDOWN); + break; + case LOCALISED_ID_SUSPEND: + m_bStarted = false; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SYSTEM_POWERDOWN, TMSG_SUSPEND); + break; + case LOCALISED_ID_HIBERNATE: + m_bStarted = false; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SYSTEM_POWERDOWN, TMSG_HIBERNATE); + break; + case LOCALISED_ID_QUIT: + m_bStarted = false; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + break; + case LOCALISED_ID_PAUSE: + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PAUSE_IF_PLAYING); + break; + case LOCALISED_ID_STOP: + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlaying()) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_STOP); + break; + } + case LOCALISED_ID_IGNORE: + break; + default: + CLog::Log(LOGERROR, "{} - Unexpected [standby_pc_on_tv_standby] setting value", __FUNCTION__); + break; + } +} + +void CPeripheralCecAdapter::CecCommand(void* cbParam, const cec_command* command) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + if (adapter->m_bIsReady) + { + switch (command->opcode) + { + case CEC_OPCODE_STANDBY: + if (command->initiator == CECDEVICE_TV && + (!adapter->m_standbySent.IsValid() || + CDateTime::GetCurrentDateTime() - adapter->m_standbySent > + CDateTimeSpan(0, 0, 0, SCREENSAVER_TIMEOUT))) + { + adapter->OnTvStandby(); + } + break; + case CEC_OPCODE_SET_MENU_LANGUAGE: + if (adapter->m_bUseTVMenuLanguage == 1 && command->initiator == CECDEVICE_TV && + command->parameters.size == 3) + { + char strNewLanguage[4]; + for (int iPtr = 0; iPtr < 3; iPtr++) + strNewLanguage[iPtr] = command->parameters[iPtr]; + strNewLanguage[3] = 0; + adapter->SetMenuLanguage(strNewLanguage); + } + break; + case CEC_OPCODE_DECK_CONTROL: + if (command->initiator == CECDEVICE_TV && command->parameters.size == 1 && + command->parameters[0] == CEC_DECK_CONTROL_MODE_STOP) + { + cec_keypress key; + key.duration = 500; + key.keycode = CEC_USER_CONTROL_CODE_STOP; + adapter->PushCecKeypress(key); + } + break; + case CEC_OPCODE_PLAY: + if (command->initiator == CECDEVICE_TV && command->parameters.size == 1) + { + if (command->parameters[0] == CEC_PLAY_MODE_PLAY_FORWARD) + { + cec_keypress key; + key.duration = 500; + key.keycode = CEC_USER_CONTROL_CODE_PLAY; + adapter->PushCecKeypress(key); + } + else if (command->parameters[0] == CEC_PLAY_MODE_PLAY_STILL) + { + cec_keypress key; + key.duration = 500; + key.keycode = CEC_USER_CONTROL_CODE_PAUSE; + adapter->PushCecKeypress(key); + } + } + break; + default: + break; + } + } +} + +void CPeripheralCecAdapter::CecConfiguration(void* cbParam, const libcec_configuration* config) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + std::unique_lock<CCriticalSection> lock(adapter->m_critSection); + adapter->SetConfigurationFromLibCEC(*config); +} + +void CPeripheralCecAdapter::CecAlert(void* cbParam, + const libcec_alert alert, + const libcec_parameter data) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + bool bReopenConnection(false); + int iAlertString(0); + switch (alert) + { + case CEC_ALERT_SERVICE_DEVICE: + iAlertString = 36027; + break; + case CEC_ALERT_CONNECTION_LOST: + bReopenConnection = true; + iAlertString = 36030; + break; +#if defined(CEC_ALERT_PERMISSION_ERROR) + case CEC_ALERT_PERMISSION_ERROR: + bReopenConnection = true; + iAlertString = 36031; + break; + case CEC_ALERT_PORT_BUSY: + bReopenConnection = true; + iAlertString = 36032; + break; +#endif + default: + break; + } + + // display the alert + if (iAlertString) + { + std::string strLog(g_localizeStrings.Get(iAlertString)); + if (data.paramType == CEC_PARAMETER_TYPE_STRING && data.paramData) + strLog += StringUtils::Format(" - {}", (const char*)data.paramData); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000), + strLog); + } + + if (bReopenConnection) + { + // Reopen the connection asynchronously. Otherwise a deadlock may occur. + // Reconnect means destruction and recreation of our libcec instance, but libcec + // calls this callback function synchronously and must not be destroyed meanwhile. + adapter->ReopenConnection(true); + } +} + +void CPeripheralCecAdapter::CecKeyPress(void* cbParam, const cec_keypress* key) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!!adapter) + adapter->PushCecKeypress(*key); +} + +void CPeripheralCecAdapter::GetNextKey(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bHasButton = false; + if (m_bIsReady) + { + std::vector<CecButtonPress>::iterator it = m_buttonQueue.begin(); + if (it != m_buttonQueue.end()) + { + m_currentButton = (*it); + m_buttonQueue.erase(it); + m_bHasButton = true; + } + } +} + +void CPeripheralCecAdapter::PushCecKeypress(const CecButtonPress& key) +{ + CLog::Log(LOGDEBUG, "{} - received key {:2x} duration {}", __FUNCTION__, key.iButton, + key.iDuration); + + std::unique_lock<CCriticalSection> lock(m_critSection); + // avoid the queue getting too long + if (m_configuration.iButtonRepeatRateMs && m_buttonQueue.size() > 5) + return; + if (m_configuration.iButtonRepeatRateMs == 0 && key.iDuration > 0) + { + if (m_currentButton.iButton == key.iButton && m_currentButton.iDuration == 0) + { + // update the duration + if (m_bHasButton) + m_currentButton.iDuration = key.iDuration; + // ignore this one, since it's already been handled by xbmc + return; + } + // if we received a keypress with a duration set, try to find the same one without a duration + // set, and replace it + for (std::vector<CecButtonPress>::reverse_iterator it = m_buttonQueue.rbegin(); + it != m_buttonQueue.rend(); ++it) + { + if ((*it).iButton == key.iButton) + { + if ((*it).iDuration == 0) + { + // replace this entry + (*it).iDuration = key.iDuration; + return; + } + // add a new entry + break; + } + } + } + + m_buttonQueue.push_back(key); +} + +void CPeripheralCecAdapter::PushCecKeypress(const cec_keypress& key) +{ + CecButtonPress xbmcKey; + xbmcKey.iDuration = key.duration; + + switch (key.keycode) + { + case CEC_USER_CONTROL_CODE_SELECT: + xbmcKey.iButton = XINPUT_IR_REMOTE_SELECT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_UP; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_DOWN; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_LEFT: + xbmcKey.iButton = XINPUT_IR_REMOTE_LEFT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_LEFT_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_LEFT; + PushCecKeypress(xbmcKey); + xbmcKey.iButton = XINPUT_IR_REMOTE_UP; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_LEFT_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_LEFT; + PushCecKeypress(xbmcKey); + xbmcKey.iButton = XINPUT_IR_REMOTE_DOWN; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_RIGHT: + xbmcKey.iButton = XINPUT_IR_REMOTE_RIGHT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_RIGHT_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_RIGHT; + PushCecKeypress(xbmcKey); + xbmcKey.iButton = XINPUT_IR_REMOTE_UP; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_RIGHT_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_RIGHT; + PushCecKeypress(xbmcKey); + xbmcKey.iButton = XINPUT_IR_REMOTE_DOWN; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_SETUP_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_TITLE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_CONTENTS_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_CONTENTS_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_ROOT_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_ROOT_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_TOP_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_TOP_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_DVD_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_DVD_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_FAVORITE_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_EXIT: + xbmcKey.iButton = XINPUT_IR_REMOTE_BACK; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_ENTER: + xbmcKey.iButton = XINPUT_IR_REMOTE_ENTER; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_CHANNEL_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_MINUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_CHANNEL_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_PLUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PREVIOUS_CHANNEL: + xbmcKey.iButton = XINPUT_IR_REMOTE_TELETEXT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_SOUND_SELECT: + xbmcKey.iButton = XINPUT_IR_REMOTE_LANGUAGE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_POWER: + case CEC_USER_CONTROL_CODE_POWER_TOGGLE_FUNCTION: + case CEC_USER_CONTROL_CODE_POWER_OFF_FUNCTION: + xbmcKey.iButton = XINPUT_IR_REMOTE_POWER; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_VOLUME_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_VOLUME_PLUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_VOLUME_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_VOLUME_MINUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_MUTE: + case CEC_USER_CONTROL_CODE_MUTE_FUNCTION: + case CEC_USER_CONTROL_CODE_RESTORE_VOLUME_FUNCTION: + xbmcKey.iButton = XINPUT_IR_REMOTE_MUTE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PLAY: + xbmcKey.iButton = XINPUT_IR_REMOTE_PLAY; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_STOP: + xbmcKey.iButton = XINPUT_IR_REMOTE_STOP; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PAUSE: + xbmcKey.iButton = XINPUT_IR_REMOTE_PAUSE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_REWIND: + xbmcKey.iButton = XINPUT_IR_REMOTE_REVERSE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_FAST_FORWARD: + xbmcKey.iButton = XINPUT_IR_REMOTE_FORWARD; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER0: + xbmcKey.iButton = XINPUT_IR_REMOTE_0; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER1: + xbmcKey.iButton = XINPUT_IR_REMOTE_1; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER2: + xbmcKey.iButton = XINPUT_IR_REMOTE_2; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER3: + xbmcKey.iButton = XINPUT_IR_REMOTE_3; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER4: + xbmcKey.iButton = XINPUT_IR_REMOTE_4; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER5: + xbmcKey.iButton = XINPUT_IR_REMOTE_5; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER6: + xbmcKey.iButton = XINPUT_IR_REMOTE_6; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER7: + xbmcKey.iButton = XINPUT_IR_REMOTE_7; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER8: + xbmcKey.iButton = XINPUT_IR_REMOTE_8; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER9: + xbmcKey.iButton = XINPUT_IR_REMOTE_9; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_RECORD: + xbmcKey.iButton = XINPUT_IR_REMOTE_RECORD; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_CLEAR: + xbmcKey.iButton = XINPUT_IR_REMOTE_CLEAR; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_DISPLAY_INFORMATION: + xbmcKey.iButton = XINPUT_IR_REMOTE_INFO; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PAGE_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_PLUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PAGE_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_MINUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_FORWARD: + xbmcKey.iButton = XINPUT_IR_REMOTE_SKIP_PLUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_BACKWARD: + xbmcKey.iButton = XINPUT_IR_REMOTE_SKIP_MINUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_F1_BLUE: + xbmcKey.iButton = XINPUT_IR_REMOTE_BLUE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_F2_RED: + xbmcKey.iButton = XINPUT_IR_REMOTE_RED; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_F3_GREEN: + xbmcKey.iButton = XINPUT_IR_REMOTE_GREEN; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_F4_YELLOW: + xbmcKey.iButton = XINPUT_IR_REMOTE_YELLOW; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_ELECTRONIC_PROGRAM_GUIDE: + xbmcKey.iButton = XINPUT_IR_REMOTE_GUIDE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_AN_CHANNELS_LIST: + xbmcKey.iButton = XINPUT_IR_REMOTE_LIVE_TV; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NEXT_FAVORITE: + case CEC_USER_CONTROL_CODE_DOT: + case CEC_USER_CONTROL_CODE_AN_RETURN: + xbmcKey.iButton = XINPUT_IR_REMOTE_TITLE; // context menu + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_DATA: + xbmcKey.iButton = XINPUT_IR_REMOTE_TELETEXT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_SUB_PICTURE: + xbmcKey.iButton = XINPUT_IR_REMOTE_SUBTITLE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_EJECT: + xbmcKey.iButton = XINPUT_IR_REMOTE_EJECT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_POWER_ON_FUNCTION: + case CEC_USER_CONTROL_CODE_INPUT_SELECT: + case CEC_USER_CONTROL_CODE_INITIAL_CONFIGURATION: + case CEC_USER_CONTROL_CODE_HELP: + case CEC_USER_CONTROL_CODE_STOP_RECORD: + case CEC_USER_CONTROL_CODE_PAUSE_RECORD: + case CEC_USER_CONTROL_CODE_ANGLE: + case CEC_USER_CONTROL_CODE_VIDEO_ON_DEMAND: + case CEC_USER_CONTROL_CODE_TIMER_PROGRAMMING: + case CEC_USER_CONTROL_CODE_PLAY_FUNCTION: + case CEC_USER_CONTROL_CODE_PAUSE_PLAY_FUNCTION: + case CEC_USER_CONTROL_CODE_RECORD_FUNCTION: + case CEC_USER_CONTROL_CODE_PAUSE_RECORD_FUNCTION: + case CEC_USER_CONTROL_CODE_STOP_FUNCTION: + case CEC_USER_CONTROL_CODE_TUNE_FUNCTION: + case CEC_USER_CONTROL_CODE_SELECT_MEDIA_FUNCTION: + case CEC_USER_CONTROL_CODE_SELECT_AV_INPUT_FUNCTION: + case CEC_USER_CONTROL_CODE_SELECT_AUDIO_INPUT_FUNCTION: + case CEC_USER_CONTROL_CODE_F5: + case CEC_USER_CONTROL_CODE_NUMBER_ENTRY_MODE: + case CEC_USER_CONTROL_CODE_NUMBER11: + case CEC_USER_CONTROL_CODE_NUMBER12: + case CEC_USER_CONTROL_CODE_SELECT_BROADCAST_TYPE: + case CEC_USER_CONTROL_CODE_SELECT_SOUND_PRESENTATION: + case CEC_USER_CONTROL_CODE_UNKNOWN: + default: + break; + } +} + +int CPeripheralCecAdapter::GetButton(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_bHasButton) + GetNextKey(); + + return m_bHasButton ? m_currentButton.iButton : 0; +} + +unsigned int CPeripheralCecAdapter::GetHoldTime(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_bHasButton) + GetNextKey(); + + return m_bHasButton ? m_currentButton.iDuration : 0; +} + +void CPeripheralCecAdapter::ResetButton(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bHasButton = false; + + // wait for the key release if the duration isn't 0 + if (m_currentButton.iDuration > 0) + { + m_currentButton.iButton = 0; + m_currentButton.iDuration = 0; + } +} + +void CPeripheralCecAdapter::OnSettingChanged(const std::string& strChangedSetting) +{ + if (StringUtils::EqualsNoCase(strChangedSetting, "enabled")) + { + bool bEnabled(GetSettingBool("enabled")); + if (!bEnabled && IsRunning()) + { + CLog::Log(LOGDEBUG, "{} - closing the CEC connection", __FUNCTION__); + StopThread(true); + } + else if (bEnabled && !IsRunning()) + { + CLog::Log(LOGDEBUG, "{} - starting the CEC connection", __FUNCTION__); + SetConfigurationFromSettings(); + InitialiseFeature(FEATURE_CEC); + } + } + else if (IsRunning()) + { + if (m_queryThread->IsRunning()) + { + CLog::Log(LOGDEBUG, "{} - sending the updated configuration to libCEC", __FUNCTION__); + SetConfigurationFromSettings(); + m_queryThread->UpdateConfiguration(&m_configuration); + } + } + else + { + CLog::Log(LOGDEBUG, "{} - restarting the CEC connection", __FUNCTION__); + SetConfigurationFromSettings(); + InitialiseFeature(FEATURE_CEC); + } +} + +void CPeripheralCecAdapter::CecSourceActivated(void* cbParam, + const CEC::cec_logical_address address, + const uint8_t activated) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + // wake up the screensaver, so the user doesn't switch to a black screen + if (activated == 1) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + appPower->WakeUpScreenSaverAndDPMS(); + } + + if (adapter->GetSettingInt("pause_or_stop_playback_on_deactivate") != LOCALISED_ID_NONE) + { + bool bShowingSlideshow = + (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW); + CGUIWindowSlideShow* pSlideShow = + bShowingSlideshow + ? CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>( + WINDOW_SLIDESHOW) + : NULL; + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + bool bPlayingAndDeactivated = activated == 0 && ((pSlideShow && pSlideShow->IsPlaying()) || + !appPlayer->IsPausedPlayback()); + bool bPausedAndActivated = + activated == 1 && adapter->m_bPlaybackPaused && + ((pSlideShow && pSlideShow->IsPaused()) || (appPlayer && appPlayer->IsPausedPlayback())); + if (bPlayingAndDeactivated) + adapter->m_bPlaybackPaused = true; + else if (bPausedAndActivated) + adapter->m_bPlaybackPaused = false; + + if ((bPlayingAndDeactivated || bPausedAndActivated) && + adapter->GetSettingInt("pause_or_stop_playback_on_deactivate") == LOCALISED_ID_PAUSE) + { + if (pSlideShow) + // pause/resume slideshow + pSlideShow->OnAction(CAction(ACTION_PAUSE)); + else + // pause/resume player + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE); + } + else if (bPlayingAndDeactivated && + adapter->GetSettingInt("pause_or_stop_playback_on_deactivate") == LOCALISED_ID_STOP) + { + if (pSlideShow) + pSlideShow->OnAction(CAction(ACTION_STOP)); + else + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP); + } + } +} + +void CPeripheralCecAdapter::CecLogMessage(void* cbParam, const cec_log_message* message) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + int iLevel = -1; + switch (message->level) + { + case CEC_LOG_ERROR: + iLevel = LOGERROR; + break; + case CEC_LOG_WARNING: + iLevel = LOGWARNING; + break; + case CEC_LOG_NOTICE: + iLevel = LOGDEBUG; + break; + case CEC_LOG_TRAFFIC: + case CEC_LOG_DEBUG: + iLevel = LOGDEBUG; + break; + default: + break; + } + + if (iLevel >= CEC_LOG_NOTICE || + (iLevel >= 0 && CServiceBroker::GetLogging().IsLogLevelLogged(LOGDEBUG))) + CLog::Log(iLevel, LOGCEC, "{} - {}", __FUNCTION__, message->message); +} + +void CPeripheralCecAdapter::SetConfigurationFromLibCEC(const CEC::libcec_configuration& config) +{ + bool bChanged(false); + + // set the primary device type + m_configuration.deviceTypes.Clear(); + m_configuration.deviceTypes.Add(config.deviceTypes[0]); + + // hide the "connected device" and "hdmi port number" settings when the PA was autodetected + bool bPAAutoDetected(config.bAutodetectAddress == 1); + + SetSettingVisible("connected_device", !bPAAutoDetected); + SetSettingVisible("cec_hdmi_port", !bPAAutoDetected); + + // set the connected device + m_configuration.baseDevice = config.baseDevice; + bChanged |= + SetSetting("connected_device", + config.baseDevice == CECDEVICE_AUDIOSYSTEM ? LOCALISED_ID_AVR : LOCALISED_ID_TV); + + // set the HDMI port number + m_configuration.iHDMIPort = config.iHDMIPort; + bChanged |= SetSetting("cec_hdmi_port", config.iHDMIPort); + + // set the physical address, when baseDevice or iHDMIPort are not set + std::string strPhysicalAddress("0"); + if (!bPAAutoDetected && (m_configuration.baseDevice == CECDEVICE_UNKNOWN || + m_configuration.iHDMIPort < CEC_MIN_HDMI_PORTNUMBER || + m_configuration.iHDMIPort > CEC_MAX_HDMI_PORTNUMBER)) + { + m_configuration.iPhysicalAddress = config.iPhysicalAddress; + strPhysicalAddress = StringUtils::Format("{:x}", config.iPhysicalAddress); + } + bChanged |= SetSetting("physical_address", strPhysicalAddress); + + // set the devices to wake when starting + m_configuration.wakeDevices = config.wakeDevices; + bChanged |= WriteLogicalAddresses(config.wakeDevices, "wake_devices", "wake_devices_advanced"); + + // set the devices to power off when stopping + m_configuration.powerOffDevices = config.powerOffDevices; + bChanged |= + WriteLogicalAddresses(config.powerOffDevices, "standby_devices", "standby_devices_advanced"); + + // set the boolean settings + m_configuration.bActivateSource = config.bActivateSource; + bChanged |= SetSetting("activate_source", m_configuration.bActivateSource == 1); + + m_configuration.iDoubleTapTimeoutMs = config.iDoubleTapTimeoutMs; + bChanged |= SetSetting("double_tap_timeout_ms", (int)m_configuration.iDoubleTapTimeoutMs); + + m_configuration.iButtonRepeatRateMs = config.iButtonRepeatRateMs; + bChanged |= SetSetting("button_repeat_rate_ms", (int)m_configuration.iButtonRepeatRateMs); + + m_configuration.iButtonReleaseDelayMs = config.iButtonReleaseDelayMs; + bChanged |= SetSetting("button_release_delay_ms", (int)m_configuration.iButtonReleaseDelayMs); + + m_configuration.bPowerOffOnStandby = config.bPowerOffOnStandby; + + m_configuration.iFirmwareVersion = config.iFirmwareVersion; + + memcpy(m_configuration.strDeviceLanguage, config.strDeviceLanguage, 3); + m_configuration.iFirmwareBuildDate = config.iFirmwareBuildDate; + + SetVersionInfo(m_configuration); + + if (bChanged) + CLog::Log(LOGDEBUG, "SetConfigurationFromLibCEC - settings updated by libCEC"); +} + +void CPeripheralCecAdapter::SetConfigurationFromSettings(void) +{ + // client version matches the version of libCEC that we originally used the API from + m_configuration.clientVersion = LIBCEC_VERSION_TO_UINT(4, 0, 0); + + // device name 'XBMC' + snprintf(m_configuration.strDeviceName, 13, "%s", GetSettingString("device_name").c_str()); + + // set the primary device type + m_configuration.deviceTypes.Clear(); + switch (GetSettingInt("device_type")) + { + case LOCALISED_ID_PLAYBACK_DEVICE: + m_configuration.deviceTypes.Add(CEC_DEVICE_TYPE_PLAYBACK_DEVICE); + break; + case LOCALISED_ID_TUNER_DEVICE: + m_configuration.deviceTypes.Add(CEC_DEVICE_TYPE_TUNER); + break; + case LOCALISED_ID_RECORDING_DEVICE: + default: + m_configuration.deviceTypes.Add(CEC_DEVICE_TYPE_RECORDING_DEVICE); + break; + } + + // always try to autodetect the address. + // when the firmware supports this, it will override the physical address, connected device and + // hdmi port settings + m_configuration.bAutodetectAddress = CEC_DEFAULT_SETTING_AUTODETECT_ADDRESS; + + // set the physical address + // when set, it will override the connected device and hdmi port settings + std::string strPhysicalAddress = GetSettingString("physical_address"); + int iPhysicalAddress; + if (sscanf(strPhysicalAddress.c_str(), "%x", &iPhysicalAddress) && + iPhysicalAddress >= CEC_PHYSICAL_ADDRESS_TV && iPhysicalAddress <= CEC_MAX_PHYSICAL_ADDRESS) + m_configuration.iPhysicalAddress = iPhysicalAddress; + else + m_configuration.iPhysicalAddress = CEC_PHYSICAL_ADDRESS_TV; + + // set the connected device + int iConnectedDevice = GetSettingInt("connected_device"); + if (iConnectedDevice == LOCALISED_ID_AVR) + m_configuration.baseDevice = CECDEVICE_AUDIOSYSTEM; + else if (iConnectedDevice == LOCALISED_ID_TV) + m_configuration.baseDevice = CECDEVICE_TV; + + // set the HDMI port number + int iHDMIPort = GetSettingInt("cec_hdmi_port"); + if (iHDMIPort >= CEC_MIN_HDMI_PORTNUMBER && iHDMIPort <= CEC_MAX_HDMI_PORTNUMBER) + m_configuration.iHDMIPort = iHDMIPort; + + // set the tv vendor override + int iVendor = GetSettingInt("tv_vendor"); + if (iVendor >= CEC_MIN_VENDORID && iVendor <= CEC_MAX_VENDORID) + m_configuration.tvVendor = iVendor; + + // read the devices to wake when starting + std::string strWakeDevices = GetSettingString("wake_devices_advanced"); + StringUtils::Trim(strWakeDevices); + m_configuration.wakeDevices.Clear(); + if (!strWakeDevices.empty()) + ReadLogicalAddresses(strWakeDevices, m_configuration.wakeDevices); + else + ReadLogicalAddresses(GetSettingInt("wake_devices"), m_configuration.wakeDevices); + + // read the devices to power off when stopping + std::string strStandbyDevices = GetSettingString("standby_devices_advanced"); + StringUtils::Trim(strStandbyDevices); + m_configuration.powerOffDevices.Clear(); + if (!strStandbyDevices.empty()) + ReadLogicalAddresses(strStandbyDevices, m_configuration.powerOffDevices); + else + ReadLogicalAddresses(GetSettingInt("standby_devices"), m_configuration.powerOffDevices); + + // read the boolean settings + m_bUseTVMenuLanguage = GetSettingBool("use_tv_menu_language"); + m_configuration.bActivateSource = GetSettingBool("activate_source") ? 1 : 0; + m_bPowerOffScreensaver = GetSettingBool("cec_standby_screensaver"); + m_bPowerOnScreensaver = GetSettingBool("cec_wake_screensaver"); + m_bSendInactiveSource = GetSettingBool("send_inactive_source"); + m_configuration.bAutoWakeAVR = GetSettingBool("power_avr_on_as") ? 1 : 0; + + // read the mutually exclusive boolean settings + int iStandbyAction(GetSettingInt("standby_pc_on_tv_standby")); + m_configuration.bPowerOffOnStandby = + (iStandbyAction == LOCALISED_ID_SUSPEND || iStandbyAction == LOCALISED_ID_HIBERNATE) ? 1 : 0; + m_bShutdownOnStandby = iStandbyAction == LOCALISED_ID_POWEROFF; + + // double tap prevention timeout in ms + m_configuration.iDoubleTapTimeoutMs = GetSettingInt("double_tap_timeout_ms"); + m_configuration.iButtonRepeatRateMs = GetSettingInt("button_repeat_rate_ms"); + m_configuration.iButtonReleaseDelayMs = GetSettingInt("button_release_delay_ms"); + + if (GetSettingBool("pause_playback_on_deactivate")) + { + SetSetting("pause_or_stop_playback_on_deactivate", LOCALISED_ID_PAUSE); + SetSetting("pause_playback_on_deactivate", false); + } +} + +void CPeripheralCecAdapter::ReadLogicalAddresses(const std::string& strString, + cec_logical_addresses& addresses) +{ + for (size_t iPtr = 0; iPtr < strString.size(); iPtr++) + { + std::string strDevice = strString.substr(iPtr, 1); + StringUtils::Trim(strDevice); + if (!strDevice.empty()) + { + int iDevice(0); + if (sscanf(strDevice.c_str(), "%x", &iDevice) == 1 && iDevice >= 0 && iDevice <= 0xF) + addresses.Set((cec_logical_address)iDevice); + } + } +} + +void CPeripheralCecAdapter::ReadLogicalAddresses(int iLocalisedId, cec_logical_addresses& addresses) +{ + addresses.Clear(); + switch (iLocalisedId) + { + case LOCALISED_ID_TV: + addresses.Set(CECDEVICE_TV); + break; + case LOCALISED_ID_AVR: + addresses.Set(CECDEVICE_AUDIOSYSTEM); + break; + case LOCALISED_ID_TV_AVR: + addresses.Set(CECDEVICE_TV); + addresses.Set(CECDEVICE_AUDIOSYSTEM); + break; + case LOCALISED_ID_NONE: + default: + break; + } +} + +bool CPeripheralCecAdapter::WriteLogicalAddresses(const cec_logical_addresses& addresses, + const std::string& strSettingName, + const std::string& strAdvancedSettingName) +{ + bool bChanged(false); + + // only update the advanced setting if it was set by the user + if (!GetSettingString(strAdvancedSettingName).empty()) + { + std::string strPowerOffDevices; + for (unsigned int iPtr = CECDEVICE_TV; iPtr <= CECDEVICE_BROADCAST; iPtr++) + if (addresses[iPtr]) + strPowerOffDevices += StringUtils::Format(" {:X}", iPtr); + StringUtils::Trim(strPowerOffDevices); + bChanged = SetSetting(strAdvancedSettingName, strPowerOffDevices); + } + + int iSettingPowerOffDevices = LOCALISED_ID_NONE; + if (addresses[CECDEVICE_TV] && addresses[CECDEVICE_AUDIOSYSTEM]) + iSettingPowerOffDevices = LOCALISED_ID_TV_AVR; + else if (addresses[CECDEVICE_TV]) + iSettingPowerOffDevices = LOCALISED_ID_TV; + else if (addresses[CECDEVICE_AUDIOSYSTEM]) + iSettingPowerOffDevices = LOCALISED_ID_AVR; + return SetSetting(strSettingName, iSettingPowerOffDevices) || bChanged; +} + +CPeripheralCecAdapterUpdateThread::CPeripheralCecAdapterUpdateThread( + CPeripheralCecAdapter* adapter, libcec_configuration* configuration) + : CThread("CECAdapterUpdate"), + m_adapter(adapter), + m_configuration(*configuration), + m_bNextConfigurationScheduled(false), + m_bIsUpdating(true) +{ + m_nextConfiguration.Clear(); + m_event.Reset(); +} + +CPeripheralCecAdapterUpdateThread::~CPeripheralCecAdapterUpdateThread(void) +{ + StopThread(false); + m_event.Set(); + StopThread(true); +} + +void CPeripheralCecAdapterUpdateThread::Signal(void) +{ + m_event.Set(); +} + +bool CPeripheralCecAdapterUpdateThread::UpdateConfiguration(libcec_configuration* configuration) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!configuration) + return false; + + if (m_bIsUpdating) + { + m_bNextConfigurationScheduled = true; + m_nextConfiguration = *configuration; + } + else + { + m_configuration = *configuration; + m_event.Set(); + } + return true; +} + +bool CPeripheralCecAdapterUpdateThread::WaitReady(void) +{ + // don't wait if we're not powering up anything + if (m_configuration.wakeDevices.IsEmpty() && m_configuration.bActivateSource == 0) + return true; + + // wait for the TV if we're configured to become the active source. + // wait for the first device in the wake list otherwise. + cec_logical_address waitFor = + (m_configuration.bActivateSource == 1) ? CECDEVICE_TV : m_configuration.wakeDevices.primary; + + cec_power_status powerStatus(CEC_POWER_STATUS_UNKNOWN); + bool bContinue(true); + while (bContinue && !m_adapter->m_bStop && !m_bStop && powerStatus != CEC_POWER_STATUS_ON) + { + powerStatus = m_adapter->m_cecAdapter->GetDevicePowerStatus(waitFor); + if (powerStatus != CEC_POWER_STATUS_ON) + bContinue = !m_event.Wait(1000ms); + } + + return powerStatus == CEC_POWER_STATUS_ON; +} + +void CPeripheralCecAdapterUpdateThread::UpdateMenuLanguage(void) +{ + // request the menu language of the TV + if (m_adapter->m_bUseTVMenuLanguage == 1) + { + CLog::Log(LOGDEBUG, "{} - requesting the menu language of the TV", __FUNCTION__); + std::string language(m_adapter->m_cecAdapter->GetDeviceMenuLanguage(CECDEVICE_TV)); + m_adapter->SetMenuLanguage(language.c_str()); + } + else + { + CLog::Log(LOGDEBUG, "{} - using TV menu language is disabled", __FUNCTION__); + } +} + +std::string CPeripheralCecAdapterUpdateThread::UpdateAudioSystemStatus(void) +{ + std::string strAmpName; + + /* disable the mute setting when an amp is found, because the amp handles the mute setting and + set PCM output to 100% */ + if (m_adapter->m_cecAdapter->IsActiveDeviceType(CEC_DEVICE_TYPE_AUDIO_SYSTEM)) + { + // request the OSD name of the amp + std::string ampName(m_adapter->m_cecAdapter->GetDeviceOSDName(CECDEVICE_AUDIOSYSTEM)); + CLog::Log(LOGDEBUG, + "{} - CEC capable amplifier found ({}). volume will be controlled on the amp", + __FUNCTION__, ampName); + strAmpName += ampName; + + // set amp present + m_adapter->SetAudioSystemConnected(true); + auto& components = CServiceBroker::GetAppComponents(); + const auto appVolume = components.GetComponent<CApplicationVolumeHandling>(); + appVolume->SetMute(false); + appVolume->SetVolume(CApplicationVolumeHandling::VOLUME_MAXIMUM, false); + } + else + { + // set amp present + CLog::Log(LOGDEBUG, "{} - no CEC capable amplifier found", __FUNCTION__); + m_adapter->SetAudioSystemConnected(false); + } + + return strAmpName; +} + +bool CPeripheralCecAdapterUpdateThread::SetInitialConfiguration(void) +{ + // the option to make XBMC the active source is set + if (m_configuration.bActivateSource == 1) + m_adapter->m_cecAdapter->SetActiveSource(); + + // devices to wake are set + cec_logical_addresses tvOnly; + tvOnly.Clear(); + tvOnly.Set(CECDEVICE_TV); + if (!m_configuration.wakeDevices.IsEmpty() && + (m_configuration.wakeDevices != tvOnly || m_configuration.bActivateSource == 0)) + m_adapter->m_cecAdapter->PowerOnDevices(CECDEVICE_BROADCAST); + + // wait until devices are powered up + if (!WaitReady()) + return false; + + UpdateMenuLanguage(); + + // request the OSD name of the TV + std::string strNotification; + std::string tvName(m_adapter->m_cecAdapter->GetDeviceOSDName(CECDEVICE_TV)); + strNotification = StringUtils::Format("{}: {}", g_localizeStrings.Get(36016), tvName); + + std::string strAmpName = UpdateAudioSystemStatus(); + if (!strAmpName.empty()) + strNotification += StringUtils::Format("- {}", strAmpName); + + m_adapter->m_bIsReady = true; + + // and let the gui know that we're done + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000), + strNotification); + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bIsUpdating = false; + return true; +} + +bool CPeripheralCecAdapter::IsRunning(void) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bIsRunning; +} + +void CPeripheralCecAdapterUpdateThread::Process(void) +{ + // set the initial configuration + if (!SetInitialConfiguration()) + return; + + // and wait for updates + bool bUpdate(false); + while (!m_bStop) + { + // update received + if (bUpdate || m_event.Wait(500ms)) + { + if (m_bStop) + return; + // set the new configuration + libcec_configuration configuration; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + configuration = m_configuration; + m_bIsUpdating = false; + } + + CLog::Log(LOGDEBUG, "{} - updating the configuration", __FUNCTION__); + bool bConfigSet(m_adapter->m_cecAdapter->SetConfiguration(&configuration)); + // display message: config updated / failed to update + if (!bConfigSet) + CLog::Log(LOGERROR, "{} - libCEC couldn't set the new configuration", __FUNCTION__); + else + { + UpdateMenuLanguage(); + UpdateAudioSystemStatus(); + } + + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000), + g_localizeStrings.Get(bConfigSet ? 36023 : 36024)); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if ((bUpdate = m_bNextConfigurationScheduled) == true) + { + // another update is scheduled + m_bNextConfigurationScheduled = false; + m_configuration = m_nextConfiguration; + } + else + { + // nothing left to do, wait for updates + m_bIsUpdating = false; + m_event.Reset(); + } + } + } + } +} + +void CPeripheralCecAdapter::OnDeviceRemoved(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bDeviceRemoved = true; +} + +namespace PERIPHERALS +{ + +class CPeripheralCecAdapterReopenJob : public CJob +{ +public: + CPeripheralCecAdapterReopenJob(CPeripheralCecAdapter* adapter) : m_adapter(adapter) {} + ~CPeripheralCecAdapterReopenJob() override = default; + + bool DoWork(void) override { return m_adapter->ReopenConnection(false); } + +private: + CPeripheralCecAdapter* m_adapter; +}; + +}; // namespace PERIPHERALS + +bool CPeripheralCecAdapter::ReopenConnection(bool bAsync /* = false */) +{ + if (bAsync) + { + CServiceBroker::GetJobManager()->AddJob(new CPeripheralCecAdapterReopenJob(this), nullptr, + CJob::PRIORITY_NORMAL); + return true; + } + + // stop running thread + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iExitCode = EXITCODE_RESTARTAPP; + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + StopThread(false); + } + StopThread(); + + // reset all members to their defaults + ResetMembers(); + + // reopen the connection + return InitialiseFeature(FEATURE_CEC); +} + +void CPeripheralCecAdapter::ActivateSource(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bActiveSourcePending = true; +} + +void CPeripheralCecAdapter::ProcessActivateSource(void) +{ + bool bActivate(false); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bActivate = m_bActiveSourcePending; + m_bActiveSourcePending = false; + } + + if (bActivate) + m_cecAdapter->SetActiveSource(); +} + +void CPeripheralCecAdapter::StandbyDevices(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bStandbyPending = true; +} + +void CPeripheralCecAdapter::ProcessStandbyDevices(void) +{ + bool bStandby(false); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bStandby = m_bStandbyPending; + m_bStandbyPending = false; + if (bStandby) + m_bGoingToStandby = true; + } + + if (bStandby) + { + if (!m_configuration.powerOffDevices.IsEmpty()) + { + m_standbySent = CDateTime::GetCurrentDateTime(); + m_cecAdapter->StandbyDevices(CECDEVICE_BROADCAST); + } + else if (m_bSendInactiveSource == 1) + { + CLog::Log(LOGDEBUG, "{} - sending inactive source commands", __FUNCTION__); + m_cecAdapter->SetInactiveView(); + } + } +} + +bool CPeripheralCecAdapter::ToggleDeviceState(CecStateChange mode /*= STATE_SWITCH_TOGGLE */, + bool forceType /*= false */) +{ + if (!IsRunning()) + return false; + if (m_cecAdapter->IsLibCECActiveSource() && + (mode == STATE_SWITCH_TOGGLE || mode == STATE_STANDBY)) + { + CLog::Log(LOGDEBUG, "{} - putting CEC device on standby...", __FUNCTION__); + StandbyDevices(); + return false; + } + else if (mode == STATE_SWITCH_TOGGLE || mode == STATE_ACTIVATE_SOURCE) + { + CLog::Log(LOGDEBUG, "{} - waking up CEC device...", __FUNCTION__); + ActivateSource(); + return true; + } + + return false; +} |