[vlc-devel] [PATCH 4/4] chromecast: add an experimental sout module that connects to a ChromeCast device and streams using the HTTP access output
Adrien Maglo
magsoft at videolan.org
Thu Aug 28 09:08:35 CEST 2014
Signed-off-by: Jean-Baptiste Kempf <jb at videolan.org>
---
configure.ac | 3 +-
modules/stream_out/Modules.am | 15 +
modules/stream_out/chromecast/cast.cpp | 892 +++++++++++++++++++++++++++++++++
3 files changed, 909 insertions(+), 1 deletion(-)
create mode 100644 modules/stream_out/chromecast/cast.cpp
diff --git a/configure.ac b/configure.ac
index a71055e..7389849 100644
--- a/configure.ac
+++ b/configure.ac
@@ -78,6 +78,7 @@ AM_PROG_AS
AC_ARG_VAR([DESKTOP_FILE_VALIDATE], [Validator for desktop entry files])
AC_CHECK_PROGS(DESKTOP_FILE_VALIDATE, [${DESKTOP_FILE_VALIDATE} desktop-file-validate], :)
AC_CHECK_PROGS(YASM, yasm)
+AC_CHECK_PROGS(PROTOC, protoc)
dnl Check for compiler properties
AC_C_CONST
@@ -637,7 +638,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)
diff --git a/modules/stream_out/Modules.am b/modules/stream_out/Modules.am
index d307c34..bed5889 100644
--- a/modules/stream_out/Modules.am
+++ b/modules/stream_out/Modules.am
@@ -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
+
+.proto.pb.cc:
+ $(PROTOC) --cpp_out=. -I$(srcdir) $<
+
+libstream_out_chromecast_plugin_la_SOURCES = chromecast/cast_channel.proto chromecast/cast.cpp
+nodist_libstream_out_chromecast_plugin_la_SOURCES = chromecast/cast_channel.pb.cc
+libstream_out_chromecast_plugin_la_CPPFLAGS = $(AM_CPPFLAGS)
+libstream_out_chromecast_plugin_la_CXXFLAGS = $(AM_CFLAGS) -I/home/jb/VideoLAN/dev/vlc/build/modules/stream_out/chromecast
+libstream_out_chromecast_plugin_la_LDFLAGS = $(AM_LDFLAGS) -rpath '$(stream_outdir)'
+libstream_out_chromecast_plugin_la_LIBADD = -lprotobuf-lite -ljsoncpp $(LIBS_stream_out_chromecast)
+stream_out_LTLIBRARIES += libstream_out_chromecast_plugin.la
diff --git a/modules/stream_out/chromecast/cast.cpp b/modules/stream_out/chromecast/cast.cpp
new file mode 100644
index 0000000..1bce71d
--- /dev/null
+++ b/modules/stream_out/chromecast/cast.cpp
@@ -0,0 +1,892 @@
+/*****************************************************************************
+ * cast.cpp: Chromecast module for vlc
+ *****************************************************************************
+ * Copyright © 2014 VideoLAN
+ *
+ * Authors: Jean-Baptiste Kempf <jb at videolan.org>
+ * Adrien Maglo <magsoft at 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 <errno.h>
+
+#include <iostream>
+#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 <json/json.h>
+
+using namespace google::protobuf::io;
+using namespace std;
+using namespace castchannel;
+
+// Status
+enum
+{
+ CHROMECAST_DISCONNECTED,
+ CHROMECAST_TLS_CONNECTED,
+ CHROMECAST_AUTHENTICATED,
+ CHROMECAST_APP_STARTED,
+ CHROMECAST_CONNECTION_DEAD,
+};
+
+#define PACKET_MAX_LEN 10 * 1024
+#define PACKET_HEADER_LEN 4
+
+struct sout_stream_sys_t
+{
+ sout_stream_sys_t()
+ : i_received(0), i_status(CHROMECAST_DISCONNECTED),
+ b_threadStarted(false), b_pingSent(false),
+ p_out(NULL)
+ {
+ atomic_init(&ab_error, false);
+ }
+
+ string serverIP;
+
+ int i_sock_fd;
+ vlc_tls_creds_t *p_creds;
+ vlc_tls_t *p_tls;
+
+ vlc_thread_t chromecastThread;
+
+ unsigned i_received;
+ char p_packet[PACKET_MAX_LEN];
+ unsigned i_requestId;
+ string appTransportId;
+
+ queue<CastMessage> messagesToSend;
+
+ int i_status;
+ bool b_threadStarted;
+ bool b_pingSent;
+ atomic_bool ab_error;
+ vlc_mutex_t loadCommandMutex;
+ vlc_cond_t loadCommandCond;
+
+ sout_stream_t *p_out;
+};
+
+// Media player Chromecast app id
+#define APP_ID "CC1AD845" // Default media player
+//#define APP_ID "698A9477"
+#define CHROMECAST_CONTROL_PORT 8009 // Chromecast control port
+
+#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, string destinationId);
+static void msgClose(sout_stream_t *p_stream, string destinationId);
+static void msgLaunch(sout_stream_t *p_stream);
+static void msgLoad(sout_stream_t *p_stream);
+
+static void *chromecastThread(void *data);
+
+static string JsonToString(Json::Value data);
+static Json::Value StringToJson(string str);
+
+static const char *const ppsz_sout_options[] = {
+ "receiver-ip", "sender-ip", "mux", "mime", NULL
+};
+
+/*****************************************************************************
+ * Module descriptor
+ *****************************************************************************/
+
+#define RECEIVER_IP_TEXT N_("Chromecast IP address")
+#define RECEIVER_IP_LONGTEXT N_("This sets the ip adress of the Chromecast receiver.")
+#define SENDER_IP_TEXT N_("Sender IP address")
+#define SENDER_IP_LONGTEXT N_("This sets the ip address that the Chromecast" \
+ "will use to connect to HTTP stream.")
+#define MUX_TEXT N_("Muxer")
+#define MUX_LONGTEXT N_("This sets the muxer used to stream to the Chromecast.")
+#define MIME_TEXT N_("Mime type")
+#define MIME_LONGTEXT N_("This sets the media mine 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 "receiver-ip", "", RECEIVER_IP_TEXT, RECEIVER_IP_LONGTEXT, false)
+ add_string(SOUT_CFG_PREFIX "sender-ip", "", SENDER_IP_TEXT, SENDER_IP_LONGTEXT, false)
+ add_string(SOUT_CFG_PREFIX "mux", "avformat{mux=mp4}", 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(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_ipLocal = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "sender-ip");
+ if (psz_ipLocal == NULL)
+ {
+ msg_Err(p_stream, "No sender IP provided");
+ clean(p_sys);
+ return VLC_EGENERIC;
+ }
+ p_sys->serverIP = psz_ipLocal;
+ free(psz_ipLocal);
+
+ char *psz_ipChromecast = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "receiver-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;
+ }
+
+ char *psz_mux = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "mux");
+ char *psz_chain = NULL;
+ int i_bytes = asprintf(&psz_chain, "http{dst=:8009/stream,mux=%s}", 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->loadCommandMutex);
+ 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;
+ }
+
+ p_sys->b_threadStarted = true;
+
+ /* 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.
+ vlc_mutex_lock(&p_sys->loadCommandMutex);
+ const mtime_t deadline = mdate() + 6000 * 1000;
+ int i_ret = vlc_cond_timedwait(&p_sys->loadCommandCond, &p_sys->loadCommandMutex, deadline);
+ if (i_ret == ETIMEDOUT)
+ {
+ msg_Err(p_stream, "Timeout reached before sending the media loading command");
+ vlc_mutex_unlock(&p_sys->loadCommandMutex);
+ clean(p_sys);
+ return VLC_EGENERIC;
+ }
+ vlc_mutex_unlock(&p_sys->loadCommandMutex);
+
+ /* Even uglier: sleep more to let to the Chromecast initiate the connection
+ * to the http server. */
+ msleep(2000 * 1000);
+
+ // 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;
+
+ if (p_sys->b_threadStarted)
+ {
+ vlc_cancel(p_sys->chromecastThread);
+ vlc_join(p_sys->chromecastThread, NULL);
+ }
+
+ // Generate the close messages.
+ if (p_sys->i_status == CHROMECAST_APP_STARTED)
+ {
+ msgClose(p_stream, p_sys->appTransportId);
+ p_sys->i_status = CHROMECAST_AUTHENTICATED;
+ }
+
+ if (p_sys->i_status == CHROMECAST_AUTHENTICATED)
+ {
+ msgClose(p_stream, "receiver-0");
+ p_sys->i_status = CHROMECAST_TLS_CONNECTED;
+ }
+
+ // Send the just added close messages.
+ sendMessages(p_stream);
+
+ if (p_sys->i_status == CHROMECAST_TLS_CONNECTED)
+ {
+ disconnectChromecast(p_stream);
+ p_sys->i_status = CHROMECAST_DISCONNECTED;
+ }
+
+ 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->loadCommandMutex);
+ 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;
+ }
+
+ p_sys->i_status = CHROMECAST_TLS_CONNECTED;
+ 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, 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[PACKET_HEADER_LEN + i_size];
+ memcpy(p_data, (char *)&i_sizeNetwork, PACKET_HEADER_LEN);
+ msg.SerializeWithCachedSizesToArray((uint8_t *)(p_data + PACKET_HEADER_LEN));
+
+ return tls_Send(p_sys->p_tls, p_data, sizeof(p_data));
+}
+
+
+/**
+ * @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
+ */
+static int recvPacket(sout_stream_t *p_stream, bool &b_msgReceived,
+ uint32_t &i_payloadSize)
+{
+ // Use here only C linkage and POD types as this function is a cancelation point.
+ sout_stream_sys_t *p_sys = p_stream->p_sys;
+ struct pollfd ufd[1];
+ ufd[0].fd = p_sys->i_sock_fd;
+ ufd[0].events = POLLIN;
+
+ mtime_t deadline = mdate();
+ deadline += 6 * 1000 * 1000; // 6 seconds
+
+ mtime_t now = mdate();
+ if (now > deadline)
+ now = deadline;
+
+ /* 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, (deadline - now) / 1000) == 0)
+ {
+ if (p_sys->b_pingSent)
+ {
+ 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");
+ msgPing(p_stream);
+ p_sys->b_pingSent = true;
+ return -1;
+ }
+
+ p_sys->b_pingSent = false;
+ char *p_data = p_sys->p_packet;
+ int i_ret;
+
+ /* Packet structure:
+ * +------------------------------------+------------------------------+
+ * | Payload size (uint32_t big endian) | Payload data |
+ * +------------------------------------+------------------------------+ */
+ if (p_sys->i_received < PACKET_HEADER_LEN)
+ {
+ // We receive the header.
+ i_ret = tls_Recv(p_sys->p_tls, p_data, PACKET_HEADER_LEN - p_sys->i_received);
+ if (i_ret <= 0)
+ return i_ret;
+ p_sys->i_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 - (p_sys->i_received - PACKET_HEADER_LEN);
+ if (i_size > i_maxPayloadSize)
+ i_size = i_maxPayloadSize;
+
+ i_ret = tls_Recv(p_sys->p_tls, p_data + PACKET_HEADER_LEN, i_size);
+ if (i_ret <= 0)
+ return i_ret;
+ p_sys->i_received += i_ret;
+
+ if (p_sys->i_received < i_payloadSize + PACKET_HEADER_LEN)
+ return i_ret;
+
+ p_sys->i_received = 0;
+ return -1;
+ }
+
+ // Normal case
+ i_ret = tls_Recv(p_sys->p_tls, p_data + p_sys->i_received,
+ i_payloadSize - (p_sys->i_received - PACKET_HEADER_LEN));
+ if (i_ret <= 0)
+ return i_ret;
+ p_sys->i_received += i_ret;
+
+ if (p_sys->i_received < i_payloadSize + PACKET_HEADER_LEN)
+ return i_ret;
+
+ assert(p_sys->i_received == i_payloadSize + PACKET_HEADER_LEN);
+ p_sys->i_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, CastMessage &msg)
+{
+ sout_stream_sys_t *p_sys = p_stream->p_sys;
+ string namespace_ = msg.namespace_();
+
+ if (namespace_ == "urn:x-cast:com.google.cast.tp.deviceauth")
+ {
+ DeviceAuthMessage authMessage;
+ authMessage.ParseFromString(msg.payload_binary());
+
+ if (authMessage.has_error())
+ {
+ msg_Err(p_stream, "Authentification error: %d", authMessage.error().error_type());
+ return -1;
+ }
+
+ if (!authMessage.has_response()) {
+ msg_Err(p_stream, "Authentification message has no response field");
+ return -1;
+ }
+
+ 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 data = StringToJson(msg.payload_utf8());
+ string type = data.get("type", "none").asString();
+
+ 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");
+ return -1;
+ }
+ }
+ else if (namespace_ == "urn:x-cast:com.google.cast.receiver")
+ {
+ Json::Value data = StringToJson(msg.payload_utf8());
+ string type = data.get("type", "none").asString();
+
+ if (type == "RECEIVER_STATUS")
+ {
+ Json::Value status = data["status"];
+ Json::Value applications = status["applications"];
+ for (unsigned i = 0; i < applications.size(); ++i)
+ {
+ string appId = applications[i]["appId"].asString();
+ if (appId == APP_ID)
+ p_sys->appTransportId = applications[i]["transportId"].asString();
+ }
+ if (p_sys->appTransportId != ""
+ && p_sys->i_status == CHROMECAST_AUTHENTICATED)
+ {
+ p_sys->i_status = CHROMECAST_APP_STARTED;
+ msgConnect(p_stream, p_sys->appTransportId);
+ msgLoad(p_stream);
+ }
+ }
+ else
+ {
+ msg_Err(p_stream, "Receiver command not supported: %s",
+ msg.payload_utf8().c_str());
+ return -1;
+ }
+ }
+ else if (namespace_ == "urn:x-cast:com.google.cast.media")
+ {
+ Json::Value data = StringToJson(msg.payload_utf8());
+ string type = data.get("type", "none").asString();
+
+ if (type == "MEDIA_STATUS")
+ {
+ Json::Value status = data["status"];
+ msg_Dbg(p_stream, "Player state: %s",
+ status[0u]["playerState"].asString().c_str());
+ }
+ 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());
+ return -1;
+ }
+ }
+ else
+ {
+ msg_Err(p_stream, "Unknown namespace: %s", msg.namespace_().c_str());
+ return -1;
+ }
+
+ return 0;
+}
+
+
+/**
+ * @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
+ */
+static CastMessage buildMessage(string namespace_,
+ castchannel::CastMessage_PayloadType payloadType,
+ string payload, string destinationId = "receiver-0")
+{
+ CastMessage msg;
+
+ msg.set_protocol_version(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 == CastMessage_PayloadType_STRING)
+ msg.set_payload_utf8(payload);
+ else // CastMessage_PayloadType_BINARY
+ msg.set_payload_binary(payload);
+
+ return msg;
+}
+
+
+/*****************************************************************************
+ * String-JSON conversion
+ *****************************************************************************/
+static string JsonToString(Json::Value data)
+{
+ Json::FastWriter writer;
+ return writer.write(data);
+}
+
+
+static Json::Value StringToJson(string str)
+{
+ Json::Reader reader;
+ Json::Value data;
+ reader.parse(str, data);
+ return data;
+}
+
+
+/*****************************************************************************
+ * Message preparation
+ *****************************************************************************/
+static void msgAuth(sout_stream_t *p_stream)
+{
+ sout_stream_sys_t *p_sys = p_stream->p_sys;
+
+ DeviceAuthMessage authMessage;
+ authMessage.mutable_challenge();
+ std::string authMessageString;
+ authMessage.SerializeToString(&authMessageString);
+
+ CastMessage msg = buildMessage("urn:x-cast:com.google.cast.tp.deviceauth",
+ CastMessage_PayloadType_BINARY, authMessageString);
+
+ p_sys->messagesToSend.push(msg);
+}
+
+
+static void msgPing(sout_stream_t *p_stream)
+{
+ sout_stream_sys_t *p_sys = p_stream->p_sys;
+
+ Json::Value data;
+ data["type"] = "PING";
+
+ CastMessage msg = buildMessage("urn:x-cast:com.google.cast.tp.heartbeat",
+ CastMessage_PayloadType_STRING, JsonToString(data));
+
+ p_sys->messagesToSend.push(msg);
+}
+
+
+static void msgPong(sout_stream_t *p_stream)
+{
+ sout_stream_sys_t *p_sys = p_stream->p_sys;
+
+ Json::Value data;
+ data["type"] = "PONG";
+
+ CastMessage msg = buildMessage("urn:x-cast:com.google.cast.tp.heartbeat",
+ CastMessage_PayloadType_STRING, JsonToString(data));
+
+ p_sys->messagesToSend.push(msg);
+}
+
+
+static void msgConnect(sout_stream_t *p_stream, string destinationId)
+{
+ sout_stream_sys_t *p_sys = p_stream->p_sys;
+
+ Json::Value data;
+ data["type"] = "CONNECT";
+
+ CastMessage msg = buildMessage("urn:x-cast:com.google.cast.tp.connection",
+ CastMessage_PayloadType_STRING, JsonToString(data), destinationId);
+
+ p_sys->messagesToSend.push(msg);
+}
+
+
+static void msgClose(sout_stream_t *p_stream, string destinationId)
+{
+ sout_stream_sys_t *p_sys = p_stream->p_sys;
+
+ Json::Value data;
+ data["type"] = "CLOSE";
+
+ CastMessage msg = buildMessage("urn:x-cast:com.google.cast.tp.connection",
+ CastMessage_PayloadType_STRING, JsonToString(data), destinationId);
+
+ p_sys->messagesToSend.push(msg);
+}
+
+
+static void msgLaunch(sout_stream_t *p_stream)
+{
+ sout_stream_sys_t *p_sys = p_stream->p_sys;
+
+ Json::Value data;
+ data["type"] = "LAUNCH";
+ data["appId"] = APP_ID;
+ data["requestId"] = p_stream->p_sys->i_requestId++;
+
+ CastMessage msg = buildMessage("urn:x-cast:com.google.cast.receiver",
+ CastMessage_PayloadType_STRING, JsonToString(data));
+
+ p_sys->messagesToSend.push(msg);
+}
+
+
+static void msgLoad(sout_stream_t *p_stream)
+{
+ sout_stream_sys_t *p_sys = p_stream->p_sys;
+
+ char *psz_mime = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "mime");
+
+ Json::Value data;
+ data["type"] = "LOAD";
+ Json::Value media;
+ media["contentId"] = "http://" + p_sys->serverIP + ":8009/stream";
+ media["streamType"] = "LIVE";
+ media["contentType"] = psz_mime;
+ data["media"] = media;
+ data["requestId"] = p_sys->i_requestId++;
+
+ free(psz_mime);
+
+ CastMessage msg = buildMessage("urn:x-cast:com.google.cast.media",
+ CastMessage_PayloadType_STRING, JsonToString(data),
+ p_sys->appTransportId);
+
+ p_sys->messagesToSend.push(msg);
+
+ // Unlock the sout tread.
+ vlc_mutex_lock(&p_sys->loadCommandMutex);
+ vlc_cond_signal(&p_sys->loadCommandCond);
+ vlc_mutex_unlock(&p_sys->loadCommandMutex);
+}
+
+
+/*****************************************************************************
+ * Chromecast thread
+ *****************************************************************************/
+static void* chromecastThread(void* p_data)
+{
+ sout_stream_t* p_stream = (sout_stream_t*)p_data;
+ sout_stream_sys_t* p_sys = p_stream->p_sys;
+
+ int canc = vlc_savecancel();
+ // Not cancellation-safe part.
+ msgAuth(p_stream);
+ sendMessages(p_stream);
+ vlc_restorecancel(canc);
+
+ while (1)
+ {
+ bool b_msgReceived = false;
+ uint32_t i_payloadSize = 0;
+ int i_ret = recvPacket(p_stream, b_msgReceived, i_payloadSize);
+
+ canc = vlc_savecancel();
+ // Not cancellation-safe part.
+
+ if ((i_ret < 0 && errno != EAGAIN) || i_ret == 0)
+ {
+ msg_Err(p_stream, "The connection to the Chromecast died.");
+ p_sys->i_status = CHROMECAST_CONNECTION_DEAD;
+ atomic_store(&p_sys->ab_error, true);
+ break;
+ }
+
+ if (b_msgReceived)
+ {
+ CastMessage msg;
+ msg.ParseFromArray(p_sys->p_packet + PACKET_HEADER_LEN, i_payloadSize);
+ processMessage(p_stream, msg);
+ // Send the answer messages if there is any.
+ if (!p_sys->messagesToSend.empty())
+ {
+ i_ret = sendMessages(p_stream);
+ if ((i_ret < 0 && errno != EAGAIN) || i_ret == 0)
+ {
+ msg_Err(p_stream, "The connection to the Chromecast died.");
+ p_sys->i_status = CHROMECAST_CONNECTION_DEAD;
+ atomic_store(&p_sys->ab_error, true);
+ break;
+ }
+ }
+ }
+ vlc_restorecancel(canc);
+ }
+
+ return NULL;
+}
--
1.9.1
More information about the vlc-devel
mailing list