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 CAirPlayServer *CAirPlayServer::ServerInstance = NULL;
67 int CAirPlayServer::m_isPlaying = 0;
70 #define EVENT_PLAYING 0
71 #define EVENT_PAUSED 1
72 #define EVENT_LOADING 2
73 #define EVENT_STOPPED 3
74 const char *eventStrings[] = {"playing", "paused", "loading", "stopped"};
76 #define PLAYBACK_INFO "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
77 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
78 "<plist version=\"1.0\">\r\n"\
80 "<key>duration</key>\r\n"\
81 "<real>%f</real>\r\n"\
82 "<key>loadedTimeRanges</key>\r\n"\
85 "\t\t\t<key>duration</key>\r\n"\
86 "\t\t\t<real>%f</real>\r\n"\
87 "\t\t\t<key>start</key>\r\n"\
88 "\t\t\t<real>0.0</real>\r\n"\
91 "<key>playbackBufferEmpty</key>\r\n"\
93 "<key>playbackBufferFull</key>\r\n"\
95 "<key>playbackLikelyToKeepUp</key>\r\n"\
97 "<key>position</key>\r\n"\
98 "<real>%f</real>\r\n"\
99 "<key>rate</key>\r\n"\
100 "<real>%d</real>\r\n"\
101 "<key>readyToPlay</key>\r\n"\
103 "<key>seekableTimeRanges</key>\r\n"\
106 "\t\t\t<key>duration</key>\r\n"\
107 "\t\t\t<real>%f</real>\r\n"\
108 "\t\t\t<key>start</key>\r\n"\
109 "\t\t\t<real>0.0</real>\r\n"\
115 #define PLAYBACK_INFO_NOT_READY "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
116 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
117 "<plist version=\"1.0\">\r\n"\
119 "<key>readyToPlay</key>\r\n"\
124 #define SERVER_INFO "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
125 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
126 "<plist version=\"1.0\">\r\n"\
128 "<key>deviceid</key>\r\n"\
129 "<string>%s</string>\r\n"\
130 "<key>features</key>\r\n"\
131 "<integer>119</integer>\r\n"\
132 "<key>model</key>\r\n"\
133 "<string>Xbmc,1</string>\r\n"\
134 "<key>protovers</key>\r\n"\
135 "<string>1.0</string>\r\n"\
136 "<key>srcvers</key>\r\n"\
137 "<string>"AIRPLAY_SERVER_VERSION_STR"</string>\r\n"\
141 #define EVENT_INFO "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\r\n"\
142 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\r\n"\
143 "<plist version=\"1.0\">\r\n"\
145 "<key>category</key>\r\n"\
146 "<string>video</string>\r\n"\
147 "<key>sessionID</key>\r\n"\
148 "<integer>%d</integer>\r\n"\
149 "<key>state</key>\r\n"\
150 "<string>%s</string>\r\n"\
154 #define AUTH_REALM "AirPlay"
155 #define AUTH_REQUIRED "WWW-Authenticate: Digest realm=\"" AUTH_REALM "\", nonce=\"%s\"\r\n"
157 void CAirPlayServer::Announce(AnnouncementFlag flag, const char *sender, const char *message, const CVariant &data)
159 if ( (flag & Player) && strcmp(sender, "xbmc") == 0 && ServerInstance)
161 if (strcmp(message, "OnStop") == 0)
163 bool shouldRestoreVolume = true;
164 if (data.isMember("player") && data["player"].isMember("playerid"))
165 shouldRestoreVolume = (data["player"]["playerid"] != PLAYLIST_PICTURE);
167 if (shouldRestoreVolume)
170 ServerInstance->AnnounceToClients(EVENT_STOPPED);
172 else if (strcmp(message, "OnPlay") == 0)
174 ServerInstance->AnnounceToClients(EVENT_PLAYING);
176 else if (strcmp(message, "OnPause") == 0)
178 ServerInstance->AnnounceToClients(EVENT_PAUSED);
183 bool CAirPlayServer::StartServer(int port, bool nonlocal)
187 ServerInstance = new CAirPlayServer(port, nonlocal);
188 if (ServerInstance->Initialize())
190 ServerInstance->Create();
197 bool CAirPlayServer::SetCredentials(bool usePassword, const CStdString& password)
203 ret = ServerInstance->SetInternalCredentials(usePassword, password);
208 bool CAirPlayServer::SetInternalCredentials(bool usePassword, const CStdString& password)
210 m_usePassword = usePassword;
211 m_password = password;
215 void CAirPlayServer::StopServer(bool bWait)
219 ServerInstance->StopThread(bWait);
222 delete ServerInstance;
223 ServerInstance = NULL;
228 bool CAirPlayServer::IsRunning()
230 if (ServerInstance == NULL)
233 return ((CThread*)ServerInstance)->IsRunning();
236 void CAirPlayServer::AnnounceToClients(int state)
238 CSingleLock lock (m_connectionLock);
240 std::vector<CTCPClient>::iterator it;
241 for (it = m_connections.begin(); it != m_connections.end(); it++)
243 CStdString reverseHeader;
244 CStdString reverseBody;
246 int reverseSocket = INVALID_SOCKET;
247 it->ComposeReverseEvent(reverseHeader, reverseBody, state);
249 // Send event status per reverse http socket (play, loading, paused)
250 // if we have a reverse header and a reverse socket
251 if (reverseHeader.size() > 0 && m_reverseSockets.find(it->m_sessionId) != m_reverseSockets.end())
253 //search the reverse socket to this sessionid
254 response = StringUtils::Format("POST /event HTTP/1.1\r\n");
255 reverseSocket = m_reverseSockets[it->m_sessionId]; //that is our reverse socket
256 response += reverseHeader;
260 if (reverseBody.size() > 0)
262 response += reverseBody;
265 // don't send it to the connection object
266 // the reverse socket itself belongs to
267 if (reverseSocket != INVALID_SOCKET && reverseSocket != it->m_socket)
269 send(reverseSocket, response.c_str(), response.size(), 0);//send the event status on the eventSocket
274 CAirPlayServer::CAirPlayServer(int port, bool nonlocal) : CThread("AirPlayServer")
277 m_nonlocal = nonlocal;
278 m_ServerSocket = INVALID_SOCKET;
279 m_usePassword = false;
281 CAnnouncementManager::AddAnnouncer(this);
284 CAirPlayServer::~CAirPlayServer()
286 CAnnouncementManager::RemoveAnnouncer(this);
289 void handleZeroconfAnnouncement()
291 #if defined(HAS_ZEROCONF)
292 static XbmcThreads::EndTime timeout(10000);
293 if(timeout.IsTimePast())
295 CZeroconf::GetInstance()->ForceReAnnounceService("servers.airplay");
301 void CAirPlayServer::Process()
304 static int sessionCounter = 0;
310 struct timeval to = {1, 0};
313 FD_SET(m_ServerSocket, &rfds);
314 max_fd = m_ServerSocket;
316 for (unsigned int i = 0; i < m_connections.size(); i++)
318 FD_SET(m_connections[i].m_socket, &rfds);
319 if (m_connections[i].m_socket > max_fd)
320 max_fd = m_connections[i].m_socket;
323 int res = select(max_fd+1, &rfds, NULL, NULL, &to);
326 CLog::Log(LOGERROR, "AIRPLAY Server: Select failed");
332 for (int i = m_connections.size() - 1; i >= 0; i--)
334 int socket = m_connections[i].m_socket;
335 if (FD_ISSET(socket, &rfds))
337 char buffer[RECEIVEBUFFER] = {};
339 nread = recv(socket, (char*)&buffer, RECEIVEBUFFER, 0);
342 CStdString sessionId;
343 m_connections[i].PushBuffer(this, buffer, nread, sessionId, m_reverseSockets);
347 CSingleLock lock (m_connectionLock);
348 CLog::Log(LOGINFO, "AIRPLAY Server: Disconnection detected");
349 m_connections[i].Disconnect();
350 m_connections.erase(m_connections.begin() + i);
355 if (FD_ISSET(m_ServerSocket, &rfds))
357 CLog::Log(LOGDEBUG, "AIRPLAY Server: New connection detected");
358 CTCPClient newconnection;
359 newconnection.m_socket = accept(m_ServerSocket, (struct sockaddr*) &newconnection.m_cliaddr, &newconnection.m_addrlen);
361 newconnection.m_sessionCounter = sessionCounter;
363 if (newconnection.m_socket == INVALID_SOCKET)
365 CLog::Log(LOGERROR, "AIRPLAY Server: Accept of new connection failed: %d", errno);
375 CSingleLock lock (m_connectionLock);
376 CLog::Log(LOGINFO, "AIRPLAY Server: New connection added");
377 m_connections.push_back(newconnection);
382 // by reannouncing the zeroconf service
383 // we fix issues where xbmc is detected
384 // as audio-only target on devices with
386 handleZeroconfAnnouncement();
392 bool CAirPlayServer::Initialize()
396 if ((m_ServerSocket = CreateTCPServerSocket(m_port, !m_nonlocal, 10, "AIRPLAY")) == INVALID_SOCKET)
399 CLog::Log(LOGINFO, "AIRPLAY Server: Successfully initialized");
403 void CAirPlayServer::Deinitialize()
405 CSingleLock lock (m_connectionLock);
406 for (unsigned int i = 0; i < m_connections.size(); i++)
407 m_connections[i].Disconnect();
409 m_connections.clear();
410 m_reverseSockets.clear();
412 if (m_ServerSocket != INVALID_SOCKET)
414 shutdown(m_ServerSocket, SHUT_RDWR);
415 close(m_ServerSocket);
416 m_ServerSocket = INVALID_SOCKET;
420 CAirPlayServer::CTCPClient::CTCPClient()
422 m_socket = INVALID_SOCKET;
423 m_httpParser = new HttpParser();
425 m_addrlen = sizeof(struct sockaddr_storage);
426 m_pLibPlist = new DllLibPlist();
428 m_bAuthenticated = false;
429 m_lastEvent = EVENT_NONE;
432 CAirPlayServer::CTCPClient::CTCPClient(const CTCPClient& client)
435 m_httpParser = new HttpParser();
436 m_pLibPlist = new DllLibPlist();
439 CAirPlayServer::CTCPClient::~CTCPClient()
441 if (m_pLibPlist->IsLoaded())
443 m_pLibPlist->Unload();
449 CAirPlayServer::CTCPClient& CAirPlayServer::CTCPClient::operator=(const CTCPClient& client)
452 m_httpParser = new HttpParser();
453 m_pLibPlist = new DllLibPlist();
457 void CAirPlayServer::CTCPClient::PushBuffer(CAirPlayServer *host, const char *buffer,
458 int length, CStdString &sessionId, std::map<CStdString,
459 int> &reverseSockets)
461 HttpParser::status_t status = m_httpParser->addBytes(buffer, length);
463 if (status == HttpParser::Done)
466 CStdString responseHeader;
467 CStdString responseBody;
468 int status = ProcessRequest(responseHeader, responseBody);
469 sessionId = m_sessionId;
470 CStdString statusMsg = "OK";
474 case AIRPLAY_STATUS_NOT_IMPLEMENTED:
475 statusMsg = "Not Implemented";
477 case AIRPLAY_STATUS_SWITCHING_PROTOCOLS:
478 statusMsg = "Switching Protocols";
479 reverseSockets[sessionId] = m_socket;//save this socket as reverse http socket for this sessionid
481 case AIRPLAY_STATUS_NEED_AUTH:
482 statusMsg = "Unauthorized";
484 case AIRPLAY_STATUS_NOT_FOUND:
485 statusMsg = "Not Found";
487 case AIRPLAY_STATUS_METHOD_NOT_ALLOWED:
488 statusMsg = "Method Not Allowed";
492 // Prepare the response
494 const time_t ltime = time(NULL);
495 char *date = asctime(gmtime(<ime)); //Fri, 17 Dec 2010 11:18:01 GMT;
496 date[strlen(date) - 1] = '\0'; // remove \n
497 response = StringUtils::Format("HTTP/1.1 %d %s\nDate: %s\r\n", status, statusMsg.c_str(), date);
498 if (responseHeader.size() > 0)
500 response += responseHeader;
503 if (responseBody.size() > 0)
505 response = StringUtils::Format("%sContent-Length: %d\r\n", response.c_str(), responseBody.size());
509 if (responseBody.size() > 0)
511 response += responseBody;
515 //don't send response on AIRPLAY_STATUS_NO_RESPONSE_NEEDED
516 if (status != AIRPLAY_STATUS_NO_RESPONSE_NEEDED)
518 send(m_socket, response.c_str(), response.size(), 0);
520 // We need a new parser...
522 m_httpParser = new HttpParser;
526 void CAirPlayServer::CTCPClient::Disconnect()
528 if (m_socket != INVALID_SOCKET)
530 CSingleLock lock (m_critSection);
531 shutdown(m_socket, SHUT_RDWR);
533 m_socket = INVALID_SOCKET;
539 void CAirPlayServer::CTCPClient::Copy(const CTCPClient& client)
541 m_socket = client.m_socket;
542 m_cliaddr = client.m_cliaddr;
543 m_addrlen = client.m_addrlen;
544 m_httpParser = client.m_httpParser;
545 m_authNonce = client.m_authNonce;
546 m_bAuthenticated = client.m_bAuthenticated;
547 m_sessionCounter = client.m_sessionCounter;
551 void CAirPlayServer::CTCPClient::ComposeReverseEvent( CStdString& reverseHeader,
552 CStdString& reverseBody,
556 if ( m_lastEvent != state )
564 reverseBody = StringUtils::Format(EVENT_INFO, m_sessionCounter, eventStrings[state]);
565 CLog::Log(LOGDEBUG, "AIRPLAY: sending event: %s", eventStrings[state]);
568 reverseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
569 reverseHeader = StringUtils::Format("%sContent-Length: %d\r\n",reverseHeader.c_str(), reverseBody.size());
570 reverseHeader = StringUtils::Format("%sx-apple-session-id: %s\r\n",reverseHeader.c_str(), m_sessionId.c_str());
575 void CAirPlayServer::CTCPClient::ComposeAuthRequestAnswer(CStdString& responseHeader, CStdString& responseBody)
577 int16_t random=rand();
578 CStdString randomStr = StringUtils::Format("%i", random);
579 m_authNonce=XBMC::XBMC_MD5::GetMD5(randomStr);
580 responseHeader = StringUtils::Format(AUTH_REQUIRED, m_authNonce.c_str());
581 responseBody.clear();
586 CStdString calcResponse(const CStdString& username,
587 const CStdString& password,
588 const CStdString& realm,
589 const CStdString& method,
590 const CStdString& digestUri,
591 const CStdString& nonce)
597 HA1 = XBMC::XBMC_MD5::GetMD5(username + ":" + realm + ":" + password);
598 HA2 = XBMC::XBMC_MD5::GetMD5(method + ":" + digestUri);
599 StringUtils::ToLower(HA1);
600 StringUtils::ToLower(HA2);
601 response = XBMC::XBMC_MD5::GetMD5(HA1 + ":" + nonce + ":" + HA2);
602 StringUtils::ToLower(response);
607 //from a string field1="value1", field2="value2" it parses the value to a field
608 CStdString getFieldFromString(const CStdString &str, const char* field)
611 CStdStringArray tmpAr1;
612 CStdStringArray tmpAr2;
614 StringUtils::SplitString(str, ",", tmpAr1);
616 for(unsigned int i = 0;i<tmpAr1.size();i++)
618 if (tmpAr1[i].find(field) != std::string::npos)
620 if (StringUtils::SplitString(tmpAr1[i], "=", tmpAr2) == 2)
622 StringUtils::Replace(tmpAr2[1], "\"", "");//remove quotes
630 bool CAirPlayServer::CTCPClient::checkAuthorization(const CStdString& authStr,
631 const CStdString& method,
632 const CStdString& uri)
634 bool authValid = true;
641 //first get username - we allow all usernames for airplay (usually it is AirPlay)
642 username = getFieldFromString(authStr, "username");
643 if (username.empty())
651 if (getFieldFromString(authStr, "realm") != AUTH_REALM)
660 if (getFieldFromString(authStr, "nonce") != m_authNonce)
669 if (getFieldFromString(authStr, "uri") != uri)
675 //last check response
678 CStdString realm = AUTH_REALM;
679 CStdString ourResponse = calcResponse(username, ServerInstance->m_password, realm, method, uri, m_authNonce);
680 CStdString theirResponse = getFieldFromString(authStr, "response");
681 if (!theirResponse.Equals(ourResponse, false))
684 CLog::Log(LOGDEBUG,"AirAuth: response mismatch - our: %s theirs: %s",ourResponse.c_str(), theirResponse.c_str());
688 CLog::Log(LOGDEBUG, "AirAuth: successfull authentication from AirPlay client");
691 m_bAuthenticated = authValid;
692 return m_bAuthenticated;
695 void CAirPlayServer::backupVolume()
697 if (ServerInstance->m_origVolume == -1)
698 ServerInstance->m_origVolume = (int)g_application.GetVolume();
701 void CAirPlayServer::restoreVolume()
703 if (ServerInstance->m_origVolume != -1 && CSettings::Get().GetBool("services.airplayvolumecontrol"))
705 g_application.SetVolume((float)ServerInstance->m_origVolume);
706 ServerInstance->m_origVolume = -1;
710 int CAirPlayServer::CTCPClient::ProcessRequest( CStdString& responseHeader,
711 CStdString& responseBody)
713 CStdString method = m_httpParser->getMethod();
714 CStdString uri = m_httpParser->getUri();
715 CStdString queryString = m_httpParser->getQueryString();
716 CStdString body = m_httpParser->getBody();
717 CStdString contentType = m_httpParser->getValue("content-type");
718 m_sessionId = m_httpParser->getValue("x-apple-session-id");
719 CStdString authorization = m_httpParser->getValue("authorization");
720 int status = AIRPLAY_STATUS_OK;
721 bool needAuth = false;
723 if (m_sessionId.empty())
724 m_sessionId = "00000000-0000-0000-0000-000000000000";
726 if (ServerInstance->m_usePassword && !m_bAuthenticated)
731 size_t startQs = uri.find('?');
732 if (startQs != std::string::npos)
737 // This is the socket which will be used for reverse HTTP
738 // negotiate reverse HTTP via upgrade
739 if (uri == "/reverse")
741 status = AIRPLAY_STATUS_SWITCHING_PROTOCOLS;
742 responseHeader = "Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
745 // The rate command is used to play/pause media.
746 // A value argument should be supplied which indicates media should be played or paused.
749 else if (uri == "/rate")
751 const char* found = strstr(queryString.c_str(), "value=");
752 int rate = found ? (int)(atof(found + strlen("value=")) + 0.5f) : 0;
754 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s with rate %i", uri.c_str(), rate);
756 if (needAuth && !checkAuthorization(authorization, method, uri))
758 status = AIRPLAY_STATUS_NEED_AUTH;
762 if (g_application.m_pPlayer->IsPlaying() && !g_application.m_pPlayer->IsPaused())
764 CApplicationMessenger::Get().MediaPause();
769 if (g_application.m_pPlayer->IsPausedPlayback())
771 CApplicationMessenger::Get().MediaPause();
776 // The volume command is used to change playback volume.
777 // A value argument should be supplied which indicates how loud we should get.
778 // 0.000000 => silent
780 else if (uri == "/volume")
782 const char* found = strstr(queryString.c_str(), "volume=");
783 float volume = found ? (float)strtod(found + strlen("volume="), NULL) : 0;
785 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s with volume %f", uri.c_str(), volume);
787 if (needAuth && !checkAuthorization(authorization, method, uri))
789 status = AIRPLAY_STATUS_NEED_AUTH;
791 else if (volume >= 0 && volume <= 1)
793 float oldVolume = g_application.GetVolume();
795 if(oldVolume != volume && CSettings::Get().GetBool("services.airplayvolumecontrol"))
798 g_application.SetVolume(volume);
799 CApplicationMessenger::Get().ShowVolumeBar(oldVolume < volume);
805 // Contains a header like format in the request body which should contain a
806 // Content-Location and optionally a Start-Position
807 else if (uri == "/play")
810 float position = 0.0;
811 m_lastEvent = EVENT_NONE;
813 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
815 if (needAuth && !checkAuthorization(authorization, method, uri))
817 status = AIRPLAY_STATUS_NEED_AUTH;
819 else if (contentType == "application/x-apple-binary-plist")
821 CAirPlayServer::m_isPlaying++;
823 if (m_pLibPlist->Load())
825 m_pLibPlist->EnableDelayedUnload(false);
827 const char* bodyChr = m_httpParser->getBody();
830 m_pLibPlist->plist_from_bin(bodyChr, m_httpParser->getContentLength(), &dict);
832 if (m_pLibPlist->plist_dict_get_size(dict))
834 plist_t tmpNode = m_pLibPlist->plist_dict_get_item(dict, "Start-Position");
837 double tmpDouble = 0;
838 m_pLibPlist->plist_get_real_val(tmpNode, &tmpDouble);
839 position = (float)tmpDouble;
842 tmpNode = m_pLibPlist->plist_dict_get_item(dict, "Content-Location");
846 m_pLibPlist->plist_get_string_val(tmpNode, &tmpStr);
848 #ifdef TARGET_WINDOWS
849 m_pLibPlist->plist_free_string_val(tmpStr);
857 m_pLibPlist->plist_free(dict);
862 CLog::Log(LOGERROR, "Error parsing plist");
864 m_pLibPlist->Unload();
869 CAirPlayServer::m_isPlaying++;
871 std::string contentLocation = "Content-Location: ";
872 size_t start = body.find(contentLocation);
873 if (start == std::string::npos)
874 return AIRPLAY_STATUS_NOT_IMPLEMENTED;
875 start += contentLocation.size();
876 int end = body.find('\n', start);
877 location = body.substr(start, end - start);
879 std::string startPosition = "Start-Position: ";
880 start = body.find(startPosition);
881 if (start != std::string::npos)
883 start += startPosition.size();
884 int end = body.find('\n', start);
885 std::string positionStr = body.substr(start, end - start);
886 position = (float)atof(positionStr.c_str());
890 if (status != AIRPLAY_STATUS_NEED_AUTH)
892 CStdString userAgent(CURL::Encode("AppleCoreMedia/1.0.0.8F455 (AppleTV; U; CPU OS 4_3 like Mac OS X; de_de)"));
893 location += "|User-Agent=" + userAgent;
895 CFileItem fileToPlay(location, false);
896 fileToPlay.SetProperty("StartPercent", position*100.0f);
897 ServerInstance->AnnounceToClients(EVENT_LOADING);
898 // froce to internal dvdplayer cause it is the only
899 // one who will work well with airplay
900 g_application.m_eForcedNextPlayer = EPC_DVDPLAYER;
901 CApplicationMessenger::Get().MediaPlay(fileToPlay);
905 // Used to perform seeking (POST request) and to retrieve current player position (GET request).
906 // GET scrub seems to also set rate 1 - strange but true
907 else if (uri == "/scrub")
909 if (needAuth && !checkAuthorization(authorization, method, uri))
911 status = AIRPLAY_STATUS_NEED_AUTH;
913 else if (method == "GET")
915 CLog::Log(LOGDEBUG, "AIRPLAY: got GET request %s", uri.c_str());
917 if (g_application.m_pPlayer->GetTotalTime())
919 float position = ((float) g_application.m_pPlayer->GetTime()) / 1000;
920 responseBody = StringUtils::Format("duration: %.6f\r\nposition: %.6f\r\n", (float)g_application.m_pPlayer->GetTotalTime() / 1000, position);
924 status = AIRPLAY_STATUS_METHOD_NOT_ALLOWED;
929 const char* found = strstr(queryString.c_str(), "position=");
931 if (found && g_application.m_pPlayer->HasPlayer())
933 int64_t position = (int64_t) (atof(found + strlen("position=")) * 1000.0);
934 g_application.m_pPlayer->SeekTime(position);
935 CLog::Log(LOGDEBUG, "AIRPLAY: got POST request %s with pos %"PRId64, uri.c_str(), position);
940 // Sent when media playback should be stopped
941 else if (uri == "/stop")
943 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
944 if (needAuth && !checkAuthorization(authorization, method, uri))
946 status = AIRPLAY_STATUS_NEED_AUTH;
950 if (IsPlaying()) //only stop player if we started him
952 CApplicationMessenger::Get().MediaStop();
953 CAirPlayServer::m_isPlaying--;
955 else //if we are not playing and get the stop request - we just wanna stop picture streaming
957 CApplicationMessenger::Get().SendAction(ACTION_PREVIOUS_MENU);
962 // RAW JPEG data is contained in the request body
963 else if (uri == "/photo")
965 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
966 if (needAuth && !checkAuthorization(authorization, method, uri))
968 status = AIRPLAY_STATUS_NEED_AUTH;
970 else if (m_httpParser->getContentLength() > 0)
972 XFILE::CFile tmpFile;
973 CStdString tmpFileName = "special://temp/airplay_photo.jpg";
975 if( m_httpParser->getContentLength() > 3 &&
976 m_httpParser->getBody()[1] == 'P' &&
977 m_httpParser->getBody()[2] == 'N' &&
978 m_httpParser->getBody()[3] == 'G')
980 tmpFileName = "special://temp/airplay_photo.png";
983 if (tmpFile.OpenForWrite(tmpFileName, true))
986 writtenBytes = tmpFile.Write(m_httpParser->getBody(), m_httpParser->getContentLength());
989 if (writtenBytes > 0 && (unsigned int)writtenBytes == m_httpParser->getContentLength())
991 CApplicationMessenger::Get().PictureShow(tmpFileName);
995 CLog::Log(LOGERROR,"AirPlayServer: Error writing tmpFile.");
1001 else if (uri == "/playback-info")
1003 float position = 0.0f;
1004 float duration = 0.0f;
1005 float cachePosition = 0.0f;
1006 bool playing = false;
1008 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
1010 if (needAuth && !checkAuthorization(authorization, method, uri))
1012 status = AIRPLAY_STATUS_NEED_AUTH;
1014 else if (g_application.m_pPlayer->HasPlayer())
1016 if (g_application.m_pPlayer->GetTotalTime())
1018 position = ((float) g_application.m_pPlayer->GetTime()) / 1000;
1019 duration = ((float) g_application.m_pPlayer->GetTotalTime()) / 1000;
1020 playing = !g_application.m_pPlayer->IsPaused();
1021 cachePosition = position + (duration * g_application.m_pPlayer->GetCachePercentage() / 100.0f);
1024 responseBody = StringUtils::Format(PLAYBACK_INFO, duration, cachePosition, position, (playing ? 1 : 0), duration);
1025 responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
1027 if (g_application.m_pPlayer->IsCaching())
1029 CAirPlayServer::ServerInstance->AnnounceToClients(EVENT_LOADING);
1034 responseBody = StringUtils::Format(PLAYBACK_INFO_NOT_READY, duration, cachePosition, position, (playing ? 1 : 0), duration);
1035 responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
1039 else if (uri == "/server-info")
1041 CLog::Log(LOGDEBUG, "AIRPLAY: got request %s", uri.c_str());
1042 responseBody = StringUtils::Format(SERVER_INFO, g_application.getNetwork().GetFirstConnectedInterface()->GetMacAddress().c_str());
1043 responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
1046 else if (uri == "/slideshow-features")
1051 else if (uri == "/authorize")
1053 // DRM, ignore for now.
1056 else if (uri == "/setProperty")
1058 status = AIRPLAY_STATUS_NOT_FOUND;
1061 else if (uri == "/getProperty")
1063 status = AIRPLAY_STATUS_NOT_FOUND;
1066 else if (uri == "200") //response OK from the event reverse message
1068 status = AIRPLAY_STATUS_NO_RESPONSE_NEEDED;
1072 CLog::Log(LOGERROR, "AIRPLAY Server: unhandled request [%s]\n", uri.c_str());
1073 status = AIRPLAY_STATUS_NOT_IMPLEMENTED;
1076 if (status == AIRPLAY_STATUS_NEED_AUTH)
1078 ComposeAuthRequestAnswer(responseHeader, responseBody);