1 /*****************************************************************************
2  * chromecast_communication.cpp: Handle chromecast protocol messages
3  *****************************************************************************
4  * Copyright © 2014-2017 VideoLAN
5  *
6  * Authors: Adrien Maglo <magsoft@videolan.org>
7  *          Jean-Baptiste Kempf <jb@videolan.org>
8  *          Steve Lhomme <robux4@videolabs.io>
9  *          Hugo Beauzée-Luyssen <hugo@beauzee.fr>
10  *
11  * This program is free software; you can redistribute it and/or modify it
12  * under the terms of the GNU Lesser General Public License as published by
13  * the Free Software Foundation; either version 2.1 of the License, or
14  * (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19  * GNU Lesser General Public License for more details.
20  *
21  * You should have received a copy of the GNU Lesser General Public License
22  * along with this program; if not, write to the Free Software Foundation,
23  * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
24  *****************************************************************************/
25 
26 #ifdef HAVE_CONFIG_H
27 # include "config.h"
28 #endif
29 
30 #include "chromecast.h"
31 #ifdef HAVE_POLL
32 # include <poll.h>
33 #endif
34 
35 #include <iomanip>
36 
ChromecastCommunication(vlc_object_t * p_module,std::string serverPath,unsigned int serverPort,const char * targetIP,unsigned int devicePort)37 ChromecastCommunication::ChromecastCommunication( vlc_object_t* p_module,
38     std::string serverPath, unsigned int serverPort, const char* targetIP, unsigned int devicePort )
39     : m_module( p_module )
40     , m_creds( NULL )
41     , m_tls( NULL )
42     , m_receiver_requestId( 1 )
43     , m_requestId( 1 )
44     , m_serverPath( serverPath )
45     , m_serverPort( serverPort )
46 {
47     if (devicePort == 0)
48         devicePort = CHROMECAST_CONTROL_PORT;
49 
50     m_creds = vlc_tls_ClientCreate( m_module->obj.parent );
51     if (m_creds == NULL)
52         throw std::runtime_error( "Failed to create TLS client" );
53 
54     /* Ignore ca checks */
55     m_creds->obj.flags |= OBJECT_FLAGS_INSECURE;
56     m_tls = vlc_tls_SocketOpenTLS( m_creds, targetIP, devicePort, "tcps",
57                                    NULL, NULL );
58     if (m_tls == NULL)
59     {
60         vlc_tls_Delete(m_creds);
61         throw std::runtime_error( "Failed to create client session" );
62     }
63 
64     char psz_localIP[NI_MAXNUMERICHOST];
65     if (net_GetSockAddress( vlc_tls_GetFD(m_tls), psz_localIP, NULL ))
66         throw std::runtime_error( "Cannot get local IP address" );
67 
68     m_serverIp = psz_localIP;
69 }
70 
~ChromecastCommunication()71 ChromecastCommunication::~ChromecastCommunication()
72 {
73     disconnect();
74 }
75 
disconnect()76 void ChromecastCommunication::disconnect()
77 {
78     if ( m_tls != NULL )
79     {
80         vlc_tls_Close(m_tls);
81         vlc_tls_Delete(m_creds);
82         m_tls = NULL;
83     }
84 }
85 
86 /**
87  * @brief Build a CastMessage to send to the Chromecast
88  * @param namespace_ the message namespace
89  * @param payloadType the payload type (CastMessage_PayloadType_STRING or
90  * CastMessage_PayloadType_BINARY
91  * @param payload the payload
92  * @param destinationId the destination idenifier
93  * @return the generated CastMessage
94  */
buildMessage(const std::string & namespace_,const std::string & payload,const std::string & destinationId,castchannel::CastMessage_PayloadType payloadType)95 int ChromecastCommunication::buildMessage(const std::string & namespace_,
96                               const std::string & payload,
97                               const std::string & destinationId,
98                               castchannel::CastMessage_PayloadType payloadType)
99 {
100     castchannel::CastMessage msg;
101 
102     msg.set_protocol_version(castchannel::CastMessage_ProtocolVersion_CASTV2_1_0);
103     msg.set_namespace_(namespace_);
104     msg.set_payload_type(payloadType);
105     msg.set_source_id("sender-vlc");
106     msg.set_destination_id(destinationId);
107     if (payloadType == castchannel::CastMessage_PayloadType_STRING)
108         msg.set_payload_utf8(payload);
109     else // CastMessage_PayloadType_BINARY
110         msg.set_payload_binary(payload);
111 
112     return sendMessage(msg);
113 }
114 
115 /**
116  * @brief Receive a data packet from the Chromecast
117  * @param p_data the buffer in which to store the data
118  * @param i_size the size of the buffer
119  * @param i_timeout maximum time to wait for a packet, in millisecond
120  * @param pb_timeout Output parameter that will contain true if no packet was received due to a timeout
121  * @return the number of bytes received of -1 on error
122  */
receive(uint8_t * p_data,size_t i_size,int i_timeout,bool * pb_timeout)123 ssize_t ChromecastCommunication::receive( uint8_t *p_data, size_t i_size, int i_timeout, bool *pb_timeout )
124 {
125     ssize_t i_received = 0;
126     struct pollfd ufd[1];
127     ufd[0].fd = vlc_tls_GetFD( m_tls );
128     ufd[0].events = POLLIN;
129 
130     struct iovec iov;
131     iov.iov_base = p_data;
132     iov.iov_len = i_size;
133 
134     /* The Chromecast normally sends a PING command every 5 seconds or so.
135      * If we do not receive one after 6 seconds, we send a PING.
136      * If after this PING, we do not receive a PONG, then we consider the
137      * connection as dead. */
138     do
139     {
140         ssize_t i_ret = m_tls->readv( m_tls, &iov, 1 );
141         if ( i_ret < 0 )
142         {
143 #ifdef _WIN32
144             if ( WSAGetLastError() != WSAEWOULDBLOCK )
145 #else
146             if ( errno != EAGAIN )
147 #endif
148             {
149                 return -1;
150             }
151             ssize_t val = vlc_poll_i11e(ufd, 1, i_timeout);
152             if ( val < 0 )
153                 return -1;
154             else if ( val == 0 )
155             {
156                 *pb_timeout = true;
157                 return i_received;
158             }
159             assert( ufd[0].revents & POLLIN );
160             continue;
161         }
162         else if ( i_ret == 0 )
163             return -1;
164         assert( i_size >= (size_t)i_ret );
165         i_size -= i_ret;
166         i_received += i_ret;
167         iov.iov_base = (uint8_t*)iov.iov_base + i_ret;
168         iov.iov_len = i_size;
169     } while ( i_size > 0 );
170     return i_received;
171 }
172 
173 
174 /*****************************************************************************
175  * Message preparation
176  *****************************************************************************/
getNextReceiverRequestId()177 unsigned ChromecastCommunication::getNextReceiverRequestId()
178 {
179     unsigned id = m_receiver_requestId++;
180     return likely(id != 0) ? id : m_receiver_requestId++;
181 }
182 
getNextRequestId()183 unsigned ChromecastCommunication::getNextRequestId()
184 {
185     unsigned id = m_requestId++;
186     return likely(id != 0) ? id : m_requestId++;
187 }
188 
msgAuth()189 unsigned ChromecastCommunication::msgAuth()
190 {
191     castchannel::DeviceAuthMessage authMessage;
192     authMessage.mutable_challenge();
193 
194     return buildMessage(NAMESPACE_DEVICEAUTH, authMessage.SerializeAsString(),
195                         DEFAULT_CHOMECAST_RECEIVER, castchannel::CastMessage_PayloadType_BINARY)
196            == VLC_SUCCESS ? 1 : kInvalidId;
197 }
198 
199 
msgPing()200 unsigned ChromecastCommunication::msgPing()
201 {
202     std::string s("{\"type\":\"PING\"}");
203     return buildMessage( NAMESPACE_HEARTBEAT, s, DEFAULT_CHOMECAST_RECEIVER )
204            == VLC_SUCCESS ? 1 : kInvalidId;
205 }
206 
207 
msgPong()208 unsigned ChromecastCommunication::msgPong()
209 {
210     std::string s("{\"type\":\"PONG\"}");
211     return buildMessage( NAMESPACE_HEARTBEAT, s, DEFAULT_CHOMECAST_RECEIVER )
212            == VLC_SUCCESS ? 1 : kInvalidId;
213 }
214 
msgConnect(const std::string & destinationId)215 unsigned ChromecastCommunication::msgConnect( const std::string& destinationId )
216 {
217     std::string s("{\"type\":\"CONNECT\"}");
218     return buildMessage( NAMESPACE_CONNECTION, s, destinationId )
219            == VLC_SUCCESS ? 1 : kInvalidId;
220 }
221 
msgReceiverClose(const std::string & destinationId)222 unsigned ChromecastCommunication::msgReceiverClose( const std::string& destinationId )
223 {
224     std::string s("{\"type\":\"CLOSE\"}");
225     return buildMessage( NAMESPACE_CONNECTION, s, destinationId )
226            == VLC_SUCCESS ? 1 : kInvalidId;
227 }
228 
msgReceiverGetStatus()229 unsigned ChromecastCommunication::msgReceiverGetStatus()
230 {
231     unsigned id = getNextReceiverRequestId();
232     std::stringstream ss;
233     ss << "{\"type\":\"GET_STATUS\","
234        <<  "\"requestId\":" << id << "}";
235 
236     return buildMessage( NAMESPACE_RECEIVER, ss.str(), DEFAULT_CHOMECAST_RECEIVER )
237            == VLC_SUCCESS ? id : kInvalidId;
238 }
239 
msgReceiverLaunchApp()240 unsigned ChromecastCommunication::msgReceiverLaunchApp()
241 {
242     unsigned id = getNextReceiverRequestId();
243     std::stringstream ss;
244     ss << "{\"type\":\"LAUNCH\","
245        <<  "\"appId\":\"" << APP_ID << "\","
246        <<  "\"requestId\":" << id << "}";
247 
248     return buildMessage( NAMESPACE_RECEIVER, ss.str(), DEFAULT_CHOMECAST_RECEIVER )
249            == VLC_SUCCESS ? id : kInvalidId;
250 }
251 
msgPlayerGetStatus(const std::string & destinationId)252 unsigned ChromecastCommunication::msgPlayerGetStatus( const std::string& destinationId )
253 {
254     unsigned id = getNextRequestId();
255     std::stringstream ss;
256     ss << "{\"type\":\"GET_STATUS\","
257        <<  "\"requestId\":" << id
258        << "}";
259 
260     return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
261 }
262 
escape_json(const std::string & s)263 static std::string escape_json(const std::string &s)
264 {
265     /* Control characters ('\x00' to '\x1f'), '"' and '\"  must be escaped */
266     std::ostringstream o;
267     for (std::string::const_iterator c = s.begin(); c != s.end(); c++)
268     {
269         if (*c == '"' || *c == '\\' || ('\x00' <= *c && *c <= '\x1f'))
270             o << "\\u"
271               << std::hex << std::setw(4) << std::setfill('0') << (int)*c;
272         else
273             o << *c;
274     }
275     return o.str();
276 }
277 
meta_get_escaped(const vlc_meta_t * p_meta,vlc_meta_type_t type)278 static std::string meta_get_escaped(const vlc_meta_t *p_meta, vlc_meta_type_t type)
279 {
280     const char *psz = vlc_meta_Get(p_meta, type);
281     if (!psz)
282         return std::string();
283     return escape_json(std::string(psz));
284 }
285 
GetMedia(const std::string & mime,const vlc_meta_t * p_meta)286 std::string ChromecastCommunication::GetMedia( const std::string& mime,
287                                                const vlc_meta_t *p_meta )
288 {
289     std::stringstream ss;
290 
291     bool b_music = strncmp(mime.c_str(), "audio", strlen("audio")) == 0;
292 
293     std::string title;
294     std::string artwork;
295     std::string artist;
296     std::string album;
297     std::string albumartist;
298     std::string tracknumber;
299     std::string discnumber;
300 
301     if( p_meta )
302     {
303         title = meta_get_escaped( p_meta, vlc_meta_Title );
304         artwork = meta_get_escaped( p_meta, vlc_meta_ArtworkURL );
305 
306         if( b_music && !title.empty() )
307         {
308             artist = meta_get_escaped( p_meta, vlc_meta_Artist );
309             album = meta_get_escaped( p_meta, vlc_meta_Album );
310             albumartist = meta_get_escaped( p_meta, vlc_meta_AlbumArtist );
311             tracknumber = meta_get_escaped( p_meta, vlc_meta_TrackNumber );
312             discnumber = meta_get_escaped( p_meta, vlc_meta_DiscNumber );
313         }
314         if( title.empty() )
315         {
316             title = meta_get_escaped( p_meta, vlc_meta_NowPlaying );
317             if( title.empty() )
318                 title = meta_get_escaped( p_meta, vlc_meta_ESNowPlaying );
319         }
320 
321         if ( !title.empty() )
322         {
323             ss << "\"metadata\":{"
324                << " \"metadataType\":" << ( b_music ? "3" : "0" )
325                << ",\"title\":\"" << title << "\"";
326             if( b_music )
327             {
328                 if( !artist.empty() )
329                     ss << ",\"artist\":\"" << artist << "\"";
330                 if( album.empty() )
331                     ss << ",\"album\":\"" << album << "\"";
332                 if( albumartist.empty() )
333                     ss << ",\"albumArtist\":\"" << albumartist << "\"";
334                 if( tracknumber.empty() )
335                     ss << ",\"trackNumber\":\"" << tracknumber << "\"";
336                 if( discnumber.empty() )
337                     ss << ",\"discNumber\":\"" << discnumber << "\"";
338             }
339 
340             if ( !artwork.empty() && !strncmp( artwork.c_str(), "http", 4 ) )
341                 ss << ",\"images\":[{\"url\":\"" << artwork << "\"}]";
342 
343             ss << "},";
344         }
345     }
346 
347     std::stringstream chromecast_url;
348     chromecast_url << "http://" << m_serverIp << ":" << m_serverPort << m_serverPath;
349 
350     msg_Dbg( m_module, "s_chromecast_url: %s", chromecast_url.str().c_str());
351 
352     ss << "\"contentId\":\"" << chromecast_url.str() << "\""
353        << ",\"streamType\":\"LIVE\""
354        << ",\"contentType\":\"" << mime << "\"";
355 
356     return ss.str();
357 }
358 
msgPlayerLoad(const std::string & destinationId,const std::string & mime,const vlc_meta_t * p_meta)359 unsigned ChromecastCommunication::msgPlayerLoad( const std::string& destinationId,
360                                              const std::string& mime, const vlc_meta_t *p_meta )
361 {
362     unsigned id = getNextRequestId();
363     std::stringstream ss;
364     ss << "{\"type\":\"LOAD\","
365        <<  "\"media\":{" << GetMedia( mime, p_meta ) << "},"
366        <<  "\"autoplay\":\"false\","
367        <<  "\"requestId\":" << id
368        << "}";
369 
370     return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
371 }
372 
msgPlayerPlay(const std::string & destinationId,int64_t mediaSessionId)373 unsigned ChromecastCommunication::msgPlayerPlay( const std::string& destinationId, int64_t mediaSessionId )
374 {
375     assert(mediaSessionId != 0);
376     unsigned id = getNextRequestId();
377 
378     std::stringstream ss;
379     ss << "{\"type\":\"PLAY\","
380        <<  "\"mediaSessionId\":" << mediaSessionId << ","
381        <<  "\"requestId\":" << id
382        << "}";
383 
384     return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
385 }
386 
msgPlayerStop(const std::string & destinationId,int64_t mediaSessionId)387 unsigned ChromecastCommunication::msgPlayerStop( const std::string& destinationId, int64_t mediaSessionId )
388 {
389     assert(mediaSessionId != 0);
390     unsigned id = getNextRequestId();
391 
392     std::stringstream ss;
393     ss << "{\"type\":\"STOP\","
394        <<  "\"mediaSessionId\":" << mediaSessionId << ","
395        <<  "\"requestId\":" << id
396        << "}";
397 
398     return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
399 }
400 
msgPlayerPause(const std::string & destinationId,int64_t mediaSessionId)401 unsigned ChromecastCommunication::msgPlayerPause( const std::string& destinationId, int64_t mediaSessionId )
402 {
403     assert(mediaSessionId != 0);
404     unsigned id = getNextRequestId();
405 
406     std::stringstream ss;
407     ss << "{\"type\":\"PAUSE\","
408        <<  "\"mediaSessionId\":" << mediaSessionId << ","
409        <<  "\"requestId\":" << id
410        << "}";
411 
412     return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
413 }
414 
msgPlayerSetVolume(const std::string & destinationId,int64_t mediaSessionId,float f_volume,bool b_mute)415 unsigned ChromecastCommunication::msgPlayerSetVolume( const std::string& destinationId, int64_t mediaSessionId, float f_volume, bool b_mute )
416 {
417     assert(mediaSessionId != 0);
418     unsigned id = getNextRequestId();
419 
420     if ( f_volume < 0.0 || f_volume > 1.0)
421         return VLC_EGENERIC;
422 
423     std::stringstream ss;
424     ss << "{\"type\":\"SET_VOLUME\","
425        <<  "\"volume\":{\"level\":" << f_volume << ",\"muted\":" << ( b_mute ? "true" : "false" ) << "},"
426        <<  "\"mediaSessionId\":" << mediaSessionId << ","
427        <<  "\"requestId\":" << id
428        << "}";
429 
430     return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
431 }
432 
433 /**
434  * @brief Send a message to the Chromecast
435  * @param msg the CastMessage to send
436  * @return vlc error code
437  */
sendMessage(const castchannel::CastMessage & msg)438 int ChromecastCommunication::sendMessage( const castchannel::CastMessage &msg )
439 {
440     int i_size = msg.ByteSize();
441     uint8_t *p_data = new(std::nothrow) uint8_t[PACKET_HEADER_LEN + i_size];
442     if (p_data == NULL)
443         return VLC_ENOMEM;
444 
445 #ifndef NDEBUG
446     msg_Dbg( m_module, "sendMessage: %s->%s %s", msg.namespace_().c_str(), msg.destination_id().c_str(), msg.payload_utf8().c_str());
447 #endif
448 
449     SetDWBE(p_data, i_size);
450     msg.SerializeWithCachedSizesToArray(p_data + PACKET_HEADER_LEN);
451 
452     int i_ret = vlc_tls_Write(m_tls, p_data, PACKET_HEADER_LEN + i_size);
453     delete[] p_data;
454     if (i_ret == PACKET_HEADER_LEN + i_size)
455         return VLC_SUCCESS;
456 
457     msg_Warn( m_module, "failed to send message %s (%s)", msg.payload_utf8().c_str(), strerror( errno ) );
458 
459     return VLC_EGENERIC;
460 }
461 
pushMediaPlayerMessage(const std::string & destinationId,const std::stringstream & payload)462 int ChromecastCommunication::pushMediaPlayerMessage( const std::string& destinationId, const std::stringstream & payload )
463 {
464     assert(!destinationId.empty());
465     return buildMessage( NAMESPACE_MEDIA, payload.str(), destinationId );
466 }
467