From c04dcc2e7d834218ef2d4194331e383402495ae1 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 20:07:22 +0200 Subject: Adding upstream version 2:20.4+dfsg. Signed-off-by: Daniel Baumann --- xbmc/network/AirTunesServer.cpp | 753 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 753 insertions(+) create mode 100644 xbmc/network/AirTunesServer.cpp (limited to 'xbmc/network/AirTunesServer.cpp') diff --git a/xbmc/network/AirTunesServer.cpp b/xbmc/network/AirTunesServer.cpp new file mode 100644 index 0000000..0add358 --- /dev/null +++ b/xbmc/network/AirTunesServer.cpp @@ -0,0 +1,753 @@ +/* + * Many concepts and protocol specification in this code are taken + * from Shairport, by James Laird. + * + * Copyright (C) 2011-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 "AirTunesServer.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "application/ApplicationActionListeners.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "application/ApplicationVolumeHandling.h" +#include "cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.h" +#include "filesystem/File.h" +#include "filesystem/PipeFile.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "interfaces/AnnouncementManager.h" +#include "messaging/ApplicationMessenger.h" +#include "music/tags/MusicInfoTag.h" +#include "network/Network.h" +#include "network/Zeroconf.h" +#include "network/ZeroconfBrowser.h" +#include "network/dacp/dacp.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/EndianSwap.h" +#include "utils/StringUtils.h" +#include "utils/SystemInfo.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include +#include +#include +#include +#include + +#if !defined(TARGET_WINDOWS) +#pragma GCC diagnostic ignored "-Wwrite-strings" +#endif + +#ifdef HAS_AIRPLAY +#include "network/AirPlayServer.h" +#endif + +#define TMP_COVERART_PATH_JPG "special://temp/airtunes_album_thumb.jpg" +#define TMP_COVERART_PATH_PNG "special://temp/airtunes_album_thumb.png" +#define ZEROCONF_DACP_SERVICE "_dacp._tcp" + +using namespace XFILE; +using namespace std::chrono_literals; + +CAirTunesServer *CAirTunesServer::ServerInstance = NULL; +std::string CAirTunesServer::m_macAddress; +std::string CAirTunesServer::m_metadata[3]; +CCriticalSection CAirTunesServer::m_metadataLock; +bool CAirTunesServer::m_streamStarted = false; +CCriticalSection CAirTunesServer::m_dacpLock; +CDACP *CAirTunesServer::m_pDACP = NULL; +std::string CAirTunesServer::m_dacp_id; +std::string CAirTunesServer::m_active_remote_header; +CCriticalSection CAirTunesServer::m_actionQueueLock; +std::list CAirTunesServer::m_actionQueue; +CEvent CAirTunesServer::m_processActions; +int CAirTunesServer::m_sampleRate = 44100; + +unsigned int CAirTunesServer::m_cachedStartTime = 0; +unsigned int CAirTunesServer::m_cachedEndTime = 0; +unsigned int CAirTunesServer::m_cachedCurrentTime = 0; + + +//parse daap metadata - thx to project MythTV +std::map decodeDMAP(const char *buffer, unsigned int size) +{ + std::map result; + unsigned int offset = 8; + while (offset < size) + { + std::string tag; + tag.append(buffer + offset, 4); + offset += 4; + uint32_t length = Endian_SwapBE32(*(const uint32_t *)(buffer + offset)); + offset += sizeof(uint32_t); + std::string content; + content.append(buffer + offset, length);//possible fixme - utf8? + offset += length; + result[tag] = content; + } + return result; +} + +void CAirTunesServer::ResetMetadata() +{ + std::unique_lock lock(m_metadataLock); + + XFILE::CFile::Delete(TMP_COVERART_PATH_JPG); + XFILE::CFile::Delete(TMP_COVERART_PATH_PNG); + RefreshCoverArt(); + + m_metadata[0] = ""; + m_metadata[1] = "AirPlay"; + m_metadata[2] = ""; + RefreshMetadata(); +} + +void CAirTunesServer::RefreshMetadata() +{ + std::unique_lock lock(m_metadataLock); + MUSIC_INFO::CMusicInfoTag tag; + CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager(); + if (infoMgr.GetCurrentSongTag()) + tag = *infoMgr.GetCurrentSongTag(); + if (m_metadata[0].length()) + tag.SetAlbum(m_metadata[0]);//album + if (m_metadata[1].length()) + tag.SetTitle(m_metadata[1]);//title + if (m_metadata[2].length()) + tag.SetArtist(m_metadata[2]);//artist + + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 1, -1, + static_cast(new CFileItem(tag))); +} + +void CAirTunesServer::RefreshCoverArt(const char *outputFilename/* = NULL*/) +{ + static std::string coverArtFile = TMP_COVERART_PATH_JPG; + + if (outputFilename != NULL) + coverArtFile = std::string(outputFilename); + + CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager(); + std::unique_lock lock(m_metadataLock); + //reset to empty before setting the new one + //else it won't get refreshed because the name didn't change + infoMgr.SetCurrentAlbumThumb(""); + //update the ui + infoMgr.SetCurrentAlbumThumb(coverArtFile); + //update the ui + CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_REFRESH_THUMBS); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); +} + +void CAirTunesServer::SetMetadataFromBuffer(const char *buffer, unsigned int size) +{ + + std::map metadata = decodeDMAP(buffer, size); + std::unique_lock lock(m_metadataLock); + + if(metadata["asal"].length()) + m_metadata[0] = metadata["asal"];//album + if(metadata["minm"].length()) + m_metadata[1] = metadata["minm"];//title + if(metadata["asar"].length()) + m_metadata[2] = metadata["asar"];//artist + + RefreshMetadata(); +} + +void CAirTunesServer::Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) +{ + if ((flag & ANNOUNCEMENT::Player) && + sender == ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER) + { + if ((message == "OnPlay" || message == "OnResume") && m_streamStarted) + { + RefreshMetadata(); + RefreshCoverArt(); + std::unique_lock lock(m_dacpLock); + if (m_pDACP) + m_pDACP->Play(); + } + + if (message == "OnStop" && m_streamStarted) + { + std::unique_lock lock(m_dacpLock); + if (m_pDACP) + m_pDACP->Stop(); + } + + if (message == "OnPause" && m_streamStarted) + { + std::unique_lock lock(m_dacpLock); + if (m_pDACP) + m_pDACP->Pause(); + } + } +} + +void CAirTunesServer::EnableActionProcessing(bool enable) +{ + ServerInstance->RegisterActionListener(enable); +} + +bool CAirTunesServer::OnAction(const CAction &action) +{ + switch(action.GetID()) + { + case ACTION_NEXT_ITEM: + case ACTION_PREV_ITEM: + case ACTION_VOLUME_UP: + case ACTION_VOLUME_DOWN: + case ACTION_MUTE: + { + std::unique_lock lock(m_actionQueueLock); + m_actionQueue.push_back(action); + m_processActions.Set(); + } + } + return false; +} + +void CAirTunesServer::Process() +{ + m_bStop = false; + while(!m_bStop) + { + if (m_streamStarted) + SetupRemoteControl();// check for remote controls + + m_processActions.Wait(1000ms); // timeout for being able to stop + std::list currentActions; + { + std::unique_lock lock(m_actionQueueLock); // copy and clear the source queue + currentActions.insert(currentActions.begin(), m_actionQueue.begin(), m_actionQueue.end()); + m_actionQueue.clear(); + } + + for (const auto& currentAction : currentActions) + { + std::unique_lock lock(m_dacpLock); + if (m_pDACP) + { + switch(currentAction.GetID()) + { + case ACTION_NEXT_ITEM: + m_pDACP->NextItem(); + break; + case ACTION_PREV_ITEM: + m_pDACP->PrevItem(); + break; + case ACTION_VOLUME_UP: + m_pDACP->VolumeUp(); + break; + case ACTION_VOLUME_DOWN: + m_pDACP->VolumeDown(); + break; + case ACTION_MUTE: + m_pDACP->ToggleMute(); + break; + } + } + } + } +} + +bool IsJPEG(const char *buffer, unsigned int size) +{ + bool ret = false; + if (size < 2) + return false; + + //JPEG image files begin with FF D8 and end with FF D9. + // check for FF D8 big + little endian on start + if ((buffer[0] == (char)0xd8 && buffer[1] == (char)0xff) || + (buffer[1] == (char)0xd8 && buffer[0] == (char)0xff)) + ret = true; + + if (ret) + { + ret = false; + //check on FF D9 big + little endian on end + if ((buffer[size - 2] == (char)0xd9 && buffer[size - 1] == (char)0xff) || + (buffer[size - 1] == (char)0xd9 && buffer[size - 2] == (char)0xff)) + ret = true; + } + + return ret; +} + +void CAirTunesServer::SetCoverArtFromBuffer(const char *buffer, unsigned int size) +{ + XFILE::CFile tmpFile; + std::string tmpFilename = TMP_COVERART_PATH_PNG; + + if(!size) + return; + + std::unique_lock lock(m_metadataLock); + + if (IsJPEG(buffer, size)) + tmpFilename = TMP_COVERART_PATH_JPG; + + if (tmpFile.OpenForWrite(tmpFilename, true)) + { + int writtenBytes=0; + writtenBytes = tmpFile.Write(buffer, size); + tmpFile.Close(); + + if (writtenBytes > 0) + RefreshCoverArt(tmpFilename.c_str()); + } +} + +void CAirTunesServer::FreeDACPRemote() +{ + std::unique_lock lock(m_dacpLock); + if (m_pDACP) + delete m_pDACP; + m_pDACP = NULL; +} + +#define RSA_KEY " \ +-----BEGIN RSA PRIVATE KEY-----\ +MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\ +wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\ +wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\ +/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\ +UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\ +BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\ +LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\ +NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\ +lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\ +aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\ +a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\ +oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\ +oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\ +k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\ +AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\ +cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\ +54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\ +17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\ +1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\ +LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\ +2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\ +-----END RSA PRIVATE KEY-----" + +void CAirTunesServer::AudioOutputFunctions::audio_set_metadata(void *cls, void *session, const void *buffer, int buflen) +{ + CAirTunesServer::SetMetadataFromBuffer((const char *)buffer, buflen); +} + +void CAirTunesServer::AudioOutputFunctions::audio_set_coverart(void *cls, void *session, const void *buffer, int buflen) +{ + CAirTunesServer::SetCoverArtFromBuffer((const char *)buffer, buflen); +} + +char session[]="Kodi-AirTunes"; + +void* CAirTunesServer::AudioOutputFunctions::audio_init(void *cls, int bits, int channels, int samplerate) +{ + XFILE::CPipeFile *pipe=(XFILE::CPipeFile *)cls; + const CURL pathToUrl(XFILE::PipesManager::GetInstance().GetUniquePipeName()); + pipe->OpenForWrite(pathToUrl); + pipe->SetOpenThreshold(300); + + Demux_BXA_FmtHeader header; + std::memcpy(header.fourcc, "BXA ", 4); + header.type = BXA_PACKET_TYPE_FMT_DEMUX; + header.bitsPerSample = bits; + header.channels = channels; + header.sampleRate = samplerate; + header.durationMs = 0; + + if (pipe->Write(&header, sizeof(header)) == 0) + return 0; + + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP); + + CFileItem *item = new CFileItem(); + item->SetPath(pipe->GetName()); + item->SetMimeType("audio/x-xbmc-pcm"); + m_streamStarted = true; + m_sampleRate = samplerate; + + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0, static_cast(item)); + + // Not all airplay streams will provide metadata (e.g. if using mirroring, + // no metadata will be sent). If there *is* metadata, it will be received + // in a later call to audio_set_metadata/audio_set_coverart. + ResetMetadata(); + + // browse for dacp services protocol which gives us the remote control service + CZeroconfBrowser::GetInstance()->Start(); + CZeroconfBrowser::GetInstance()->AddServiceType(ZEROCONF_DACP_SERVICE); + CAirTunesServer::EnableActionProcessing(true); + + return session;//session +} + +void CAirTunesServer::AudioOutputFunctions::audio_remote_control_id(void *cls, const char *dacp_id, const char *active_remote_header) +{ + if (dacp_id && active_remote_header) + { + m_dacp_id = dacp_id; + m_active_remote_header = active_remote_header; + } +} + +void CAirTunesServer::InformPlayerAboutPlayTimes() +{ + if (m_cachedEndTime > 0) + { + unsigned int duration = m_cachedEndTime - m_cachedStartTime; + unsigned int position = m_cachedCurrentTime - m_cachedStartTime; + duration /= m_sampleRate; + position /= m_sampleRate; + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent(); + if (appPlayer->IsPlaying()) + { + appPlayer->SetTime(position * 1000); + appPlayer->SetTotalTime(duration * 1000); + + // reset play times now that we have informed the player + m_cachedEndTime = 0; + m_cachedCurrentTime = 0; + m_cachedStartTime = 0; + + } + } +} + +void CAirTunesServer::AudioOutputFunctions::audio_set_progress(void *cls, void *session, unsigned int start, unsigned int curr, unsigned int end) +{ + m_cachedStartTime = start; + m_cachedCurrentTime = curr; + m_cachedEndTime = end; + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent(); + if (appPlayer->IsPlaying()) + { + // player is there - directly inform him about play times + InformPlayerAboutPlayTimes(); + } +} + +void CAirTunesServer::SetupRemoteControl() +{ + // check if we found the remote control service via zeroconf already or + // if no valid id and headers was received yet + if (m_dacp_id.empty() || m_active_remote_header.empty() || m_pDACP != NULL) + return; + + // check for the service matching m_dacp_id + std::vector services = CZeroconfBrowser::GetInstance()->GetFoundServices(); + for (auto service : services ) + { + if (StringUtils::EqualsNoCase(service.GetType(), std::string(ZEROCONF_DACP_SERVICE) + ".")) + { +#define DACP_NAME_PREFIX "iTunes_Ctrl_" + // name has the form "iTunes_Ctrl_56B29BB6CB904862" + // were we are interested in the 56B29BB6CB904862 identifier + if (StringUtils::StartsWithNoCase(service.GetName(), DACP_NAME_PREFIX)) + { + std::vector tokens = StringUtils::Split(service.GetName(), DACP_NAME_PREFIX); + // if we found the service matching the given identifier + if (tokens.size() > 1 && tokens[1] == m_dacp_id) + { + // resolve the service and save it + CZeroconfBrowser::GetInstance()->ResolveService(service); + std::unique_lock lock(m_dacpLock); + // recheck with lock hold + if (m_pDACP == NULL) + { + // we can control the client with this object now + m_pDACP = new CDACP(m_active_remote_header, service.GetIP(), service.GetPort()); + } + break; + } + } + } + } +} + +void CAirTunesServer::AudioOutputFunctions::audio_set_volume(void *cls, void *session, float volume) +{ + //volume from -30 - 0 - -144 means mute + float volPercent = volume < -30.0f ? 0 : 1 - volume/-30; +#ifdef HAS_AIRPLAY + CAirPlayServer::backupVolume(); +#endif + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL)) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appVolume = components.GetComponent(); + appVolume->SetVolume(volPercent, false); //non-percent volume 0.0-1.0 + } +} + +void CAirTunesServer::AudioOutputFunctions::audio_process(void *cls, void *session, const void *buffer, int buflen) +{ + XFILE::CPipeFile *pipe=(XFILE::CPipeFile *)cls; + pipe->Write(buffer, buflen); + + // in case there are some play times cached that are not yet sent to the player - do it here + InformPlayerAboutPlayTimes(); +} + +void CAirTunesServer::AudioOutputFunctions::audio_destroy(void *cls, void *session) +{ + XFILE::CPipeFile *pipe=(XFILE::CPipeFile *)cls; + pipe->SetEof(); + pipe->Close(); + + CAirTunesServer::FreeDACPRemote(); + m_dacp_id.clear(); + m_active_remote_header.clear(); + + //fix airplay video for ios5 devices + //on ios5 when airplaying video + //the client first opens an airtunes stream + //while the movie is loading + //in that case we don't want to stop the player here + //because this would stop the airplaying video +#ifdef HAS_AIRPLAY + if (!CAirPlayServer::IsPlaying()) +#endif + { + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP); + CLog::Log(LOGDEBUG, "AIRTUNES: AirPlay not running - stopping player"); + } + + m_streamStarted = false; + + // no need to browse for dacp services while we don't receive + // any airtunes streams... + CZeroconfBrowser::GetInstance()->RemoveServiceType(ZEROCONF_DACP_SERVICE); + CZeroconfBrowser::GetInstance()->Stop(); + CAirTunesServer::EnableActionProcessing(false); +} + +void shairplay_log(void *cls, int level, const char *msg) +{ + int xbmcLevel = LOGINFO; + if (!CServiceBroker::GetLogging().CanLogComponent(LOGAIRTUNES)) + return; + + switch(level) + { + case RAOP_LOG_EMERG: // system is unusable + case RAOP_LOG_ALERT: // action must be taken immediately + case RAOP_LOG_CRIT: // critical conditions + xbmcLevel = LOGFATAL; + break; + case RAOP_LOG_ERR: // error conditions + xbmcLevel = LOGERROR; + break; + case RAOP_LOG_WARNING: // warning conditions + xbmcLevel = LOGWARNING; + break; + case RAOP_LOG_NOTICE: // normal but significant condition + case RAOP_LOG_INFO: // informational + xbmcLevel = LOGINFO; + break; + case RAOP_LOG_DEBUG: // debug-level messages + xbmcLevel = LOGDEBUG; + break; + default: + break; + } + CLog::Log(xbmcLevel, "AIRTUNES: {}", msg); +} + +bool CAirTunesServer::StartServer(int port, bool nonlocal, bool usePassword, const std::string &password/*=""*/) +{ + bool success = false; + std::string pw = password; + CNetworkInterface *net = CServiceBroker::GetNetwork().GetFirstConnectedInterface(); + StopServer(true); + + if (net) + { + m_macAddress = net->GetMacAddress(); + StringUtils::Replace(m_macAddress, ":",""); + while (m_macAddress.size() < 12) + { + m_macAddress = '0' + m_macAddress; + } + } + else + { + m_macAddress = "000102030405"; + } + + if (!usePassword) + { + pw.clear(); + } + + ServerInstance = new CAirTunesServer(port, nonlocal); + if (ServerInstance->Initialize(pw)) + { + success = true; + std::string appName = StringUtils::Format("{}@{}", m_macAddress, CSysInfo::GetDeviceName()); + + std::vector > txt; + txt.emplace_back("txtvers", "1"); + txt.emplace_back("cn", "0,1"); + txt.emplace_back("ch", "2"); + txt.emplace_back("ek", "1"); + txt.emplace_back("et", "0,1"); + txt.emplace_back("sv", "false"); + txt.emplace_back("tp", "UDP"); + txt.emplace_back("sm", "false"); + txt.emplace_back("ss", "16"); + txt.emplace_back("sr", "44100"); + txt.emplace_back("pw", usePassword ? "true" : "false"); + txt.emplace_back("vn", "3"); + txt.emplace_back("da", "true"); + txt.emplace_back("md", "0,1,2"); + txt.emplace_back("am", "Kodi,1"); + txt.emplace_back("vs", "130.14"); + + CZeroconf::GetInstance()->PublishService("servers.airtunes", "_raop._tcp", appName, port, txt); + } + + return success; +} + +void CAirTunesServer::StopServer(bool bWait) +{ + if (ServerInstance) + { + ServerInstance->Deinitialize(); + if (bWait) + { + delete ServerInstance; + ServerInstance = NULL; + } + + CZeroconf::GetInstance()->RemoveService("servers.airtunes"); + } +} + +bool CAirTunesServer::IsRunning() +{ + if (ServerInstance == NULL) + return false; + + return ServerInstance->IsRAOPRunningInternal(); +} + +bool CAirTunesServer::IsRAOPRunningInternal() +{ + if (m_pRaop) + { + return raop_is_running(m_pRaop) != 0; + } + + return false; +} + +CAirTunesServer::CAirTunesServer(int port, bool nonlocal) + : CThread("AirTunesActionThread") +{ + m_port = port; + m_pPipe = new XFILE::CPipeFile; +} + +CAirTunesServer::~CAirTunesServer() +{ + delete m_pPipe; +} + +void CAirTunesServer::RegisterActionListener(bool doRegister) +{ + auto& components = CServiceBroker::GetAppComponents(); + const auto appListener = components.GetComponent(); + + if (doRegister) + { + CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this); + appListener->RegisterActionListener(this); + ServerInstance->Create(); + } + else + { + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + appListener->UnregisterActionListener(this); + ServerInstance->StopThread(true); + } +} + +bool CAirTunesServer::Initialize(const std::string &password) +{ + bool ret = false; + + Deinitialize(); + + raop_callbacks_t ao = {}; + ao.cls = m_pPipe; + ao.audio_init = AudioOutputFunctions::audio_init; + ao.audio_set_volume = AudioOutputFunctions::audio_set_volume; + ao.audio_set_metadata = AudioOutputFunctions::audio_set_metadata; + ao.audio_set_coverart = AudioOutputFunctions::audio_set_coverart; + ao.audio_process = AudioOutputFunctions::audio_process; + ao.audio_destroy = AudioOutputFunctions::audio_destroy; + ao.audio_remote_control_id = AudioOutputFunctions::audio_remote_control_id; + ao.audio_set_progress = AudioOutputFunctions::audio_set_progress; + m_pRaop = raop_init(1, &ao, RSA_KEY, nullptr); //1 - we handle one client at a time max + + if (m_pRaop) + { + char macAdr[6]; + unsigned short port = (unsigned short)m_port; + + raop_set_log_level(m_pRaop, RAOP_LOG_WARNING); + if (CServiceBroker::GetLogging().CanLogComponent(LOGAIRTUNES)) + { + raop_set_log_level(m_pRaop, RAOP_LOG_DEBUG); + } + + raop_set_log_callback(m_pRaop, shairplay_log, NULL); + + CNetworkInterface* net = CServiceBroker::GetNetwork().GetFirstConnectedInterface(); + + if (net) + { + net->GetMacAddressRaw(macAdr); + } + + ret = raop_start(m_pRaop, &port, macAdr, 6, password.c_str()) >= 0; + } + return ret; +} + +void CAirTunesServer::Deinitialize() +{ + RegisterActionListener(false); + + if (m_pRaop) + { + raop_stop(m_pRaop); + raop_destroy(m_pRaop); + m_pRaop = nullptr; + } +} -- cgit v1.2.3