You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
466 lines
15 KiB
466 lines
15 KiB
/*****************************************************************************
|
|
* chromecast_communication.cpp: Handle chromecast protocol messages
|
|
*****************************************************************************
|
|
* Copyright © 2014-2017 VideoLAN
|
|
*
|
|
* Authors: Adrien Maglo <magsoft@videolan.org>
|
|
* Jean-Baptiste Kempf <jb@videolan.org>
|
|
* Steve Lhomme <robux4@videolabs.io>
|
|
* Hugo Beauzée-Luyssen <hugo@beauzee.fr>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include "config.h"
|
|
#endif
|
|
|
|
#include "chromecast.h"
|
|
#ifdef HAVE_POLL_H
|
|
# include <poll.h>
|
|
#endif
|
|
|
|
#include <iomanip>
|
|
|
|
ChromecastCommunication::ChromecastCommunication( vlc_object_t* p_module,
|
|
std::string serverPath, unsigned int serverPort, const char* targetIP, unsigned int devicePort )
|
|
: m_module( p_module )
|
|
, m_creds( NULL )
|
|
, m_tls( NULL )
|
|
, m_receiver_requestId( 1 )
|
|
, m_requestId( 1 )
|
|
, m_serverPath( serverPath )
|
|
, m_serverPort( serverPort )
|
|
{
|
|
if (devicePort == 0)
|
|
devicePort = CHROMECAST_CONTROL_PORT;
|
|
|
|
m_creds = vlc_tls_ClientCreate( vlc_object_parent(m_module) );
|
|
if (m_creds == NULL)
|
|
throw std::runtime_error( "Failed to create TLS client" );
|
|
|
|
m_tls = vlc_tls_SocketOpenTLS( m_creds, targetIP, devicePort, "tcps",
|
|
NULL, NULL );
|
|
if (m_tls == NULL)
|
|
{
|
|
vlc_tls_ClientDelete(m_creds);
|
|
throw std::runtime_error( "Failed to create client session" );
|
|
}
|
|
|
|
char psz_localIP[NI_MAXNUMERICHOST];
|
|
if (net_GetSockAddress( vlc_tls_GetFD(m_tls), psz_localIP, NULL ))
|
|
throw std::runtime_error( "Cannot get local IP address" );
|
|
|
|
m_serverIp = psz_localIP;
|
|
}
|
|
|
|
ChromecastCommunication::~ChromecastCommunication()
|
|
{
|
|
disconnect();
|
|
}
|
|
|
|
void ChromecastCommunication::disconnect()
|
|
{
|
|
if ( m_tls != NULL )
|
|
{
|
|
vlc_tls_Close(m_tls);
|
|
vlc_tls_ClientDelete(m_creds);
|
|
m_tls = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Build a CastMessage to send to the Chromecast
|
|
* @param namespace_ the message namespace
|
|
* @param payloadType the payload type (CastMessage_PayloadType_STRING or
|
|
* CastMessage_PayloadType_BINARY
|
|
* @param payload the payload
|
|
* @param destinationId the destination idenifier
|
|
* @return the generated CastMessage
|
|
*/
|
|
int ChromecastCommunication::buildMessage(const std::string & namespace_,
|
|
const std::string & payload,
|
|
const std::string & destinationId,
|
|
castchannel::CastMessage_PayloadType payloadType)
|
|
{
|
|
castchannel::CastMessage msg;
|
|
|
|
msg.set_protocol_version(castchannel::CastMessage_ProtocolVersion_CASTV2_1_0);
|
|
msg.set_namespace_(namespace_);
|
|
msg.set_payload_type(payloadType);
|
|
msg.set_source_id("sender-vlc");
|
|
msg.set_destination_id(destinationId);
|
|
if (payloadType == castchannel::CastMessage_PayloadType_STRING)
|
|
msg.set_payload_utf8(payload);
|
|
else // CastMessage_PayloadType_BINARY
|
|
msg.set_payload_binary(payload);
|
|
|
|
return sendMessage(msg);
|
|
}
|
|
|
|
/**
|
|
* @brief Receive a data packet from the Chromecast
|
|
* @param p_data the buffer in which to store the data
|
|
* @param i_size the size of the buffer
|
|
* @param i_timeout maximum time to wait for a packet, in millisecond
|
|
* @param pb_timeout Output parameter that will contain true if no packet was received due to a timeout
|
|
* @return the number of bytes received of -1 on error
|
|
*/
|
|
ssize_t ChromecastCommunication::receive( uint8_t *p_data, size_t i_size, int i_timeout, bool *pb_timeout )
|
|
{
|
|
ssize_t i_received = 0;
|
|
|
|
struct iovec iov;
|
|
iov.iov_base = p_data;
|
|
iov.iov_len = i_size;
|
|
|
|
/* The Chromecast normally sends a PING command every 5 seconds or so.
|
|
* If we do not receive one after 6 seconds, we send a PING.
|
|
* If after this PING, we do not receive a PONG, then we consider the
|
|
* connection as dead. */
|
|
do
|
|
{
|
|
ssize_t i_ret = m_tls->ops->readv( m_tls, &iov, 1 );
|
|
if ( i_ret < 0 )
|
|
{
|
|
#ifdef _WIN32
|
|
if ( WSAGetLastError() != WSAEWOULDBLOCK )
|
|
#else
|
|
if ( errno != EAGAIN )
|
|
#endif
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
struct pollfd ufd[1];
|
|
ufd[0].events = POLLIN;
|
|
ufd[0].fd = vlc_tls_GetPollFD( m_tls, &ufd[0].events );
|
|
|
|
ssize_t val = vlc_poll_i11e(ufd, 1, i_timeout);
|
|
if ( val < 0 )
|
|
return -1;
|
|
else if ( val == 0 )
|
|
{
|
|
*pb_timeout = true;
|
|
return i_received;
|
|
}
|
|
assert( ufd[0].revents & POLLIN );
|
|
continue;
|
|
}
|
|
else if ( i_ret == 0 )
|
|
return -1;
|
|
assert( i_size >= (size_t)i_ret );
|
|
i_size -= i_ret;
|
|
i_received += i_ret;
|
|
iov.iov_base = (uint8_t*)iov.iov_base + i_ret;
|
|
iov.iov_len = i_size;
|
|
} while ( i_size > 0 );
|
|
return i_received;
|
|
}
|
|
|
|
|
|
/*****************************************************************************
|
|
* Message preparation
|
|
*****************************************************************************/
|
|
unsigned ChromecastCommunication::getNextReceiverRequestId()
|
|
{
|
|
unsigned id = m_receiver_requestId++;
|
|
return likely(id != 0) ? id : m_receiver_requestId++;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::getNextRequestId()
|
|
{
|
|
unsigned id = m_requestId++;
|
|
return likely(id != 0) ? id : m_requestId++;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgAuth()
|
|
{
|
|
castchannel::DeviceAuthMessage authMessage;
|
|
authMessage.mutable_challenge();
|
|
|
|
return buildMessage(NAMESPACE_DEVICEAUTH, authMessage.SerializeAsString(),
|
|
DEFAULT_CHOMECAST_RECEIVER, castchannel::CastMessage_PayloadType_BINARY)
|
|
== VLC_SUCCESS ? 1 : kInvalidId;
|
|
}
|
|
|
|
|
|
unsigned ChromecastCommunication::msgPing()
|
|
{
|
|
std::string s("{\"type\":\"PING\"}");
|
|
return buildMessage( NAMESPACE_HEARTBEAT, s, DEFAULT_CHOMECAST_RECEIVER )
|
|
== VLC_SUCCESS ? 1 : kInvalidId;
|
|
}
|
|
|
|
|
|
unsigned ChromecastCommunication::msgPong()
|
|
{
|
|
std::string s("{\"type\":\"PONG\"}");
|
|
return buildMessage( NAMESPACE_HEARTBEAT, s, DEFAULT_CHOMECAST_RECEIVER )
|
|
== VLC_SUCCESS ? 1 : kInvalidId;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgConnect( const std::string& destinationId )
|
|
{
|
|
std::string s("{\"type\":\"CONNECT\"}");
|
|
return buildMessage( NAMESPACE_CONNECTION, s, destinationId )
|
|
== VLC_SUCCESS ? 1 : kInvalidId;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgReceiverClose( const std::string& destinationId )
|
|
{
|
|
std::string s("{\"type\":\"CLOSE\"}");
|
|
return buildMessage( NAMESPACE_CONNECTION, s, destinationId )
|
|
== VLC_SUCCESS ? 1 : kInvalidId;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgReceiverGetStatus()
|
|
{
|
|
unsigned id = getNextReceiverRequestId();
|
|
std::stringstream ss;
|
|
ss << "{\"type\":\"GET_STATUS\","
|
|
<< "\"requestId\":" << id << "}";
|
|
|
|
return buildMessage( NAMESPACE_RECEIVER, ss.str(), DEFAULT_CHOMECAST_RECEIVER )
|
|
== VLC_SUCCESS ? id : kInvalidId;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgReceiverLaunchApp()
|
|
{
|
|
unsigned id = getNextReceiverRequestId();
|
|
std::stringstream ss;
|
|
ss << "{\"type\":\"LAUNCH\","
|
|
<< "\"appId\":\"" << APP_ID << "\","
|
|
<< "\"requestId\":" << id << "}";
|
|
|
|
return buildMessage( NAMESPACE_RECEIVER, ss.str(), DEFAULT_CHOMECAST_RECEIVER )
|
|
== VLC_SUCCESS ? id : kInvalidId;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgPlayerGetStatus( const std::string& destinationId )
|
|
{
|
|
unsigned id = getNextRequestId();
|
|
std::stringstream ss;
|
|
ss << "{\"type\":\"GET_STATUS\","
|
|
<< "\"requestId\":" << id
|
|
<< "}";
|
|
|
|
return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
|
|
}
|
|
|
|
static std::string escape_json(const std::string &s)
|
|
{
|
|
/* Control characters ('\x00' to '\x1f'), '"' and '\" must be escaped */
|
|
std::ostringstream o;
|
|
for (std::string::const_iterator c = s.begin(); c != s.end(); c++)
|
|
{
|
|
if (*c == '"' || *c == '\\' || ('\x00' <= *c && *c <= '\x1f'))
|
|
o << "\\u"
|
|
<< std::hex << std::setw(4) << std::setfill('0') << (int)*c;
|
|
else
|
|
o << *c;
|
|
}
|
|
return o.str();
|
|
}
|
|
|
|
static std::string meta_get_escaped(const vlc_meta_t *p_meta, vlc_meta_type_t type)
|
|
{
|
|
const char *psz = vlc_meta_Get(p_meta, type);
|
|
if (!psz)
|
|
return std::string();
|
|
return escape_json(std::string(psz));
|
|
}
|
|
|
|
std::string ChromecastCommunication::GetMedia( const std::string& mime,
|
|
const vlc_meta_t *p_meta )
|
|
{
|
|
std::stringstream ss;
|
|
|
|
bool b_music = strncmp(mime.c_str(), "audio", strlen("audio")) == 0;
|
|
|
|
std::string title;
|
|
std::string artwork;
|
|
std::string artist;
|
|
std::string album;
|
|
std::string albumartist;
|
|
std::string tracknumber;
|
|
std::string discnumber;
|
|
|
|
if( p_meta )
|
|
{
|
|
title = meta_get_escaped( p_meta, vlc_meta_Title );
|
|
artwork = meta_get_escaped( p_meta, vlc_meta_ArtworkURL );
|
|
|
|
if( b_music && !title.empty() )
|
|
{
|
|
artist = meta_get_escaped( p_meta, vlc_meta_Artist );
|
|
album = meta_get_escaped( p_meta, vlc_meta_Album );
|
|
albumartist = meta_get_escaped( p_meta, vlc_meta_AlbumArtist );
|
|
tracknumber = meta_get_escaped( p_meta, vlc_meta_TrackNumber );
|
|
discnumber = meta_get_escaped( p_meta, vlc_meta_DiscNumber );
|
|
}
|
|
if( title.empty() )
|
|
{
|
|
title = meta_get_escaped( p_meta, vlc_meta_NowPlaying );
|
|
if( title.empty() )
|
|
title = meta_get_escaped( p_meta, vlc_meta_ESNowPlaying );
|
|
}
|
|
|
|
if ( !title.empty() )
|
|
{
|
|
ss << "\"metadata\":{"
|
|
<< " \"metadataType\":" << ( b_music ? "3" : "0" )
|
|
<< ",\"title\":\"" << title << "\"";
|
|
if( b_music )
|
|
{
|
|
if( !artist.empty() )
|
|
ss << ",\"artist\":\"" << artist << "\"";
|
|
if( !album.empty() )
|
|
ss << ",\"album\":\"" << album << "\"";
|
|
if( !albumartist.empty() )
|
|
ss << ",\"albumArtist\":\"" << albumartist << "\"";
|
|
if( !tracknumber.empty() )
|
|
ss << ",\"trackNumber\":\"" << tracknumber << "\"";
|
|
if( !discnumber.empty() )
|
|
ss << ",\"discNumber\":\"" << discnumber << "\"";
|
|
}
|
|
|
|
if ( !artwork.empty() && !strncmp( artwork.c_str(), "http", 4 ) )
|
|
ss << ",\"images\":[{\"url\":\"" << artwork << "\"}]";
|
|
|
|
ss << "},";
|
|
}
|
|
}
|
|
|
|
std::stringstream chromecast_url;
|
|
chromecast_url << "http://" << m_serverIp << ":" << m_serverPort << m_serverPath;
|
|
|
|
msg_Dbg( m_module, "s_chromecast_url: %s", chromecast_url.str().c_str());
|
|
|
|
ss << "\"contentId\":\"" << chromecast_url.str() << "\""
|
|
<< ",\"streamType\":\"LIVE\""
|
|
<< ",\"contentType\":\"" << mime << "\"";
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgPlayerLoad( const std::string& destinationId,
|
|
const std::string& mime, const vlc_meta_t *p_meta )
|
|
{
|
|
unsigned id = getNextRequestId();
|
|
std::stringstream ss;
|
|
ss << "{\"type\":\"LOAD\","
|
|
<< "\"media\":{" << GetMedia( mime, p_meta ) << "},"
|
|
<< "\"autoplay\":\"false\","
|
|
<< "\"requestId\":" << id
|
|
<< "}";
|
|
|
|
return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgPlayerPlay( const std::string& destinationId, int64_t mediaSessionId )
|
|
{
|
|
assert(mediaSessionId != 0);
|
|
unsigned id = getNextRequestId();
|
|
|
|
std::stringstream ss;
|
|
ss << "{\"type\":\"PLAY\","
|
|
<< "\"mediaSessionId\":" << mediaSessionId << ","
|
|
<< "\"requestId\":" << id
|
|
<< "}";
|
|
|
|
return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgPlayerStop( const std::string& destinationId, int64_t mediaSessionId )
|
|
{
|
|
assert(mediaSessionId != 0);
|
|
unsigned id = getNextRequestId();
|
|
|
|
std::stringstream ss;
|
|
ss << "{\"type\":\"STOP\","
|
|
<< "\"mediaSessionId\":" << mediaSessionId << ","
|
|
<< "\"requestId\":" << id
|
|
<< "}";
|
|
|
|
return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgPlayerPause( const std::string& destinationId, int64_t mediaSessionId )
|
|
{
|
|
assert(mediaSessionId != 0);
|
|
unsigned id = getNextRequestId();
|
|
|
|
std::stringstream ss;
|
|
ss << "{\"type\":\"PAUSE\","
|
|
<< "\"mediaSessionId\":" << mediaSessionId << ","
|
|
<< "\"requestId\":" << id
|
|
<< "}";
|
|
|
|
return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
|
|
}
|
|
|
|
unsigned ChromecastCommunication::msgPlayerSetVolume( const std::string& destinationId, int64_t mediaSessionId, float f_volume, bool b_mute )
|
|
{
|
|
assert(mediaSessionId != 0);
|
|
unsigned id = getNextRequestId();
|
|
|
|
if ( f_volume < 0.0 || f_volume > 1.0)
|
|
return VLC_EGENERIC;
|
|
|
|
std::stringstream ss;
|
|
ss << "{\"type\":\"SET_VOLUME\","
|
|
<< "\"volume\":{\"level\":" << f_volume << ",\"muted\":" << ( b_mute ? "true" : "false" ) << "},"
|
|
<< "\"mediaSessionId\":" << mediaSessionId << ","
|
|
<< "\"requestId\":" << id
|
|
<< "}";
|
|
|
|
return pushMediaPlayerMessage( destinationId, ss ) == VLC_SUCCESS ? id : kInvalidId;
|
|
}
|
|
|
|
/**
|
|
* @brief Send a message to the Chromecast
|
|
* @param msg the CastMessage to send
|
|
* @return vlc error code
|
|
*/
|
|
int ChromecastCommunication::sendMessage( const castchannel::CastMessage &msg )
|
|
{
|
|
size_t i_size = msg.ByteSizeLong();
|
|
uint8_t *p_data = new(std::nothrow) uint8_t[PACKET_HEADER_LEN + i_size];
|
|
if (p_data == NULL)
|
|
return VLC_ENOMEM;
|
|
|
|
#ifndef NDEBUG
|
|
msg_Dbg( m_module, "sendMessage: %s->%s %s", msg.namespace_().c_str(), msg.destination_id().c_str(), msg.payload_utf8().c_str());
|
|
#endif
|
|
|
|
SetDWBE(p_data, i_size);
|
|
msg.SerializeWithCachedSizesToArray(p_data + PACKET_HEADER_LEN);
|
|
|
|
ssize_t i_ret = vlc_tls_Write(m_tls, p_data, PACKET_HEADER_LEN + i_size);
|
|
delete[] p_data;
|
|
if (i_ret > 0 && (size_t)i_ret == PACKET_HEADER_LEN + i_size)
|
|
return VLC_SUCCESS;
|
|
|
|
msg_Warn( m_module, "failed to send message %s (%s)", msg.payload_utf8().c_str(), strerror( errno ) );
|
|
|
|
return VLC_EGENERIC;
|
|
}
|
|
|
|
int ChromecastCommunication::pushMediaPlayerMessage( const std::string& destinationId, const std::stringstream & payload )
|
|
{
|
|
assert(!destinationId.empty());
|
|
return buildMessage( NAMESPACE_MEDIA, payload.str(), destinationId );
|
|
}
|
|
|