MythTV  0.26-pre
mythairplayserver.cpp
Go to the documentation of this file.
00001 // TODO
00002 // locking ?
00003 // race on startup?
00004 // http date format and locale
00005 
00006 #include <QTcpSocket>
00007 #include <QNetworkInterface>
00008 #include <QCoreApplication>
00009 #include <QKeyEvent>
00010 
00011 #include "mthread.h"
00012 #include "mythlogging.h"
00013 #include "mythcorecontext.h"
00014 #include "mythuiactions.h"
00015 #include "mythmainwindow.h"
00016 #include "mythuistatetracker.h"
00017 #include "plist.h"
00018 #include "tv_play.h"
00019 
00020 #include "bonjourregister.h"
00021 #include "mythairplayserver.h"
00022 #include "mythraopdevice.h"
00023 
00024 MythAirplayServer* MythAirplayServer::gMythAirplayServer = NULL;
00025 MThread*           MythAirplayServer::gMythAirplayServerThread = NULL;
00026 QMutex*            MythAirplayServer::gMythAirplayServerMutex = new QMutex(QMutex::Recursive);
00027 
00028 #define LOC QString("AirPlay: ")
00029 
00030 #define HTTP_STATUS_OK                  200
00031 #define HTTP_STATUS_SWITCHING_PROTOCOLS 101
00032 #define HTTP_STATUS_NOT_IMPLEMENTED     501
00033 
00034 #define AIRPLAY_SERVER_VERSION_STR ""
00035 #define SERVER_INFO  QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
00036 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
00037 "<plist version=\"1.0\">\r\n"\
00038 "<dict>\r\n"\
00039 "<key>deviceid</key>\r\n"\
00040 "<string>%1</string>\r\n"\
00041 "<key>features</key>\r\n"\
00042 "<integer>119</integer>\r\n"\
00043 "<key>model</key>\r\n"\
00044 "<string>AppleTV2,1</string>\r\n"\
00045 "<key>protovers</key>\r\n"\
00046 "<string>1.0</string>\r\n"\
00047 "<key>srcvers</key>\r\n"\
00048 "<string>101.28</string>\r\n"\
00049 "</dict>\r\n"\
00050 "</plist>\r\n")
00051 
00052 #define EVENT_INFO QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\r\n"\
00053 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\r\n"\
00054 "<plist version=\"1.0\">\r\n"\
00055 "<dict>\r\n"\
00056 "<key>category</key>\r\n"\
00057 "<string>video</string>\r\n"\
00058 "<key>state</key>\r\n"\
00059 "<string>%1</string>\r\n"\
00060 "</dict>\r\n"\
00061 "</plist>\r\n")
00062 
00063 #define PLAYBACK_INFO  QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
00064 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
00065 "<plist version=\"1.0\">\r\n"\
00066 "<dict>\r\n"\
00067 "<key>duration</key>\r\n"\
00068 "<real>%1</real>\r\n"\
00069 "<key>loadedTimeRanges</key>\r\n"\
00070 "<array>\r\n"\
00071 "\t\t<dict>\r\n"\
00072 "\t\t\t<key>duration</key>\r\n"\
00073 "\t\t\t<real>%2</real>\r\n"\
00074 "\t\t\t<key>start</key>\r\n"\
00075 "\t\t\t<real>0.0</real>\r\n"\
00076 "\t\t</dict>\r\n"\
00077 "</array>\r\n"\
00078 "<key>playbackBufferEmpty</key>\r\n"\
00079 "<true/>\r\n"\
00080 "<key>playbackBufferFull</key>\r\n"\
00081 "<false/>\r\n"\
00082 "<key>playbackLikelyToKeepUp</key>\r\n"\
00083 "<true/>\r\n"\
00084 "<key>position</key>\r\n"\
00085 "<real>%3</real>\r\n"\
00086 "<key>rate</key>\r\n"\
00087 "<real>%4</real>\r\n"\
00088 "<key>readyToPlay</key>\r\n"\
00089 "<true/>\r\n"\
00090 "<key>seekableTimeRanges</key>\r\n"\
00091 "<array>\r\n"\
00092 "\t\t<dict>\r\n"\
00093 "\t\t\t<key>duration</key>\r\n"\
00094 "\t\t\t<real>%1</real>\r\n"\
00095 "\t\t\t<key>start</key>\r\n"\
00096 "\t\t\t<real>0.0</real>\r\n"\
00097 "\t\t</dict>\r\n"\
00098 "</array>\r\n"\
00099 "</dict>\r\n"\
00100 "</plist>\r\n")
00101 
00102 #define NOT_READY  QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
00103 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
00104 "<plist version=\"1.0\">\r\n"\
00105 "<dict>\r\n"\
00106 "<key>readyToPlay</key>\r\n"\
00107 "<false/>\r\n"\
00108 "</dict>\r\n"\
00109 "</plist>\r\n")
00110 
00111 class APHTTPRequest
00112 {
00113   public:
00114     APHTTPRequest(QByteArray &data) : m_readPos(0), m_data(data)
00115     {
00116         Process();
00117         Check();
00118     }
00119    ~APHTTPRequest() { }
00120 
00121     QByteArray&       GetMethod(void)  { return m_method;   }
00122     QByteArray&       GetURI(void)     { return m_uri;      }
00123     QByteArray&       GetBody(void)    { return m_body;     }
00124     QMap<QByteArray,QByteArray>& GetHeaders(void)
00125                                        { return m_headers;  }
00126 
00127     QByteArray GetQueryValue(QByteArray key)
00128     {
00129         for (int i = 0; i < m_queries.size(); i++)
00130             if (m_queries[i].first == key)
00131                 return m_queries[i].second;
00132         return "";
00133     }
00134 
00135     QMap<QByteArray,QByteArray> GetHeadersFromBody(void)
00136     {
00137         QMap<QByteArray,QByteArray> result;
00138         QList<QByteArray> lines = m_body.split('\n');;
00139         foreach (QByteArray line, lines)
00140         {
00141             int index = line.indexOf(":");
00142             if (index > 0)
00143             {
00144                 result.insert(line.left(index).trimmed(),
00145                               line.mid(index + 1).trimmed());
00146             }
00147         }
00148         return result;
00149     }
00150 
00151   private:
00152     QByteArray GetLine(void)
00153     {
00154         int next = m_data.indexOf("\r\n", m_readPos);
00155         if (next < 0) return QByteArray();
00156         QByteArray line = m_data.mid(m_readPos, next - m_readPos);
00157         m_readPos = next + 2;
00158         return line;
00159     }
00160 
00161     void Process(void)
00162     {
00163         if (!m_data.size())
00164             return;
00165 
00166         // request line
00167         QByteArray line = GetLine();
00168         if (line.isEmpty())
00169             return;
00170         QList<QByteArray> vals = line.split(' ');
00171         if (vals.size() < 3)
00172             return;
00173         m_method = vals[0].trimmed();
00174         QUrl url = QUrl::fromEncoded(vals[1].trimmed());
00175         m_uri = url.encodedPath();
00176         m_queries = url.encodedQueryItems();
00177         if (m_method.isEmpty() || m_uri.isEmpty())
00178             return;
00179 
00180         // headers
00181         while (!(line = GetLine()).isEmpty())
00182         {
00183             int index = line.indexOf(":");
00184             if (index > 0)
00185             {
00186                 m_headers.insert(line.left(index).trimmed(),
00187                                  line.mid(index + 1).trimmed());
00188             }
00189         }
00190 
00191         // body?
00192         if (m_headers.contains("Content-Length"))
00193         {
00194             int remaining = m_data.size() - m_readPos;
00195             int size = m_headers["Content-Length"].toInt();
00196             if (size > 0 && remaining > 0 && size <= remaining)
00197             {
00198                 m_body = m_data.mid(m_readPos, size);
00199                 m_readPos += size;
00200             }
00201         }
00202     }
00203 
00204     void Check(void)
00205     {
00206         LOG(VB_GENERAL, LOG_DEBUG, LOC +
00207             QString("HTTP Request:\n%1").arg(m_data.data()));
00208         if (m_readPos == m_data.size())
00209             return;
00210         LOG(VB_GENERAL, LOG_WARNING, LOC +
00211             "AP HTTPRequest: Didn't read entire buffer.");
00212     }
00213 
00214     int  m_readPos;
00215     QByteArray m_data;
00216     QByteArray m_method;
00217     QByteArray m_uri;
00218     QList<QPair<QByteArray, QByteArray> > m_queries;
00219     QMap<QByteArray,QByteArray> m_headers;
00220     QByteArray m_body;
00221 };
00222 
00223 bool MythAirplayServer::Create(void)
00224 {
00225     QMutexLocker locker(gMythAirplayServerMutex);
00226 
00227     // create the server thread
00228     if (!gMythAirplayServerThread)
00229         gMythAirplayServerThread = new MThread("AirplayServer");
00230     if (!gMythAirplayServerThread)
00231     {
00232         LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create airplay thread.");
00233         return false;
00234     }
00235 
00236     // create the server object
00237     if (!gMythAirplayServer)
00238         gMythAirplayServer = new MythAirplayServer();
00239     if (!gMythAirplayServer)
00240     {
00241         LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create airplay object.");
00242         return false;
00243     }
00244 
00245     // start the thread
00246     if (!gMythAirplayServerThread->isRunning())
00247     {
00248         gMythAirplayServer->moveToThread(gMythAirplayServerThread->qthread());
00249         QObject::connect(
00250             gMythAirplayServerThread->qthread(), SIGNAL(started()),
00251             gMythAirplayServer,                  SLOT(Start()));
00252         gMythAirplayServerThread->start(QThread::LowestPriority);
00253     }
00254 
00255     LOG(VB_GENERAL, LOG_INFO, LOC + "Created airplay objects.");
00256     return true;
00257 }
00258 
00259 void MythAirplayServer::Cleanup(void)
00260 {
00261     LOG(VB_GENERAL, LOG_INFO, LOC + "Cleaning up.");
00262 
00263     if (gMythAirplayServer)
00264         gMythAirplayServer->Teardown();
00265 
00266     QMutexLocker locker(gMythAirplayServerMutex);
00267     if (gMythAirplayServerThread)
00268     {
00269         gMythAirplayServerThread->exit();
00270         gMythAirplayServerThread->wait();
00271     }
00272     delete gMythAirplayServerThread;
00273     gMythAirplayServerThread = NULL;
00274 
00275     delete gMythAirplayServer;
00276     gMythAirplayServer = NULL;
00277 }
00278 
00279 
00280 MythAirplayServer::MythAirplayServer()
00281   : ServerPool(), m_name(QString("MythTV")), m_bonjour(NULL), m_valid(false),
00282     m_lock(new QMutex(QMutex::Recursive)), m_setupPort(5100)
00283 {
00284 }
00285 
00286 MythAirplayServer::~MythAirplayServer()
00287 {
00288     Teardown();
00289 
00290     delete m_lock;
00291     m_lock = NULL;
00292 }
00293 
00294 void MythAirplayServer::Teardown(void)
00295 {
00296     QMutexLocker locker(m_lock);
00297 
00298     // invalidate
00299     m_valid = false;
00300 
00301     // disconnect from mDNS
00302     delete m_bonjour;
00303     m_bonjour = NULL;
00304 
00305     // disconnect connections
00306     foreach (QTcpSocket* connection, m_sockets)
00307     {
00308         disconnect(connection, 0, 0, 0);
00309         delete connection;
00310     }
00311     m_sockets.clear();
00312 }
00313 
00314 void MythAirplayServer::Start(void)
00315 {
00316     QMutexLocker locker(m_lock);
00317 
00318     // already started?
00319     if (m_valid)
00320         return;
00321 
00322     // join the dots
00323     connect(this, SIGNAL(newConnection(QTcpSocket*)),
00324             this, SLOT(newConnection(QTcpSocket*)));
00325 
00326     // start listening for connections
00327     // try a few ports in case the default is in use
00328     int baseport = m_setupPort;
00329     m_setupPort = tryListeningPort(m_setupPort, AIRPLAY_PORT_RANGE);
00330     if (m_setupPort < 0)
00331     {
00332         LOG(VB_GENERAL, LOG_ERR, LOC +
00333                 "Failed to find a port for incoming connections.");
00334     }
00335     else
00336     {
00337         // announce service
00338         m_bonjour = new BonjourRegister(this);
00339         if (!m_bonjour)
00340         {
00341             LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create Bonjour object.");
00342             return;
00343         }
00344 
00345         // give each frontend a unique name
00346         int multiple = m_setupPort - baseport;
00347         if (multiple > 0)
00348             m_name += QString::number(multiple);
00349 
00350         QByteArray name = m_name.toUtf8();
00351         name.append(" on ");
00352         name.append(gCoreContext->GetHostName());
00353         QByteArray type = "_airplay._tcp";
00354         QByteArray txt;
00355         txt.append(26); txt.append("deviceid="); txt.append(GetMacAddress());
00356         txt.append(14); txt.append("features=0x219");
00357         txt.append(16); txt.append("model=AppleTV2,1");
00358         txt.append(14); txt.append("srcvers=101.28");
00359 
00360         if (!m_bonjour->Register(m_setupPort, type, name, txt))
00361         {
00362             LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service.");
00363             return;
00364         }
00365     }
00366     m_valid = true;
00367     return;
00368 }
00369 
00370 void MythAirplayServer::newConnection(QTcpSocket *client)
00371 {
00372     QMutexLocker locker(m_lock);
00373     LOG(VB_GENERAL, LOG_INFO, LOC + QString("New connection from %1:%2")
00374         .arg(client->peerAddress().toString()).arg(client->peerPort()));
00375 
00376     m_sockets.append(client);
00377     connect(client, SIGNAL(disconnected()), this, SLOT(deleteConnection()));
00378     connect(client, SIGNAL(readyRead()), this, SLOT(read()));
00379 }
00380 
00381 void MythAirplayServer::deleteConnection(void)
00382 {
00383     QMutexLocker locker(m_lock);
00384     QTcpSocket *socket = (QTcpSocket *)sender();
00385     if (!socket)
00386         return;
00387 
00388     if (!m_sockets.contains(socket))
00389         return;
00390 
00391     LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing connection %1:%2")
00392         .arg(socket->peerAddress().toString()).arg(socket->peerPort()));
00393     m_sockets.removeOne(socket);
00394 
00395     QByteArray remove;
00396     QMutableHashIterator<QByteArray,AirplayConnection> it(m_connections);
00397     while (it.hasNext())
00398     {
00399         it.next();
00400         if (it.value().reverseSocket == socket)
00401             it.value().reverseSocket = NULL;
00402         if (it.value().controlSocket == socket)
00403             it.value().controlSocket = NULL;
00404         if (!it.value().reverseSocket &&
00405             !it.value().controlSocket &&
00406             it.value().stopped)
00407         {
00408             remove = it.key();
00409         }
00410     }
00411 
00412     if (!remove.isEmpty())
00413     {
00414         LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing session '%1'")
00415             .arg(remove.data()));
00416         m_connections.remove(remove);
00417     }
00418 
00419     socket->deleteLater();
00420 }
00421 
00422 void MythAirplayServer::read(void)
00423 {
00424     QMutexLocker locker(m_lock);
00425     QTcpSocket *socket = (QTcpSocket *)sender();
00426     if (!socket)
00427         return;
00428 
00429     LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Read for %1:%2")
00430         .arg(socket->peerAddress().toString()).arg(socket->peerPort()));
00431 
00432     QByteArray buf = socket->readAll();
00433     APHTTPRequest request(buf);
00434     HandleResponse(&request, socket);
00435 }
00436 
00437 QByteArray MythAirplayServer::StatusToString(int status)
00438 {
00439     switch (status)
00440     {
00441         case HTTP_STATUS_OK: return "OK";
00442         case HTTP_STATUS_SWITCHING_PROTOCOLS: return "Switching Protocols";
00443         case HTTP_STATUS_NOT_IMPLEMENTED: return "Not Implemented";
00444     }
00445     return "";
00446 }
00447 
00448 void MythAirplayServer::HandleResponse(APHTTPRequest *req,
00449                                        QTcpSocket *socket)
00450 {
00451     if (!socket)
00452         return;
00453     QHostAddress addr = socket->peerAddress();
00454     QByteArray session;
00455     QByteArray header;
00456     QString    body;
00457     int status = HTTP_STATUS_OK;
00458     QByteArray content_type;
00459 
00460     if (req->GetURI() != "/playback-info")
00461     {
00462         LOG(VB_GENERAL, LOG_INFO, LOC +
00463             QString("Method: %1 URI: %2")
00464             .arg(req->GetMethod().data()).arg(req->GetURI().data()));
00465     }
00466     else
00467     {
00468         LOG(VB_GENERAL, LOG_DEBUG, LOC +
00469             QString("Method: %1 URI: %2")
00470             .arg(req->GetMethod().data()).arg(req->GetURI().data()));
00471     }
00472 
00473     if (req->GetURI() == "200" || req->GetMethod().startsWith("HTTP"))
00474         return;
00475 
00476     if (!req->GetHeaders().contains("X-Apple-Session-ID"))
00477     {
00478         LOG(VB_GENERAL, LOG_DEBUG, LOC +
00479             QString("No session ID in http request. "
00480                     "Connection from iTunes? Using IP %1").arg(addr.toString()));
00481     }
00482     else
00483     {
00484         session = req->GetHeaders()["X-Apple-Session-ID"];
00485     }
00486 
00487     if (session.size() == 0)
00488     {
00489         // No session ID, use IP address instead
00490         session = addr.toString().toAscii();
00491     }
00492     if (!m_connections.contains(session))
00493     {
00494         AirplayConnection apcon;
00495         m_connections.insert(session, apcon);
00496     }
00497 
00498     if (req->GetURI() == "/reverse")
00499     {
00500         QTcpSocket *s = m_connections[session].reverseSocket;
00501         if (s != socket && s != NULL)
00502         {
00503             LOG(VB_GENERAL, LOG_ERR, LOC +
00504                 "Already have a different reverse socket for this connection.");
00505             return;
00506         }
00507         m_connections[session].reverseSocket = socket;
00508         status = HTTP_STATUS_SWITCHING_PROTOCOLS;
00509         header = "Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
00510         SendResponse(socket, status, header, content_type, body);
00511         return;
00512     }
00513 
00514     QTcpSocket *s = m_connections[session].controlSocket;
00515     if (s != socket && s != NULL)
00516     {
00517         LOG(VB_GENERAL, LOG_ERR, LOC +
00518             "Already have a different control socket for this connection.");
00519         return;
00520     }
00521     m_connections[session].controlSocket = socket;
00522 
00523     double position    = 0.0f;
00524     double duration    = 0.0f;
00525     float  playerspeed = 0.0f;
00526     bool   playing     = false;
00527     GetPlayerStatus(playing, playerspeed, position, duration);
00528     // set initial position if it was set at start of playback.
00529     if (duration > 0.01f && playing)
00530     {
00531         if (m_connections[session].initial_position > 0.0f)
00532         {
00533             position = duration * m_connections[session].initial_position;
00534             MythEvent* me = new MythEvent(ACTION_SEEKABSOLUTE,
00535                                           QStringList(QString::number((uint64_t)position)));
00536             qApp->postEvent(GetMythMainWindow(), me);
00537             m_connections[session].position = position;
00538             m_connections[session].initial_position = -1.0f;
00539         }
00540         else if (position < .01f)
00541         {
00542             // Assume playback hasn't started yet, get saved position
00543             position = m_connections[session].position;
00544         }
00545     }
00546     if (!playing && m_connections[session].was_playing)
00547     {
00548         // playback got interrupted, notify client to stop
00549         if (SendReverseEvent(session, AP_EVENT_STOPPED))
00550         {
00551             m_connections[session].was_playing = false;
00552         }
00553     }
00554     else
00555     {
00556         m_connections[session].was_playing = playing;
00557     }
00558 
00559     if (req->GetURI() == "/server-info")
00560     {
00561         content_type = "text/x-apple-plist+xml\r\n";
00562         body = SERVER_INFO;
00563         body.replace("%1", GetMacAddress());
00564         LOG(VB_GENERAL, LOG_INFO, body);
00565     }
00566     else if (req->GetURI() == "/scrub")
00567     {
00568         double pos = req->GetQueryValue("position").toDouble();
00569         if (req->GetMethod() == "POST")
00570         {
00571             // this may be received before playback starts...
00572             uint64_t intpos = (uint64_t)pos;
00573             m_connections[session].position = pos;
00574             LOG(VB_GENERAL, LOG_INFO, LOC +
00575                 QString("Scrub: (post) seek to %1").arg(intpos));
00576             MythEvent* me = new MythEvent(ACTION_SEEKABSOLUTE,
00577                                           QStringList(QString::number(intpos)));
00578             qApp->postEvent(GetMythMainWindow(), me);
00579         }
00580         else if (req->GetMethod() == "GET")
00581         {
00582             content_type = "text/parameters\r\n";
00583             body = QString("duration: %1\r\nposition: %2\r\n")
00584                 .arg((double)duration, 0, 'f', 6, '0')
00585                 .arg((double)position, 0, 'f', 6, '0');
00586 
00587             LOG(VB_GENERAL, LOG_INFO, LOC +
00588                 QString("Scrub: (get) returned %1 of %2")
00589                 .arg(position).arg(duration));
00590 
00591             /*
00592             if (playing && playerspeed < 1.0f)
00593             {
00594                 SendReverseEvent(session, AP_EVENT_PLAYING);
00595                 QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
00596                                               Qt::NoModifier, ACTION_PLAY);
00597                 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
00598             }
00599             */
00600         }
00601     }
00602     else if (req->GetURI() == "/stop")
00603     {
00604         m_connections[session].stopped = true;
00605         QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
00606                                       Qt::NoModifier, ACTION_STOP);
00607         qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
00608     }
00609     else if (req->GetURI() == "/photo" ||
00610              req->GetURI() == "/slideshow-features")
00611     {
00612         LOG(VB_GENERAL, LOG_INFO, LOC +
00613             "Slideshow functionality not implemented.");
00614     }
00615     else if (req->GetURI() == "/authorize")
00616     {
00617         LOG(VB_GENERAL, LOG_INFO, LOC + "Ignoring authorize request.");
00618     }
00619     else if (req->GetURI() == "/rate")
00620     {
00621         float rate = req->GetQueryValue("value").toFloat();
00622         m_connections[session].speed = rate;
00623 
00624         if (rate < 1.0f)
00625         {
00626             SendReverseEvent(session, AP_EVENT_PAUSED);
00627             if (playerspeed > 0.0f)
00628             {
00629                 QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
00630                                               Qt::NoModifier, ACTION_PAUSE);
00631                 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
00632             }
00633         }
00634         else
00635         {
00636             SendReverseEvent(session, AP_EVENT_PLAYING);
00637             if (playerspeed < 1.0f)
00638             {
00639                 QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
00640                                               Qt::NoModifier, ACTION_PLAY);
00641                 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
00642             }
00643         }
00644     }
00645     else if (req->GetURI() == "/play")
00646     {
00647         QByteArray file;
00648         double start_pos = 0.0f;
00649         if (req->GetHeaders().contains("Content-Type") &&
00650             req->GetHeaders()["Content-Type"] == "application/x-apple-binary-plist")
00651         {
00652             PList plist(req->GetBody());
00653             LOG(VB_GENERAL, LOG_DEBUG, LOC + plist.ToString());
00654 
00655             QVariant start   = plist.GetValue("Start-Position");
00656             QVariant content = plist.GetValue("Content-Location");
00657             if (start.isValid() && start.canConvert<double>())
00658                 start_pos = start.toDouble();
00659             if (content.isValid() && content.canConvert<QByteArray>())
00660                 file = content.toByteArray();
00661         }
00662         else
00663         {
00664             QMap<QByteArray,QByteArray> headers = req->GetHeadersFromBody();
00665             file  = headers["Content-Location"];
00666             start_pos = headers["Start-Position"].toDouble();
00667         }
00668 
00669         if (!TV::IsTVRunning())
00670         {
00671             if (!file.isEmpty())
00672             {
00673                 m_connections[session].url = QUrl(file);
00674                 m_connections[session].position = 0.0f;
00675                 m_connections[session].initial_position = start_pos;
00676 
00677                 MythEvent* me = new MythEvent(ACTION_HANDLEMEDIA,
00678                                               QStringList(file));
00679                 qApp->postEvent(GetMythMainWindow(), me);
00680             }
00681         }
00682         else
00683         {
00684             LOG(VB_GENERAL, LOG_WARNING, LOC +
00685                 "Ignoring playback - something else is playing.");
00686         }
00687 
00688         SendReverseEvent(session, AP_EVENT_PLAYING);
00689         LOG(VB_GENERAL, LOG_INFO, LOC + QString("File: '%1' start_pos '%2'")
00690             .arg(file.data()).arg(start_pos));
00691     }
00692     else if (req->GetURI() == "/playback-info")
00693     {
00694         content_type = "text/x-apple-plist+xml\r\n";
00695 
00696         if (!playing)
00697         {
00698             body = NOT_READY;
00699             SendReverseEvent(session, AP_EVENT_LOADING);
00700         }
00701         else
00702         {
00703             body = PLAYBACK_INFO;
00704             body.replace("%1", QString("%1").arg((double)duration, 0, 'f', 6, '0'));
00705             body.replace("%2", QString("%1").arg((double)duration, 0, 'f', 6, '0')); // cached
00706             body.replace("%3", QString("%1").arg((double)position, 0, 'f', 6, '0'));
00707             body.replace("%4", playerspeed > 0.0f ? "1.0" : "0.0");
00708             LOG(VB_GENERAL, LOG_DEBUG, body);
00709             SendReverseEvent(session, playerspeed > 0.0f ? AP_EVENT_PLAYING :
00710                                                            AP_EVENT_PAUSED);
00711         }
00712     }
00713     SendResponse(socket, status, header, content_type, body);
00714 }
00715 
00716 void MythAirplayServer::SendResponse(QTcpSocket *socket,
00717                                      int status, QByteArray header,
00718                                      QByteArray content_type, QString body)
00719 {
00720     QTextStream response(socket);
00721     response.setCodec("UTF-8");
00722     QByteArray reply;
00723     reply.append("HTTP/1.1 ");
00724     reply.append(QString::number(status));
00725     reply.append(" ");
00726     reply.append(StatusToString(status));
00727     reply.append("\r\n");
00728     reply.append("DATE: ");
00729     reply.append(QDateTime::currentDateTime().toString("ddd, d MMM yyyy hh:mm:ss"));
00730     reply.append(" GMT\r\n");
00731     if (header.size())
00732         reply.append(header);
00733 
00734     if (body.size())
00735     {
00736         reply.append("Content-Type: ");
00737         reply.append(content_type);
00738         reply.append("Content-Length: ");
00739         reply.append(QString::number(body.size()));
00740         reply.append("\r\n");
00741     }
00742 
00743     reply.append("\r\n");
00744 
00745     if (body.size())
00746         reply.append(body);
00747 
00748     response << reply;
00749     response.flush();
00750 
00751     LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Send: %1 \n\n%2\n")
00752         .arg(socket->flush()).arg(reply.data()));
00753 }
00754 
00755 bool MythAirplayServer::SendReverseEvent(QByteArray &session,
00756                                          AirplayEvent event)
00757 {
00758     if (!m_connections.contains(session))
00759         return false;
00760     if (m_connections[session].lastEvent == event)
00761         return false;
00762     if (!m_connections[session].reverseSocket)
00763         return false;
00764 
00765     QString body;
00766     if (AP_EVENT_PLAYING == event ||
00767         AP_EVENT_LOADING == event ||
00768         AP_EVENT_PAUSED  == event ||
00769         AP_EVENT_STOPPED == event)
00770     {
00771         body = EVENT_INFO;
00772         body.replace("%1", eventToString(event));
00773     }
00774 
00775     m_connections[session].lastEvent = event;
00776     QTextStream response(m_connections[session].reverseSocket);
00777     response.setCodec("UTF-8");
00778     QByteArray reply;
00779     reply.append("POST /event HTTP/1.1\r\n");
00780     reply.append("Content-Type: text/x-apple-plist+xml\r\n");
00781     reply.append("Content-Length: ");
00782     reply.append(QString::number(body.size()));
00783     reply.append("\r\n");
00784     reply.append("x-apple-session-id: ");
00785     reply.append(session);
00786     reply.append("\r\n\r\n");
00787     if (body.size())
00788         reply.append(body);
00789 
00790     response << reply;
00791     response.flush();
00792 
00793     LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Send reverse: %1 \n\n%2\n")
00794          .arg(m_connections[session].reverseSocket->flush())
00795          .arg(reply.data()));
00796     return true;
00797 }
00798 
00799 QString MythAirplayServer::eventToString(AirplayEvent event)
00800 {
00801     switch (event)
00802     {
00803         case AP_EVENT_PLAYING: return "playing";
00804         case AP_EVENT_PAUSED:  return "paused";
00805         case AP_EVENT_LOADING: return "loading";
00806         case AP_EVENT_STOPPED: return "stopped";
00807     }
00808     return "";
00809 }
00810 
00811 void MythAirplayServer::GetPlayerStatus(bool &playing, float &speed,
00812                                         double &position, double &duration)
00813 {
00814     QVariantMap state;
00815     MythUIStateTracker::GetFreshState(state);
00816     if (state.contains("state"))
00817         playing = state["state"].toString() != "idle";
00818     if (state.contains("playspeed"))
00819         speed = state["playspeed"].toFloat();
00820     if (state.contains("secondsplayed"))
00821         position = state["secondsplayed"].toDouble();
00822     if (state.contains("totalseconds"))
00823         duration = state["totalseconds"].toDouble();
00824 }
00825 
00826 QString MythAirplayServer::GetMacAddress()
00827 {
00828     QString id = MythRAOPDevice::HardwareId();
00829 
00830     QString res;
00831     for (int i = 1; i <= id.size(); i++)
00832     {
00833         res.append(id[i-1]);
00834         if (i % 2 == 0 && i != id.size())
00835         {
00836             res.append(':');
00837         }
00838     }
00839     return res;
00840 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends