2 * Many concepts and protocol specification in this code are taken
3 * from Airplayer. https://github.com/PascalW/Airplayer
7 * This Program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2.1, or (at your option)
12 * This Program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with XBMC; see the file COPYING. If not, see
19 * <http://www.gnu.org/licenses/>.
23 #include "network/Network.h"
24 #include "AirPlayServer.h"
28 #include <netinet/in.h>
29 #include <arpa/inet.h>
30 #include "DllLibPlist.h"
31 #include "utils/log.h"
32 #include "utils/URIUtils.h"
33 #include "utils/StringUtils.h"
34 #include "threads/SingleLock.h"
35 #include "filesystem/File.h"
37 #include "Application.h"
38 #include "ApplicationMessenger.h"
39 #include "utils/md5.h"
40 #include "utils/Variant.h"
41 #include "settings/Settings.h"
42 #include "guilib/Key.h"
44 #include "cores/IPlayer.h"
45 #include "interfaces/AnnouncementManager.h"
47 #include "network/Zeroconf.h"
48 #endif // HAS_ZEROCONF
50 using namespace ANNOUNCEMENT;
53 #define close closesocket
56 #define RECEIVEBUFFER 1024
58 #define AIRPLAY_STATUS_OK 200
59 #define AIRPLAY_STATUS_SWITCHING_PROTOCOLS 101
60 #define AIRPLAY_STATUS_NEED_AUTH 401
61 #define AIRPLAY_STATUS_NOT_FOUND 404
62 #define AIRPLAY_STATUS_METHOD_NOT_ALLOWED 405
63 #define AIRPLAY_STATUS_NOT_IMPLEMENTED 501
64 #define AIRPLAY_STATUS_NO_RESPONSE_NEEDED 1000
66 CCriticalSection CAirPlayServer::ServerInstanceLock;
67 CAirPlayServer *CAirPlayServer::ServerInstance = NULL;
68 int CAirPlayServer::m_isPlaying = 0;
71 #define EVENT_PLAYING 0
72 #define EVENT_PAUSED 1
73 #define EVENT_LOADING 2
74 #define EVENT_STOPPED 3
75 const char *eventStrings[] = {"playing", "paused", "loading", "stopped"};
77 #define PLAYBACK_INFO "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
78 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
79 "<plist version=\"1.0\">\r\n"\
81 "<key>duration</key>\r\n"\
82 "<real>%f</real>\r\n"\
83 "<key>loadedTimeRanges</key>\r\n"\
86 "\t\t\t<key>duration</key>\r\n"\
87 "\t\t\t<real>%f</real>\r\n"\
88 "\t\t\t<key>start</key>\r\n"\
89 "\t\t\t<real>0.0</real>\r\n"\
92 "<key>playbackBufferEmpty</key>\r\n"\
94 "<key>playbackBufferFull</key>\r\n"\
96 "<key>playbackLikelyToKeepUp</key>\r\n"\
98 "<key>position</key>\r\n"\
99 "<real>%f</real>\r\n"\
100 "<key>rate</key>\r\n"\
101 "<real>%d</real>\r\n"\
102 "<key>readyToPlay</key>\r\n"\
104 "<key>seekableTimeRanges</key>\r\n"\
107 "\t\t\t<key>duration</key>\r\n"\
108 "\t\t\t<real>%f</real>\r\n"\
109 "\t\t\t<key>start</key>\r\n"\
110 "\t\t\t<real>0.0</real>\r\n"\
116 #define PLAYBACK_INFO_NOT_READY "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
117 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
118 "<plist version=\"1.0\">\r\n"\
120 "<key>readyToPlay</key>\r\n"\
125 #define SERVER_INFO "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
126 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
127 "<plist version=\"1.0\">\r\n"\
129 "<key>deviceid</key>\r\n"\
130 "<string>%s</string>\r\n"\
131 "<key>features</key>\r\n"\
132 "<integer>119</integer>\r\n"\
133 "<key>model</key>\r\n"\
134 "<string>Xbmc,1</string>\r\n"\
135 "<key>protovers</key>\r\n"\
136 "<string>1.0</string>\r\n"\
137 "<key>srcvers</key>\r\n"\
138 "<string>"AIRPLAY_SERVER_VERSION_STR"</string>\r\n"\
142 #define EVENT_INFO "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\r\n"\
143 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\r\n"\
144 "<plist version=\"1.0\">\r\n"\
146 "<key>category</key>\r\n"\
147 "<string>video</string>\r\n"\
148 "<key>sessionID</key>\r\n"\
149 "<integer>%d</integer>\r\n"\
150 "<key>state</key>\r\n"\
151 "<string>%s</string>\r\n"\
155 #define AUTH_REALM "AirPlay"
156 #define AUTH_REQUIRED "WWW-Authenticate: Digest realm=\"" AUTH_REALM "\", nonce=\"%s\"\r\n"
158 void CAirPlayServer::Announce(AnnouncementFlag flag, const char *sender, const char *message, const CVariant &data)
160 CSingleLock lock(ServerInstanceLock);
162 if ( (flag & Player) && strcmp(sender, "xbmc") == 0 && ServerInstance)
164 if (strcmp(message, "OnStop") == 0)
166 bool shouldRestoreVolume = true;
167 if (data.isMember("player") && data["player"].isMember("playerid"))
168 shouldRestoreVolume = (data["player"]["playerid"] != PLAYLIST_PICTURE);
170 if (shouldRestoreVolume)
173 ServerInstance->AnnounceToClients(EVENT_STOPPED);
175 else if (strcmp(message, "OnPlay") == 0)
177 ServerInstance->AnnounceToClients(EVENT_PLAYING);
179 else if (strcmp(message, "OnPause") == 0)
181 ServerInstance->AnnounceToClients(EVENT_PAUSED);
186 bool CAirPlayServer::StartServer(int port, bool nonlocal)
190 CSingleLock lock(ServerInstanceLock);
192 ServerInstance = new CAirPlayServer(port, nonlocal);
193 if (ServerInstance->Initialize())
195 ServerInstance->Create();
202 bool CAirPlayServer::SetCredentials(bool usePassword, const CStdString& password)
204 CSingleLock lock(ServerInstanceLock);
209 ret = ServerInstance->SetInternalCredentials(usePassword, password);
214 bool CAirPlayServer::SetInternalCredentials(bool usePassword, const CStdString& password)
216 m_usePassword = usePassword;
217 m_password = password;
221 void CAirPlayServer::StopServer(bool bWait)
223 CSingleLock lock(ServerInstanceLock);
226 ServerInstance->StopThread(bWait);
229 delete ServerInstance;
230 ServerInstance = NULL;
235 bool CAirPlayServer::IsRunning()
237 if (ServerInstance == NULL)
240 return ((CThread*)ServerInstance)->IsRunning();
243 void CAirPlayServer::AnnounceToClients(int state)
245 CSingleLock lock (m_connectionLock);
247 std::vector<CTCPClient>::iterator it;
248 for (it = m_connections.begin(); it != m_connections.end(); it++)
250 CStdString reverseHeader;
251 CStdString reverseBody;
253 int reverseSocket = INVALID_SOCKET;
254 it->ComposeReverseEvent(reverseHeader, reverseBody, state);
256 // Send event status per reverse http socket (play, loading, paused)
257 // if we have a reverse header and a reverse socket
258 if (reverseHeader.size() > 0 && m_reverseSockets.find(it->m_sessionId) != m_reverseSockets.end())
260 //search the reverse socket to this sessionid
261 response = StringUtils::Format("POST /event HTTP/1.1\r\n");
262 reverseSocket = m_reverseSockets[it->m_sessionId]; //that is our reverse socket
263 response += reverseHeader;
267 if (reverseBody.size() > 0)
269 response += reverseBody;
272 // don't send it to the connection object
273 // the reverse socket itself belongs to
274 if (reverseSocket != INVALID_SOCKET && reverseSocket != it->m_socket)
276 send(reverseSocket, response.c_str(), response.size(), 0);//send the event status on the eventSocket
281 CAirPlayServer::CAirPlayServer(int port, bool nonlocal) : CThread("AirPlayServer")
284 m_nonlocal = nonlocal;
285 m_ServerSocket = INVALID_SOCKET;
286 m_usePassword = false;
288 CAnnouncementManager::AddAnnouncer(this);
291 CAirPlayServer::~CAirPlayServer()
293 CAnnouncementManager::RemoveAnnouncer(this);
296 void handleZeroconfAnnouncement()
298 #if defined(HAS_ZEROCONF)
299 static XbmcThreads::EndTime timeout(10000);
300 if(timeout.IsTimePast())
302 CZeroconf::GetInstance()->ForceReAnnounceService("servers.airplay");
308 void CAirPlayServer::Process()
311 static int sessionCounter = 0;
317 struct timeval to = {1, 0};
320 FD_SET(m_ServerSocket, &rfds);
321 max_fd = m_ServerSocket;
323 for (unsigned int i = 0; i < m_connections.size(); i++)
325 FD_SET(m_connections[i].m_socket, &rfds);
326 if (m_connections[i].m_socket > max_fd)
327 max_fd = m_connections[i].m_socket;
330 int res = select(max_fd+1, &rfds, NULL, NULL, &to);
333 CLog::Log(LOGERROR, "AIRPLAY Server: Select failed");
339 for (int i = m_connections.size() - 1; i >= 0; i--)
341 int socket = m_connections[i].m_socket;
342 if (FD_ISSET(socket, &rfds))
344 char buffer[RECEIVEBUFFER] = {};
346 nread = recv(socket, (char*)&buffer, RECEIVEBUFFER, 0);
349 CStdString sessionId;
350 m_connections[i].PushBuffer(this, buffer, nread, sessionId, m_reverseSockets);
354 CSingleLock lock (m_connectionLock);
355 CLog::Log(LOGINFO, "AIRPLAY Server: Disconnection detected");
356 m_connections[i].Disconnect();
357 m_connections.erase(m_connections.begin() + i);
362 if (FD_ISSET(m_ServerSocket, &rfds))
364 CLog::Log(LOGDEBUG, "AIRPLAY Server: New connection detected");
365 CTCPClient newconnection;
366 newconnection.m_socket = accept(m_ServerSocket, (struct sockaddr*) &newconnection.m_cliaddr, &newconnection.m_addrlen);
368 newconnection.m_sessionCounter = sessionCounter;
370 if (newconnection.m_socket == INVALID_SOCKET)
372 CLog::Log(LOGERROR, "AIRPLAY Server: Accept of new connection failed: %d", errno);
382 CSingleLock lock (m_connectionLock);
383 CLog::Log(LOGINFO, "AIRPLAY Server: New connection added");
384 m_connections.push_back(newconnection);
389 // by reannouncing the zeroconf service
390 // we fix issues where xbmc is detected
391 // as audio-only target on devices with
393 handleZeroconfAnnouncement();
399 bool CAirPlayServer::Initialize()
403 if ((m_ServerSocket = CreateTCPServerSocket(m_port, !m_nonlocal, 10, "AIRPLAY")) == INVALID_SOCKET)
406 CLog::Log(LOGINFO, "AIRPLAY Server: Successfully initialized");
410 void CAirPlayServer::Deinitialize()
412 CSingleLock lock (m_connectionLock);
413 for (unsigned int i = 0; i < m_connections.size(); i++)
414 m_connections[i].Disconnect();
416 m_connections.clear();
417 m_reverseSockets.clear();
419 if (m_ServerSocket != INVALID_SOCKET)
421 shutdown(m_ServerSocket, SHUT_RDWR);
422 close(m_ServerSocket);
423 m_ServerSocket = INVALID_SOCKET;
427 CAirPlayServer::CTCPClient::CTCPClient()
429 m_socket = INVALID_SOCKET;
430 m_httpParser = new HttpParser();
432 m_addrlen = sizeof(struct sockaddr_storage);
433 m_pLibPlist = new DllLibPlist();
435 m_bAuthenticated = false;
436 m_lastEvent = EVENT_NONE;
439 CAirPlayServer::CTCPClient::CTCPClient(const CTCPClient& client)
442 m_httpParser = new HttpParser();
443 m_pLibPlist = new DllLibPlist();
446 CAirPlayServer::CTCPClient::~CTCPClient()
448 if (m_pLibPlist->IsLoaded())
450 m_pLibPlist->Unload();
456 CAirPlayServer::CTCPClient& CAirPlayServer::CTCPClient::operator=(const CTCPClient& client)
459 m_httpParser = new HttpParser();
460 m_pLibPlist = new DllLibPlist();
464 void CAirPlayServer::CTCPClient::PushBuffer(CAirPlayServer *host, const char *buffer,
465 int length, CStdString &sessionId, std::map<CStdString,
466 int> &reverseSockets)
468 HttpParser::status_t status = m_httpParser->addBytes(buffer, length);
470 if (status == HttpParser::Done)
473 CStdString responseHeader;
474 CStdString responseBody;
475 int status = ProcessRequest(responseHeader, responseBody);
476 sessionId = m_sessionId;
477 CStdString statusMsg = "OK";
481 case AIRPLAY_STATUS_NOT_IMPLEMENTED:
482 statusMsg = "Not Implemented";
484 case AIRPLAY_STATUS_SWITCHING_PROTOCOLS:
485 statusMsg = "Switching Protocols";
486 reverseSockets[sessionId] = m_socket;//save this socket as reverse http socket for this sessionid
488 case AIRPLAY_STATUS_NEED_AUTH:
489 statusMsg = "Unauthorized";
491 case AIRPLAY_STATUS_NOT_FOUND:
492 statusMsg = "Not Found";
494 case AIRPLAY_STATUS_METHOD_NOT_ALLOWED:
495 statusMsg = "Method Not Allowed";
499 // Prepare the response
501 const time_t ltime = time(NULL);
502 char *date = asctime(gmtime(<ime)); //Fri, 17 Dec 2010 11:18:01 GMT;
503 date[strlen(date) - 1] = '\0'; // remove \n
504 response = StringUtils::Format("HTTP/1.1 %d %s\nDate: %s\r\n", status, statusMsg.c_str(), date);
505 if (responseHeader.size() > 0)
507 response += responseHeader;
510 if (responseBody.size() > 0)
512 response = StringUtils::Format("%sContent-Length: %d\r\n", response.c_str(), responseBody.size());
516 if (responseBody.size() > 0)
518 response += responseBody;
522 //don't send response on AIRPLAY_STATUS_NO_RESPONSE_NEEDED
523 if (status != AIRPLAY_STATUS_NO_RESPONSE_NEEDED)
525 send(m_socket, response.c_str(), response.size(), 0);
527 // We need a new parser...
529 m_httpParser = new HttpParser;
533 void CAirPlayServer::CTCPClient::Disconnect()
535 if (m_socket != INVALID_SOCKET)
537 CSingleLock lock (m_critSection);
538 shutdown(m_socket, SHUT_RDWR);
540 m_socket = INVALID_SOCKET;
546 void CAirPlayServer::CTCPClient::Copy(const CTCPClient& client)
548 m_socket = client.m_socket;
549 m_cliaddr = client.m_cliaddr;
550 m_addrlen = client.m_addrlen;
551 m_httpParser = client.m_httpParser;
552 m_authNonce = client.m_authNonce;
553 m_bAuthenticated = client.m_bAuthenticated;
554 m_sessionCounter = client.m_sessionCounter;
558 void CAirPlayServer::CTCPClient::ComposeReverseEvent( CStdString& reverseHeader,
559 CStdString& reverseBody,
563 if ( m_lastEvent != state )
571 reverseBody = StringUtils::Format(EVENT_INFO, m_sessionCounter, eventStrings[state]);
572 CLog::Log(LOGDEBUG, "AIRPLAY: sending event: %s", eventStrings[state]);
575 reverseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
576 reverseHeader = StringUtils::Format("%sContent-Length: %d\r\n",reverseHeader.c_str(), reverseBody.size());
577 reverseHeader = StringUtils::Format("%sx-apple-session-id: %s\r\n",reverseHeader.c_str(), m_sessionId.c_str());
582 void CAirPlayServer::CTCPClient::ComposeAuthRequestAnswer(CStdString& responseHeader, CStdString& responseBody)
584 int16_t random=rand();
585 CStdString randomStr = StringUtils::Format("%i", random);
586 m_authNonce=XBMC::XBMC_MD5::GetMD5(randomStr);
587 responseHeader = StringUtils::Format(AUTH_REQUIRED, m_authNonce.c_str());
588 responseBody.clear();
593 CStdString calcResponse(const CStdString& username,
594 const CStdString& password,
595 const CStdString& realm,
596 const CStdString& method,
597 const CStdString& digestUri,
598 const CStdString& nonce)
604 HA1 = XBMC::XBMC_MD5::GetMD5(username + ":" + realm + ":" + password);
605 HA2 = XBMC::XBMC_MD5::GetMD5(method + ":" + digestUri);
606 StringUtils::ToLower(HA1);
607 StringUtils::ToLower(HA2);
608 response = XBMC::XBMC_MD5::GetMD5(HA1 + ":" + nonce + ":" + HA2);
609 StringUtils::ToLower(response);
614 //from a string field1="value1", field2="value2" it parses the value to a field
615 CStdString getFieldFromString(const CStdString &str, const char* field)
618 CStdStringArray tmpAr1;
619 CStdStringArray tmpAr2;
621 StringUtils::SplitString(str, ",", tmpAr1);
623 for(unsigned int i = 0;i<tmpAr1.size();i++)
625 if (tmpAr1[i].find(field) != std::string::npos)
627 if (StringUtils::SplitString(tmpAr1[i], "=", tmpAr2) == 2)
629 StringUtils::Replace(tmpAr2[1], "\"", "");//remove quotes
637 bool CAirPlayServer::CTCPClient::checkAuthorization(const CStdString& authStr,
638 const CStdString& method,
639 const CStdString& uri)
641 bool authValid = true;
648 //first get username - we allow all usernames for airplay (usually it is AirPlay)
649 username = getFieldFromString(authStr, "username");
650 if (username.empty())
658 if (getFieldFromString(authStr, "realm") != AUTH_REALM)
667 if (getFieldFromString(authStr, "nonce") != m_authNonce)
676 if (getFieldFromString(authStr, "uri") != uri)
682 //last check response
685 CStdString realm = AUTH_REALM;
686 CStdString ourResponse = calcResponse(username, ServerInstance->m_password, realm, method, uri, m_authNonce);
687 CStdString theirResponse = getFieldFromString(authStr, "response");
688 if (!theirResponse.Equals(ourResponse, false))
691 CLog::Log(LOGDEBUG,"AirAuth: response mismatch - our: %s theirs: %s",ourResponse.c_str(), theirResponse.c_str());
695 CLog::Log(LOGDEBUG, "AirAuth: successfull authentication from AirPlay client");
698 m_bAuthenticated = authValid;
699 return m_bAuthenticated;
702 void CAirPlayServer::backupVolume()
704 CSingleLock lock(ServerInstanceLock);
706 if (ServerInstance && ServerInstance->m_origVolume == -1)
707 ServerInstance->m_origVolume = (int)g_application.GetVolume();
710 void CAirPlayServer::restoreVolume()
712 CSingleLock lock(ServerInstanceLock);
714 if (ServerInstance && ServerInstance->m_origVolume != -1 && CSettings::Get().GetBool("services.airplayvolumecontrol"))
716 g_application.SetVolume((float)ServerInstance->m_origVolume);
717 ServerInstance->m_origVolume = -1;
721 int CAirPlayServer::CTCPClient::ProcessRequest( CStdString& responseHeader,
722 CStdString& responseBody)
724 CStdString method = m_httpParser->getMethod();
725 CStdString uri = m_httpParser->getUri();
726 CStdString queryString = m_httpParser->getQueryString();
727 CStdString body = m_httpParser->getBody();
728 CStdString contentType = m_httpParser->getValue("content-type");
729 m_sessionId = m_httpParser->getValue("x-apple-session-id");
730 CStdString authorization = m_httpParser->getValue("authorization");
731 int status = AIRPLAY_STATUS_OK;
732 bool needAuth = false;
734 if (m_sessionId.empty())
735 m_sessionId = "00000000-0000-0000-0000-000000000000";
737 if (ServerInstance->m_usePassword && !m_bAuthenticated)
742 size_t startQs = uri.find('?');
743 if (startQs != std::string::npos)
748 // This is the socket which will be used for reverse HTTP
749 // negotiate reverse HTTP via upgrade
750 if (uri == "/reverse")
752 status = AIRPLAY_STATUS_SWITCHING_PROTOCOLS;
753 responseHeader = "Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
756 // The rate command is used to play/pause media.
757 // A value argument should be supplied which indicates media should be played or paused.
760 else if (uri == "/rate")
762 const char* found = strstr(queryString.c_str(), "value=");
763 int rate = found ? (int)(atof(found + strlen("value=")) + 0.5f) : 0;
765 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s with rate %i", uri.c_str(), rate);
767 if (needAuth && !checkAuthorization(authorization, method, uri))
769 status = AIRPLAY_STATUS_NEED_AUTH;
773 if (g_application.m_pPlayer->IsPlaying() && !g_application.m_pPlayer->IsPaused())
775 CApplicationMessenger::Get().MediaPause();
780 if (g_application.m_pPlayer->IsPausedPlayback())
782 CApplicationMessenger::Get().MediaPause();
787 // The volume command is used to change playback volume.
788 // A value argument should be supplied which indicates how loud we should get.
789 // 0.000000 => silent
791 else if (uri == "/volume")
793 const char* found = strstr(queryString.c_str(), "volume=");
794 float volume = found ? (float)strtod(found + strlen("volume="), NULL) : 0;
796 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s with volume %f", uri.c_str(), volume);
798 if (needAuth && !checkAuthorization(authorization, method, uri))
800 status = AIRPLAY_STATUS_NEED_AUTH;
802 else if (volume >= 0 && volume <= 1)
804 float oldVolume = g_application.GetVolume();
806 if(oldVolume != volume && CSettings::Get().GetBool("services.airplayvolumecontrol"))
809 g_application.SetVolume(volume);
810 CApplicationMessenger::Get().ShowVolumeBar(oldVolume < volume);
816 // Contains a header like format in the request body which should contain a
817 // Content-Location and optionally a Start-Position
818 else if (uri == "/play")
821 float position = 0.0;
822 m_lastEvent = EVENT_NONE;
824 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
826 if (needAuth && !checkAuthorization(authorization, method, uri))
828 status = AIRPLAY_STATUS_NEED_AUTH;
830 else if (contentType == "application/x-apple-binary-plist")
832 CAirPlayServer::m_isPlaying++;
834 if (m_pLibPlist->Load())
836 m_pLibPlist->EnableDelayedUnload(false);
838 const char* bodyChr = m_httpParser->getBody();
841 m_pLibPlist->plist_from_bin(bodyChr, m_httpParser->getContentLength(), &dict);
843 if (m_pLibPlist->plist_dict_get_size(dict))
845 plist_t tmpNode = m_pLibPlist->plist_dict_get_item(dict, "Start-Position");
848 double tmpDouble = 0;
849 m_pLibPlist->plist_get_real_val(tmpNode, &tmpDouble);
850 position = (float)tmpDouble;
853 tmpNode = m_pLibPlist->plist_dict_get_item(dict, "Content-Location");
857 m_pLibPlist->plist_get_string_val(tmpNode, &tmpStr);
859 #ifdef TARGET_WINDOWS
860 m_pLibPlist->plist_free_string_val(tmpStr);
868 m_pLibPlist->plist_free(dict);
873 CLog::Log(LOGERROR, "Error parsing plist");
875 m_pLibPlist->Unload();
880 CAirPlayServer::m_isPlaying++;
882 std::string contentLocation = "Content-Location: ";
883 size_t start = body.find(contentLocation);
884 if (start == std::string::npos)
885 return AIRPLAY_STATUS_NOT_IMPLEMENTED;
886 start += contentLocation.size();
887 int end = body.find('\n', start);
888 location = body.substr(start, end - start);
890 std::string startPosition = "Start-Position: ";
891 start = body.find(startPosition);
892 if (start != std::string::npos)
894 start += startPosition.size();
895 int end = body.find('\n', start);
896 std::string positionStr = body.substr(start, end - start);
897 position = (float)atof(positionStr.c_str());
901 if (status != AIRPLAY_STATUS_NEED_AUTH)
903 CStdString userAgent(CURL::Encode("AppleCoreMedia/1.0.0.8F455 (AppleTV; U; CPU OS 4_3 like Mac OS X; de_de)"));
904 location += "|User-Agent=" + userAgent;
906 CFileItem fileToPlay(location, false);
907 fileToPlay.SetProperty("StartPercent", position*100.0f);
908 ServerInstance->AnnounceToClients(EVENT_LOADING);
909 // froce to internal dvdplayer cause it is the only
910 // one who will work well with airplay
911 g_application.m_eForcedNextPlayer = EPC_DVDPLAYER;
912 CApplicationMessenger::Get().MediaPlay(fileToPlay);
916 // Used to perform seeking (POST request) and to retrieve current player position (GET request).
917 // GET scrub seems to also set rate 1 - strange but true
918 else if (uri == "/scrub")
920 if (needAuth && !checkAuthorization(authorization, method, uri))
922 status = AIRPLAY_STATUS_NEED_AUTH;
924 else if (method == "GET")
926 CLog::Log(LOGDEBUG, "AIRPLAY: got GET request %s", uri.c_str());
928 if (g_application.m_pPlayer->GetTotalTime())
930 float position = ((float) g_application.m_pPlayer->GetTime()) / 1000;
931 responseBody = StringUtils::Format("duration: %.6f\r\nposition: %.6f\r\n", (float)g_application.m_pPlayer->GetTotalTime() / 1000, position);
935 status = AIRPLAY_STATUS_METHOD_NOT_ALLOWED;
940 const char* found = strstr(queryString.c_str(), "position=");
942 if (found && g_application.m_pPlayer->HasPlayer())
944 int64_t position = (int64_t) (atof(found + strlen("position=")) * 1000.0);
945 g_application.m_pPlayer->SeekTime(position);
946 CLog::Log(LOGDEBUG, "AIRPLAY: got POST request %s with pos %"PRId64, uri.c_str(), position);
951 // Sent when media playback should be stopped
952 else if (uri == "/stop")
954 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
955 if (needAuth && !checkAuthorization(authorization, method, uri))
957 status = AIRPLAY_STATUS_NEED_AUTH;
961 if (IsPlaying()) //only stop player if we started him
963 CApplicationMessenger::Get().MediaStop();
964 CAirPlayServer::m_isPlaying--;
966 else //if we are not playing and get the stop request - we just wanna stop picture streaming
968 CApplicationMessenger::Get().SendAction(ACTION_PREVIOUS_MENU);
973 // RAW JPEG data is contained in the request body
974 else if (uri == "/photo")
976 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
977 if (needAuth && !checkAuthorization(authorization, method, uri))
979 status = AIRPLAY_STATUS_NEED_AUTH;
981 else if (m_httpParser->getContentLength() > 0)
983 XFILE::CFile tmpFile;
984 CStdString tmpFileName = "special://temp/airplay_photo.jpg";
986 if( m_httpParser->getContentLength() > 3 &&
987 m_httpParser->getBody()[1] == 'P' &&
988 m_httpParser->getBody()[2] == 'N' &&
989 m_httpParser->getBody()[3] == 'G')
991 tmpFileName = "special://temp/airplay_photo.png";
994 if (tmpFile.OpenForWrite(tmpFileName, true))
997 writtenBytes = tmpFile.Write(m_httpParser->getBody(), m_httpParser->getContentLength());
1000 if (writtenBytes > 0 && (unsigned int)writtenBytes == m_httpParser->getContentLength())
1002 CApplicationMessenger::Get().PictureShow(tmpFileName);
1006 CLog::Log(LOGERROR,"AirPlayServer: Error writing tmpFile.");
1012 else if (uri == "/playback-info")
1014 float position = 0.0f;
1015 float duration = 0.0f;
1016 float cachePosition = 0.0f;
1017 bool playing = false;
1019 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
1021 if (needAuth && !checkAuthorization(authorization, method, uri))
1023 status = AIRPLAY_STATUS_NEED_AUTH;
1025 else if (g_application.m_pPlayer->HasPlayer())
1027 if (g_application.m_pPlayer->GetTotalTime())
1029 position = ((float) g_application.m_pPlayer->GetTime()) / 1000;
1030 duration = ((float) g_application.m_pPlayer->GetTotalTime()) / 1000;
1031 playing = !g_application.m_pPlayer->IsPaused();
1032 cachePosition = position + (duration * g_application.m_pPlayer->GetCachePercentage() / 100.0f);
1035 responseBody = StringUtils::Format(PLAYBACK_INFO, duration, cachePosition, position, (playing ? 1 : 0), duration);
1036 responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
1038 if (g_application.m_pPlayer->IsCaching())
1040 CAirPlayServer::ServerInstance->AnnounceToClients(EVENT_LOADING);
1045 responseBody = StringUtils::Format(PLAYBACK_INFO_NOT_READY, duration, cachePosition, position, (playing ? 1 : 0), duration);
1046 responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
1050 else if (uri == "/server-info")
1052 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
1053 responseBody = StringUtils::Format(SERVER_INFO, g_application.getNetwork().GetFirstConnectedInterface()->GetMacAddress().c_str());
1054 responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
1057 else if (uri == "/slideshow-features")
1062 else if (uri == "/authorize")
1064 // DRM, ignore for now.
1067 else if (uri == "/setProperty")
1069 status = AIRPLAY_STATUS_NOT_FOUND;
1072 else if (uri == "/getProperty")
1074 status = AIRPLAY_STATUS_NOT_FOUND;
1077 else if (uri == "200") //response OK from the event reverse message
1079 status = AIRPLAY_STATUS_NO_RESPONSE_NEEDED;
1083 CLog::Log(LOGERROR, "AIRPLAY Server: unhandled request [%s]\n", uri.c_str());
1084 status = AIRPLAY_STATUS_NOT_IMPLEMENTED;
1087 if (status == AIRPLAY_STATUS_NEED_AUTH)
1089 ComposeAuthRequestAnswer(responseHeader, responseBody);