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