Commit 2e8a23cf authored by Adrien Maglo's avatar Adrien Maglo

chromecast: add an experimental sout module that connects to a ChromeCast...

chromecast: add an experimental sout module that connects to a ChromeCast device and streams using the HTTP access output

Example: vlc input.mp4 --sout="#chromecast{ip=192.168.1.11}"
parent 31d84ce0
......@@ -636,7 +636,7 @@ AC_CHECK_FUNCS([if_nameindex if_nametoindex])
VLC_RESTORE_FLAGS
AS_IF([test -n "$SOCKET_LIBS"], [
VLC_ADD_LIBS([access_rtmp access_output_shout sap stream_out_standard stream_out_rtp stream_out_raop vod_rtsp oldrc netsync ts remoteosd audiobargraph_a],[${SOCKET_LIBS}])
VLC_ADD_LIBS([access_rtmp access_output_shout sap stream_out_standard stream_out_rtp stream_out_raop stream_out_chromecast vod_rtsp oldrc netsync ts remoteosd audiobargraph_a],[${SOCKET_LIBS}])
])
AC_SUBST(SOCKET_LIBS)
......@@ -3729,6 +3729,30 @@ PKG_WITH_MODULES([CHROMAPRINT],[libchromaprint >= libchromaprint_version],
[(Chromaprint based audio fingerprinter)],[auto])
m4_popdef([libchromaprint_version])
dnl
dnl Chromecast streaming support
dnl
m4_pushdef([protobuf_lite_version], 2.5.0)
PKG_WITH_MODULES([CHROMECAST],[protobuf-lite >= protobuf_lite_version], [
AC_CHECK_PROGS(PROTOC, protoc)
AS_IF([test "${PROTOC}" = "protoc"], [
VLC_ADD_PLUGIN([stream_out_chromecast])
VLC_ADD_CXXFLAGS([stream_out_chromecast],[${CHROMECAST_CFLAGS}] [-I./chromecast])
VLC_ADD_LIBS([stream_out_chromecast],[${CHROMECAST_LIBS}])
build_chromecast="yes"
], [
AC_MSG_ERROR(protoc compiler needed for [chromecast] was not found)
])
], [
AS_IF([test "${enable_chromecast}" = "yes"],
AC_MSG_ERROR(Library [protobuf-lite >= protobuf_lite_version] needed for [chromecast] was not found),
AC_MSG_WARN(Library [protobuf-lite >= protobuf_lite_version] needed for [chromecast] was not found)
)
enable_chromecast="no"
], [(Chromecast streaming support)], [auto])
AM_CONDITIONAL([BUILD_CHROMECAST], [test "${build_chromecast}" = "yes"])
m4_popdef([protobuf_lite_version])
dnl
dnl Interface plugins
dnl
......
......@@ -69,3 +69,18 @@ BUILT_SOURCES += dummy.cpp
dummy.cpp:
touch dummy.cpp
# Chromecast plugin
SUFFIXES += .proto .pb.cc
BUILT_SOURCES += chromecast/cast_channel.pb.cc
%.pb.cc: %.proto
$(PROTOC) --cpp_out=. -I$(srcdir) $<
SOURCES_stream_out_chromecast = chromecast/cast_channel.proto chromecast/cast.cpp \
../misc/webservices/json.h ../misc/webservices/json.c
nodist_libstream_out_chromecast_plugin_la_SOURCES = chromecast/cast_channel.pb.cc
if BUILD_CHROMECAST
stream_out_LTLIBRARIES += libstream_out_chromecast_plugin.la
endif
/*****************************************************************************
* cast.cpp: Chromecast module for vlc
*****************************************************************************
* Copyright © 2014 VideoLAN
*
* Authors: Adrien Maglo <magsoft@videolan.org>
* Jean-Baptiste Kempf <jb@videolan.org>
*
* 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.
*****************************************************************************/
/*****************************************************************************
* Preamble
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#ifdef HAVE_POLL
# include <poll.h>
#endif
#include <vlc_common.h>
#include <vlc_plugin.h>
#include <vlc_sout.h>
#include <vlc_tls.h>
#include <vlc_url.h>
#include <vlc_threads.h>
#include <vlc_atomic.h>
#include <cerrno>
#include <sstream>
#include <queue>
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/io/coded_stream.h>
#include "cast_channel.pb.h"
#include "../../misc/webservices/json.h"
// Status
enum
{
CHROMECAST_DISCONNECTED,
CHROMECAST_TLS_CONNECTED,
CHROMECAST_AUTHENTICATED,
CHROMECAST_APP_STARTED,
CHROMECAST_MEDIA_LOAD_SENT,
CHROMECAST_CONNECTION_DEAD,
};
#define PACKET_MAX_LEN 10 * 1024
#define PACKET_HEADER_LEN 4
struct sout_stream_sys_t
{
sout_stream_sys_t()
: i_status(CHROMECAST_DISCONNECTED), p_out(NULL)
{
atomic_init(&ab_error, false);
}
std::string serverIP;
int i_sock_fd;
vlc_tls_creds_t *p_creds;
vlc_tls_t *p_tls;
vlc_thread_t chromecastThread;
unsigned i_requestId;
std::string appTransportId;
std::queue<castchannel::CastMessage> messagesToSend;
int i_status;
atomic_bool ab_error;
vlc_mutex_t lock;
vlc_cond_t loadCommandCond;
sout_stream_t *p_out;
};
// Media player Chromecast app id
#define APP_ID "CC1AD845" // Default media player
#define CHROMECAST_CONTROL_PORT 8009
#define HTTP_PORT 8010
#define SOUT_CFG_PREFIX "sout-chromecast-"
/*****************************************************************************
* Local prototypes
*****************************************************************************/
static int Open(vlc_object_t *);
static void Close(vlc_object_t *);
static void Clean(sout_stream_sys_t *p_sys);
static int connectChromecast(sout_stream_t *p_stream, char *psz_ipChromecast);
static void disconnectChromecast(sout_stream_t *p_stream);
static int sendMessages(sout_stream_t *p_stream);
static void msgAuth(sout_stream_t *p_stream);
static void msgPing(sout_stream_t *p_stream);
static void msgPong(sout_stream_t *p_stream);
static void msgConnect(sout_stream_t *p_stream, std::string destinationId);
static void msgClose(sout_stream_t *p_stream, std::string destinationId);
static void msgLaunch(sout_stream_t *p_stream);
static void msgLoad(sout_stream_t *p_stream);
static void *chromecastThread(void *data);
static const char *const ppsz_sout_options[] = {
"ip", "http-port", "mux", "mime", NULL
};
/*****************************************************************************
* Module descriptor
*****************************************************************************/
#define IP_TEXT N_("Chromecast IP address")
#define IP_LONGTEXT N_("This sets the IP adress of the Chromecast receiver.")
#define HTTP_PORT_TEXT N_("HTTP port")
#define HTTP_PORT_LONGTEXT N_("This sets the HTTP port of the server " \
"used to stream the media to the Chromecast.")
#define MUX_TEXT N_("Muxer")
#define MUX_LONGTEXT N_("This sets the muxer used to stream to the Chromecast.")
#define MIME_TEXT N_("MIME content type")
#define MIME_LONGTEXT N_("This sets the media MIME content type sent to the Chromecast.")
vlc_module_begin ()
set_shortname(N_("Chromecast"))
set_description(N_("Chromecast stream output"))
set_capability("sout stream", 0)
add_shortcut("chromecast")
set_category(CAT_SOUT)
set_subcategory(SUBCAT_SOUT_STREAM)
set_callbacks(Open, Close)
add_string(SOUT_CFG_PREFIX "ip", "", IP_TEXT, IP_LONGTEXT, false)
add_integer(SOUT_CFG_PREFIX "http-port", HTTP_PORT, HTTP_PORT_TEXT, HTTP_PORT_LONGTEXT, false)
add_string(SOUT_CFG_PREFIX "mux", "mp4stream", MUX_TEXT, MUX_LONGTEXT, false)
add_string(SOUT_CFG_PREFIX "mime", "video/mp4", MIME_TEXT, MIME_LONGTEXT, false)
vlc_module_end ()
/*****************************************************************************
* Sout callbacks
*****************************************************************************/
static sout_stream_id_sys_t *Add(sout_stream_t *p_stream, es_format_t *p_fmt)
{
sout_stream_sys_t *p_sys = p_stream->p_sys;
return p_sys->p_out->pf_add(p_sys->p_out, p_fmt);
}
static int Del(sout_stream_t *p_stream, sout_stream_id_sys_t *id)
{
sout_stream_sys_t *p_sys = p_stream->p_sys;
return p_sys->p_out->pf_del(p_sys->p_out, id);
}
static int Send(sout_stream_t *p_stream, sout_stream_id_sys_t *id,
block_t *p_buffer)
{
sout_stream_sys_t *p_sys = p_stream->p_sys;
if (atomic_load(&p_sys->ab_error))
return VLC_EGENERIC;
return p_sys->p_out->pf_send(p_sys->p_out, id, p_buffer);
}
/*****************************************************************************
* Open: connect to the Chromecast and initialize the sout
*****************************************************************************/
static int Open(vlc_object_t *p_this)
{
sout_stream_t *p_stream = (sout_stream_t*)p_this;
sout_stream_sys_t *p_sys;
p_sys = new(std::nothrow) sout_stream_sys_t;
if (p_sys == NULL)
return VLC_ENOMEM;
p_stream->p_sys = p_sys;
config_ChainParse(p_stream, SOUT_CFG_PREFIX, ppsz_sout_options, p_stream->p_cfg);
char *psz_ipChromecast = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "ip");
if (psz_ipChromecast == NULL)
{
msg_Err(p_stream, "No Chromecast receiver IP provided");
Clean(p_sys);
return VLC_EGENERIC;
}
p_sys->i_sock_fd = connectChromecast(p_stream, psz_ipChromecast);
free(psz_ipChromecast);
if (p_sys->i_sock_fd < 0)
{
msg_Err(p_stream, "Could not connect the Chromecast");
Clean(p_sys);
return VLC_EGENERIC;
}
p_sys->i_status = CHROMECAST_TLS_CONNECTED;
char psz_localIP[NI_MAXNUMERICHOST];
if (net_GetSockAddress(p_sys->i_sock_fd, psz_localIP, NULL))
{
msg_Err(p_this, "Cannot get local IP address");
Clean(p_sys);
return VLC_EGENERIC;
}
p_sys->serverIP = psz_localIP;
char *psz_mux = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "mux");
if (psz_mux == NULL)
{
Clean(p_sys);
return VLC_EGENERIC;
}
char *psz_chain = NULL;
int i_bytes = asprintf(&psz_chain, "http{dst=:%u/stream,mux=%s}",
(unsigned)var_InheritInteger(p_stream, SOUT_CFG_PREFIX"http-port"),
psz_mux);
free(psz_mux);
if (i_bytes < 0)
{
Clean(p_sys);
return VLC_EGENERIC;
}
p_sys->p_out = sout_StreamChainNew(p_stream->p_sout, psz_chain, NULL, NULL);
free(psz_chain);
if (p_sys->p_out == NULL)
{
Clean(p_sys);
return VLC_EGENERIC;
}
vlc_mutex_init(&p_sys->lock);
vlc_cond_init(&p_sys->loadCommandCond);
// Start the Chromecast event thread.
if (vlc_clone(&p_sys->chromecastThread, chromecastThread, p_stream,
VLC_THREAD_PRIORITY_LOW))
{
msg_Err(p_stream, "Could not start the Chromecast talking thread");
Clean(p_sys);
return VLC_EGENERIC;
}
/* Ugly part:
* We want to be sure that the Chromecast receives the first data packet sent by
* the HTTP server. */
// Lock the sout thread until we have sent the media loading command to the Chromecast.
int i_ret = 0;
{
const mtime_t deadline = mdate() + 6 * CLOCK_FREQ;
vlc_mutex_locker locker(&p_sys->lock);
while (p_sys->i_status != CHROMECAST_MEDIA_LOAD_SENT)
i_ret = vlc_cond_timedwait(&p_sys->loadCommandCond, &p_sys->lock, deadline);
}
if (i_ret == ETIMEDOUT)
{
msg_Err(p_stream, "Timeout reached before sending the media loading command");
vlc_cancel(p_sys->chromecastThread);
vlc_join(p_sys->chromecastThread, NULL);
Clean(p_sys);
return VLC_EGENERIC;
}
/* Even uglier: sleep more to let to the Chromecast initiate the connection
* to the http server. */
msleep(2 * CLOCK_FREQ);
// Set the sout callbacks.
p_stream->pf_add = Add;
p_stream->pf_del = Del;
p_stream->pf_send = Send;
return VLC_SUCCESS;
}
/*****************************************************************************
* Close: destroy interface
*****************************************************************************/
static void Close(vlc_object_t *p_this)
{
sout_stream_t *p_stream = (sout_stream_t *)p_this;
sout_stream_sys_t *p_sys = p_stream->p_sys;
vlc_cancel(p_sys->chromecastThread);
vlc_join(p_sys->chromecastThread, NULL);
switch (p_sys->i_status)
{
case CHROMECAST_MEDIA_LOAD_SENT:
case CHROMECAST_APP_STARTED:
// Generate the close messages.
msgClose(p_stream, p_sys->appTransportId);
case CHROMECAST_AUTHENTICATED:
msgClose(p_stream, "receiver-0");
// Send the just added close messages.
sendMessages(p_stream);
case CHROMECAST_TLS_CONNECTED:
disconnectChromecast(p_stream);
p_sys->i_status = CHROMECAST_DISCONNECTED;
default:
break;
}
Clean(p_sys);
}
/**
* @brief Clean and release the variables in a sout_stream_sys_t structure
*/
static void Clean(sout_stream_sys_t *p_sys)
{
if (p_sys->p_out)
{
vlc_mutex_destroy(&p_sys->lock);
vlc_cond_destroy(&p_sys->loadCommandCond);
sout_StreamChainDelete(p_sys->p_out, p_sys->p_out);
}
delete p_sys;
}
/**
* @brief Connect to the Chromecast
* @param p_stream the sout_stream_t structure
* @return the opened socket file descriptor or -1 on error
*/
static int connectChromecast(sout_stream_t *p_stream, char *psz_ipChromecast)
{
sout_stream_sys_t *p_sys = p_stream->p_sys;
int fd = net_ConnectTCP(p_stream, psz_ipChromecast, CHROMECAST_CONTROL_PORT);
if (fd < 0)
return -1;
p_sys->p_creds = vlc_tls_ClientCreate(VLC_OBJECT(p_stream));
if (p_sys->p_creds == NULL)
return -1;
p_sys->p_tls = vlc_tls_ClientSessionCreate(p_sys->p_creds, fd, psz_ipChromecast,
"tcps", NULL, NULL);
if (p_sys->p_tls == NULL)
{
vlc_tls_Delete(p_sys->p_creds);
return -1;
}
return fd;
}
/**
* @brief Disconnect from the Chromecast
*/
static void disconnectChromecast(sout_stream_t *p_stream)
{
sout_stream_sys_t *p_sys = p_stream->p_sys;
vlc_tls_SessionDelete(p_sys->p_tls);
vlc_tls_Delete(p_sys->p_creds);
}
/**
* @brief Send a message to the Chromecast
* @param p_stream the sout_stream_t structure
* @param msg the CastMessage to send
* @return the number of bytes sent or -1 on error
*/
static int sendMessage(sout_stream_t *p_stream, castchannel::CastMessage &msg)
{
sout_stream_sys_t *p_sys = p_stream->p_sys;
uint32_t i_size = msg.ByteSize();
uint32_t i_sizeNetwork = hton32(i_size);
char *p_data = new(std::nothrow) char[PACKET_HEADER_LEN + i_size];
if (p_data == NULL)
return -1;
memcpy(p_data, &i_sizeNetwork, PACKET_HEADER_LEN);
msg.SerializeWithCachedSizesToArray((uint8_t *)(p_data + PACKET_HEADER_LEN));
int i_ret = tls_Send(p_sys->p_tls, p_data, PACKET_HEADER_LEN + i_size);
delete[] p_data;
return i_ret;
}
/**
* @brief Send all the messages in the pending queue to the Chromecast
* @param p_stream the sout_stream_t structure
* @param msg the CastMessage to send
* @return the number of bytes sent or -1 on error
*/
static int sendMessages(sout_stream_t *p_stream)
{
sout_stream_sys_t *p_sys = p_stream->p_sys;
int i_ret = 0;
while (!p_sys->messagesToSend.empty())
{
unsigned i_retSend = sendMessage(p_stream, p_sys->messagesToSend.front());
if (i_retSend <= 0)
return i_retSend;
p_sys->messagesToSend.pop();
i_ret += i_retSend;
}
return i_ret;
}
/**
* @brief Receive a data packet from the Chromecast
* @param p_stream the sout_stream_t structure
* @param b_msgReceived returns true if a message has been entirely received else false
* @param i_payloadSize returns the payload size of the message received
* @return the number of bytes received of -1 on error
*/
// Use here only C linkage and POD types as this function is a cancelation point.
extern "C" int recvPacket(sout_stream_t *p_stream, bool &b_msgReceived,
uint32_t &i_payloadSize, int i_sock_fd, vlc_tls_t *p_tls,
unsigned *pi_received, char *p_data, bool *pb_pingTimeout)
{
struct pollfd ufd[1];
ufd[0].fd = i_sock_fd;
ufd[0].events = POLLIN;
/* 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. */
if (poll(ufd, 1, 6000) == 0)
{
if (*pb_pingTimeout)
{
msg_Err(p_stream, "No PONG answer received from the Chromecast");
return 0; // Connection died
}
msg_Warn(p_stream, "No PING received from the Chromecast, sending a PING");
*pb_pingTimeout = true;
}
else
*pb_pingTimeout = false;
int i_ret;
/* Packet structure:
* +------------------------------------+------------------------------+
* | Payload size (uint32_t big endian) | Payload data |
* +------------------------------------+------------------------------+ */
if (*pi_received < PACKET_HEADER_LEN)
{
// We receive the header.
i_ret = tls_Recv(p_tls, p_data, PACKET_HEADER_LEN - *pi_received);
if (i_ret <= 0)
return i_ret;
*pi_received += i_ret;
}
else
{
// We receive the payload.
// Get the size of the payload
memcpy(&i_payloadSize, p_data, PACKET_HEADER_LEN);
i_payloadSize = hton32(i_payloadSize);
const uint32_t i_maxPayloadSize = PACKET_MAX_LEN - PACKET_HEADER_LEN;
if (i_payloadSize > i_maxPayloadSize)
{
// Error case: the packet sent by the Chromecast is too long: we drop it.
msg_Err(p_stream, "Packet too long: droping its data");
uint32_t i_size = i_payloadSize - (*pi_received - PACKET_HEADER_LEN);
if (i_size > i_maxPayloadSize)
i_size = i_maxPayloadSize;
i_ret = tls_Recv(p_tls, p_data + PACKET_HEADER_LEN, i_size);
if (i_ret <= 0)
return i_ret;
*pi_received += i_ret;
if (*pi_received < i_payloadSize + PACKET_HEADER_LEN)
return i_ret;
*pi_received = 0;
return -1;
}
// Normal case
i_ret = tls_Recv(p_tls, p_data + *pi_received,
i_payloadSize - (*pi_received - PACKET_HEADER_LEN));
if (i_ret <= 0)
return i_ret;
*pi_received += i_ret;
if (*pi_received < i_payloadSize + PACKET_HEADER_LEN)
return i_ret;
assert(*pi_received == i_payloadSize + PACKET_HEADER_LEN);
*pi_received = 0;
b_msgReceived = true;
return i_ret;
}
return i_ret;
}
/**
* @brief Process a message received from the Chromecast
* @param p_stream the sout_stream_t structure
* @param msg the CastMessage to process
* @return 0 if the message has been successfuly processed else -1
*/
static int processMessage(sout_stream_t *p_stream, const castchannel::CastMessage &msg)
{
int i_ret = 0;
sout_stream_sys_t *p_sys = p_stream->p_sys;
std::string namespace_ = msg.namespace_();
if (namespace_ == "urn:x-cast:com.google.cast.tp.deviceauth")
{
castchannel::DeviceAuthMessage authMessage;
authMessage.ParseFromString(msg.payload_binary());
if (authMessage.has_error())
{
msg_Err(p_stream, "Authentification error: %d", authMessage.error().error_type());
i_ret = -1;
}
else if (!authMessage.has_response())
{
msg_Err(p_stream, "Authentification message has no response field");
i_ret = -1;
}
else
{
vlc_mutex_locker locker(&p_sys->lock);
p_sys->i_status = CHROMECAST_AUTHENTICATED;
msgConnect(p_stream, "receiver-0");
msgLaunch(p_stream);
}
}
else if (namespace_ == "urn:x-cast:com.google.cast.tp.heartbeat")
{
json_value *p_data = json_parse(msg.payload_utf8().c_str());
std::string type((*p_data)["type"]);
if (type == "PING")
{
msg_Dbg(p_stream, "PING received from the Chromecast");
msgPong(p_stream);
}
else if (type == "PONG")
{
msg_Dbg(p_stream, "PONG received from the Chromecast");
}
else
{
msg_Err(p_stream, "Heartbeat command not supported");
i_ret = -1;
}
json_value_free(p_data);
}
else if (namespace_ == "urn:x-cast:com.google.cast.receiver")
{
json_value *p_data = json_parse(msg.payload_utf8().c_str());
std::string type((*p_data)["type"]);
if (type == "RECEIVER_STATUS")
{
json_value applications = (*p_data)["status"]["applications"];
for (unsigned i = 0; i < applications.u.array.length; ++i)
{
std::string appId(applications[i]["appId"]);
if (appId == APP_ID)
p_sys->appTransportId = std::string(applications[i]["transportId"]);
}
vlc_mutex_locker locker(&p_sys->lock);
if (!p_sys->appTransportId.empty()
&& p_sys->i_status == CHROMECAST_AUTHENTICATED)
{
p_sys->i_status = CHROMECAST_APP_STARTED;
msgConnect(p_stream, p_sys->appTransportId);
msgLoad(p_stream);
p_sys->i_status = CHROMECAST_MEDIA_LOAD_SENT;
vlc_cond_signal(&p_sys->loadCommandCond);
}
}
else
{
msg_Err(p_stream, "Receiver command not supported: %s",
msg.payload_utf8().c_str());
i_ret = -1;
}
json_value_free(p_data);
}
else if (namespace_ == "urn:x-cast:com.google.cast.media")
{
json_value *p_data = json_parse(msg.payload_utf8().c_str());
std::string type((*p_data)["type"]);
if (type == "MEDIA_STATUS")
{
json_value status = (*p_data)["status"];
msg_Dbg(p_stream, "Player state: %s",
status[0]["playerState"].operator const char *());
}
else if (type == "LOAD_FAILED")
{
msg_Err(p_stream, "Media load failed");
atomic_store(&p_sys->ab_error, true);
}
else
{
msg_Err(p_stream, "Media command not supported: %s",
msg.payload_utf8().c_str());
i_ret = -1;
}
json_value_free(p_data);
}
else
{
msg_Err(p_stream, "Unknown namespace: %s", msg.namespace_().c_str());
i_ret = -1;
}
return i_ret;
}
/**
* @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