Commit d99478cb authored by Steve Lhomme's avatar Steve Lhomme Committed by Jean-Baptiste Kempf

chromecast: move the Chromecast communication in a control interface module

Signed-off-by: Jean-Baptiste Kempf's avatarJean-Baptiste Kempf <jb@videolan.org>
parent 80a1eb94
......@@ -85,6 +85,7 @@ $Id$
* console_logger: Logger outputting in the terminal
* croppadd: Crop/Padd image filter
* crystalhd: crystalhd decoder
* ctrl_chromecast: Module to handle communication with Chromecast and redirect input playback
* cvdsub: CVD subtitles decoder
* cvpx_i420: filter to copy from OS X accelerated surfaces to CPU in YUV
* d3d11_surface: Convert D3D11 GPU textures to YUV planes
......
......@@ -80,18 +80,23 @@ SUFFIXES += .proto .pb.cc
%.pb.h %.pb.cc: %.proto
$(PROTOC) --cpp_out=. -I$(srcdir) $<
libstream_out_chromecast_plugin_la_SOURCES = \
libctrl_chromecast_plugin_la_SOURCES = \
stream_out/chromecast/cast_channel.proto stream_out/chromecast/chromecast.h \
stream_out/chromecast/cast.cpp stream_out/chromecast/chromecast_ctrl.cpp \
stream_out/chromecast/chromecast_ctrl.cpp \
misc/webservices/json.h misc/webservices/json.c
nodist_libstream_out_chromecast_plugin_la_SOURCES = stream_out/chromecast/cast_channel.pb.cc
nodist_libctrl_chromecast_plugin_la_SOURCES = stream_out/chromecast/cast_channel.pb.cc
libctrl_chromecast_plugin_la_CPPFLAGS = $(AM_CPPFLAGS) -Istream_out/chromecast $(CHROMECAST_CFLAGS)
libctrl_chromecast_plugin_la_LIBADD = $(CHROMECAST_LIBS) $(SOCKET_LIBS)
CLEANFILES += $(nodist_libctrl_chromecast_plugin_la_SOURCES)
libstream_out_chromecast_plugin_la_SOURCES = stream_out/chromecast/cast.cpp stream_out/chromecast/chromecast.h
libstream_out_chromecast_plugin_la_CPPFLAGS = $(AM_CPPFLAGS) -Istream_out/chromecast $(CHROMECAST_CFLAGS)
libstream_out_chromecast_plugin_la_LIBADD = $(CHROMECAST_LIBS) $(SOCKET_LIBS)
CLEANFILES += $(nodist_libstream_out_chromecast_plugin_la_SOURCES)
if ENABLE_SOUT
if BUILD_CHROMECAST
BUILT_SOURCES += stream_out/chromecast/cast_channel.pb.h
sout_LTLIBRARIES += libstream_out_chromecast_plugin.la
control_LTLIBRARIES += libctrl_chromecast_plugin.la
endif
endif
/*****************************************************************************
* cast.cpp: Chromecast module for vlc
* cast.cpp: Chromecast sout module for vlc
*****************************************************************************
* Copyright © 2014 VideoLAN
* Copyright © 2014-2015 VideoLAN
*
* Authors: Adrien Maglo <magsoft@videolan.org>
* Jean-Baptiste Kempf <jb@videolan.org>
* Steve Lhomme <robux4@videolabs.io>
*
* 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
......@@ -31,17 +32,14 @@
#include "chromecast.h"
#include <errno.h>
#include <vlc_sout.h>
#include <vlc_threads.h>
#include <cassert>
struct sout_stream_sys_t
{
sout_stream_sys_t(intf_sys_t *intf)
: p_out(NULL)
sout_stream_sys_t(intf_sys_t *intf, sout_stream_t *sout)
: p_out(sout)
, p_intf(intf)
{
assert(p_intf != NULL);
......@@ -49,17 +47,15 @@ struct sout_stream_sys_t
~sout_stream_sys_t()
{
delete p_intf;
sout_StreamChainDelete(p_out, p_out);
}
vlc_thread_t chromecastThread;
sout_stream_t *p_out;
sout_stream_t * const p_out;
intf_sys_t * const p_intf;
};
#define HTTP_PORT 8010
#define SOUT_CFG_PREFIX "sout-chromecast-"
/*****************************************************************************
......@@ -69,18 +65,14 @@ static int Open(vlc_object_t *);
static void Close(vlc_object_t *);
static void Clean(sout_stream_t *p_stream);
static void *chromecastThread(void *data);
static const char *const ppsz_sout_options[] = {
"ip", "http-port", "mux", "mime", NULL
"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.")
......@@ -99,7 +91,6 @@ vlc_module_begin ()
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)
......@@ -155,105 +146,32 @@ static int Open(vlc_object_t *p_this)
{
sout_stream_t *p_stream = reinterpret_cast<sout_stream_t*>(p_this);
sout_stream_sys_t *p_sys;
intf_sys_t *p_intf = new(std::nothrow) intf_sys_t(p_stream);
if (p_intf == NULL)
return VLC_ENOMEM;
p_sys = new(std::nothrow) sout_stream_sys_t(p_intf);
if (p_sys == NULL)
{
delete p_intf;
return VLC_ENOMEM;
}
p_stream->p_sys = p_sys;
intf_sys_t *p_intf = NULL;
char *psz_mux = NULL;
sout_stream_t *p_sout = NULL;
std::stringstream ss;
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_stream);
return VLC_EGENERIC;
}
p_intf->i_sock_fd = p_intf->connectChromecast(psz_ipChromecast);
free(psz_ipChromecast);
if (p_intf->i_sock_fd < 0)
{
msg_Err(p_stream, "Could not connect the Chromecast");
Clean(p_stream);
return VLC_EGENERIC;
}
p_intf->setConnectionStatus(CHROMECAST_TLS_CONNECTED);
char psz_localIP[NI_MAXNUMERICHOST];
if (net_GetSockAddress(p_intf->i_sock_fd, psz_localIP, NULL))
{
msg_Err(p_this, "Cannot get local IP address");
Clean(p_stream);
return VLC_EGENERIC;
}
p_intf->serverIP = psz_localIP;
char *psz_mux = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "mux");
if (psz_mux == NULL)
{
Clean(p_stream);
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_stream);
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)
psz_mux = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "mux");
if (psz_mux == NULL || !psz_mux[0])
{
Clean(p_stream);
return VLC_EGENERIC;
goto error;
}
// 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_stream);
return VLC_EGENERIC;
}
ss << "http{dst=:" << var_InheritInteger(p_stream, SOUT_CFG_PREFIX "http-port") << "/stream"
<< ",mux=" << psz_mux
<< ",access=http}";
/* 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_lock(&p_intf->lock);
while (p_intf->getConnectionStatus() != CHROMECAST_MEDIA_LOAD_SENT)
{
i_ret = vlc_cond_timedwait(&p_intf->loadCommandCond, &p_intf->lock, deadline);
if (i_ret == ETIMEDOUT)
{
msg_Err(p_stream, "Timeout reached before sending the media loading command");
vlc_mutex_unlock(&p_intf->lock);
vlc_cancel(p_sys->chromecastThread);
Clean(p_stream);
return VLC_EGENERIC;
}
p_sout = sout_StreamChainNew( p_stream->p_sout, ss.str().c_str(), NULL, NULL);
if (p_sout == NULL) {
msg_Dbg(p_stream, "could not create sout chain:%s", ss.str().c_str());
goto error;
}
vlc_mutex_unlock(&p_intf->lock);
/* Even uglier: sleep more to let to the Chromecast initiate the connection
* to the http server. */
msleep(2 * CLOCK_FREQ);
p_sys = new(std::nothrow) sout_stream_sys_t(p_intf, p_sout);
if (unlikely(p_sys == NULL))
goto error;
// Set the sout callbacks.
p_stream->pf_add = Add;
......@@ -262,9 +180,17 @@ static int Open(vlc_object_t *p_this)
p_stream->pf_flush = Flush;
p_stream->pf_control = Control;
p_stream->p_sys = p_sys;
free(psz_mux);
return VLC_SUCCESS;
}
error:
sout_StreamChainDelete(p_sout, p_sout);
free(psz_mux);
Clean(p_stream);
return VLC_EGENERIC;
}
/*****************************************************************************
* Close: destroy interface
......@@ -272,24 +198,6 @@ static int Open(vlc_object_t *p_this)
static void Close(vlc_object_t *p_this)
{
sout_stream_t *p_stream = reinterpret_cast<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->p_intf->getConnectionStatus())
{
case CHROMECAST_MEDIA_LOAD_SENT:
case CHROMECAST_APP_STARTED:
// Generate the close messages.
p_sys->p_intf->msgReceiverClose(p_sys->p_intf->appTransportId);
// ft
case CHROMECAST_AUTHENTICATED:
p_sys->p_intf->msgReceiverClose(DEFAULT_CHOMECAST_RECEIVER);
// ft
default:
break;
}
Clean(p_stream);
}
......@@ -300,46 +208,6 @@ static void Close(vlc_object_t *p_this)
*/
static void Clean(sout_stream_t *p_stream)
{
sout_stream_sys_t *p_sys = p_stream->p_sys;
if (p_sys->p_out)
{
sout_StreamChainDelete(p_sys->p_out, p_sys->p_out);
}
p_sys->p_intf->disconnectChromecast();
delete p_sys;
delete p_stream->p_sys;
}
/*****************************************************************************
* Chromecast thread
*****************************************************************************/
static void* chromecastThread(void* p_data)
{
int canc = vlc_savecancel();
// Not cancellation-safe part.
sout_stream_t *p_stream = reinterpret_cast<sout_stream_t*>(p_data);
sout_stream_sys_t* p_sys = p_stream->p_sys;
p_sys->p_intf->msgAuth();
vlc_restorecancel(canc);
while (1)
{
p_sys->p_intf->handleMessages();
vlc_mutex_lock(&p_sys->p_intf->lock);
if ( p_sys->p_intf->getConnectionStatus() == CHROMECAST_CONNECTION_DEAD )
{
vlc_mutex_unlock(&p_sys->p_intf->lock);
break;
}
vlc_mutex_unlock(&p_sys->p_intf->lock);
}
return NULL;
}
......@@ -30,10 +30,13 @@
#define VLC_CHROMECAST_H
#include <vlc_common.h>
#include <vlc_interface.h>
#include <vlc_plugin.h>
#include <vlc_sout.h>
#include <vlc_tls.h>
#include <sstream>
#include "cast_channel.pb.h"
#define PACKET_HEADER_LEN 4
......@@ -43,6 +46,7 @@ static const std::string DEFAULT_CHOMECAST_RECEIVER = "receiver-0";
/* see https://developers.google.com/cast/docs/reference/messages */
static const std::string NAMESPACE_MEDIA = "urn:x-cast:com.google.cast.media";
#define HTTP_PORT 8010
// Status
enum connection_status
......@@ -57,10 +61,10 @@ enum connection_status
struct intf_sys_t
{
intf_sys_t(sout_stream_t * const p_stream);
intf_sys_t(intf_thread_t * const intf);
~intf_sys_t();
sout_stream_t * const p_stream;
intf_thread_t * const p_stream;
std::string serverIP;
std::string appTransportId;
......@@ -70,6 +74,7 @@ struct intf_sys_t
vlc_mutex_t lock;
vlc_cond_t loadCommandCond;
vlc_thread_t chromecastThread;
void msgAuth();
void msgReceiverClose(std::string destinationId);
......
......@@ -32,9 +32,11 @@
#include "chromecast.h"
#include <vlc_sout.h>
#include <vlc_playlist.h>
#include <vlc_threads.h>
#include <sstream>
#include <cassert>
#include <cerrno>
#ifdef HAVE_POLL
# include <poll.h>
#endif
......@@ -46,7 +48,7 @@
// Media player Chromecast app id
#define APP_ID "CC1AD845" // Default media player aka DEFAULT_MEDIA_RECEIVER_APPLICATION_ID
#define CHROMECAST_CONTROL_PORT 8009
static const int CHROMECAST_CONTROL_PORT = 8009;
/* deadline regarding pings sent from receiver */
#define PING_WAIT_TIME 6000
......@@ -55,13 +57,176 @@
#define PONG_WAIT_TIME 500
#define PONG_WAIT_RETRIES 2
#define SOUT_CFG_PREFIX "sout-chromecast-"
#define CONTROL_CFG_PREFIX "chromecast-"
static const std::string NAMESPACE_DEVICEAUTH = "urn:x-cast:com.google.cast.tp.deviceauth";
static const std::string NAMESPACE_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
static const std::string NAMESPACE_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
static const std::string NAMESPACE_RECEIVER = "urn:x-cast:com.google.cast.receiver";
/*****************************************************************************
* Local prototypes
*****************************************************************************/
static int Open(vlc_object_t *);
static void Close(vlc_object_t *);
static void Clean(intf_thread_t *);
static void *ChromecastThread(void *data);
/*****************************************************************************
* 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 MUXER_TEXT N_("Muxer")
#define MUXER_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_category( CAT_INTERFACE )
set_subcategory( SUBCAT_INTERFACE_CONTROL )
set_description( N_("Chromecast interface") )
set_capability( "interface", 0 )
add_shortcut("chromecast")
add_string(CONTROL_CFG_PREFIX "addr", "", IP_TEXT, IP_LONGTEXT, false)
add_integer(CONTROL_CFG_PREFIX "http-port", HTTP_PORT, HTTP_PORT_TEXT, HTTP_PORT_LONGTEXT, false)
add_string(CONTROL_CFG_PREFIX "mime", "video/x-matroska", MIME_TEXT, MIME_LONGTEXT, false)
add_string(CONTROL_CFG_PREFIX "mux", "avformat{mux=matroska}", MUXER_TEXT, MUXER_LONGTEXT, false)
set_callbacks( Open, Close )
vlc_module_end ()
/*****************************************************************************
* Open: connect to the Chromecast and initialize the sout
*****************************************************************************/
int Open(vlc_object_t *p_this)
{
intf_thread_t *p_intf = reinterpret_cast<intf_thread_t*>(p_this);
intf_sys_t *p_sys = new(std::nothrow) intf_sys_t(p_intf);
if (unlikely(p_sys == NULL))
return VLC_ENOMEM;
char *psz_ipChromecast = var_InheritString(p_intf, CONTROL_CFG_PREFIX "addr");
if (psz_ipChromecast == NULL)
{
msg_Err(p_intf, "No Chromecast receiver IP provided");
Clean(p_intf);
return VLC_EGENERIC;
}
p_sys->i_sock_fd = p_sys->connectChromecast(psz_ipChromecast);
free(psz_ipChromecast);
if (p_sys->i_sock_fd < 0)
{
msg_Err(p_intf, "Could not connect the Chromecast");
Clean(p_intf);
return VLC_EGENERIC;
}
p_sys->setConnectionStatus(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_intf);
return VLC_EGENERIC;
}
p_sys->serverIP = psz_localIP;
char *psz_mux = var_InheritString(p_intf, CONTROL_CFG_PREFIX "mux");
if (psz_mux == NULL)
{
Clean(p_intf);
return VLC_EGENERIC;
}
// Start the Chromecast event thread.
if (vlc_clone(&p_sys->chromecastThread, ChromecastThread, p_intf,
VLC_THREAD_PRIORITY_LOW))
{
msg_Err(p_intf, "Could not start the Chromecast talking thread");
Clean(p_intf);
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_lock(&p_sys->lock);
while (p_sys->getConnectionStatus() != CHROMECAST_MEDIA_LOAD_SENT)
{
i_ret = vlc_cond_timedwait(&p_sys->loadCommandCond, &p_sys->lock, deadline);
if (i_ret == ETIMEDOUT)
{
msg_Err(p_intf, "Timeout reached before sending the media loading command");
vlc_mutex_unlock(&p_sys->lock);
vlc_cancel(p_sys->chromecastThread);
Clean(p_intf);
return VLC_EGENERIC;
}
}
vlc_mutex_unlock(&p_sys->lock);
/* Even uglier: sleep more to let to the Chromecast initiate the connection
* to the http server. */
msleep(2 * CLOCK_FREQ);
p_intf->p_sys = p_sys;
return VLC_SUCCESS;
}
/*****************************************************************************
* Close: destroy interface
*****************************************************************************/
void Close(vlc_object_t *p_this)
{
intf_thread_t *p_intf = reinterpret_cast<intf_thread_t*>(p_this);
intf_sys_t *p_sys = p_intf->p_sys;
vlc_cancel(p_sys->chromecastThread);
vlc_join(p_sys->chromecastThread, NULL);
switch (p_sys->getConnectionStatus())
{
case CHROMECAST_MEDIA_LOAD_SENT:
case CHROMECAST_APP_STARTED:
// Generate the close messages.
p_sys->msgReceiverClose(p_sys->appTransportId);
// ft
case CHROMECAST_AUTHENTICATED:
p_sys->msgReceiverClose(DEFAULT_CHOMECAST_RECEIVER);
// ft
default:
break;
}
Clean(p_intf);
}
/**
* @brief Clean and release the variables in a sout_stream_sys_t structure
*/
void Clean(intf_thread_t *p_stream)
{
intf_sys_t *p_sys = p_stream->p_sys;
p_sys->disconnectChromecast();
delete p_sys;
}
/**
* @brief Build a CastMessage to send to the Chromecast
* @param namespace_ the message namespace
......@@ -91,7 +256,7 @@ void intf_sys_t::buildMessage(const std::string & namespace_,
sendMessage(msg);
}
intf_sys_t::intf_sys_t(sout_stream_t * const p_this)
intf_sys_t::intf_sys_t(intf_thread_t * const p_this)
: p_stream(p_this)
, p_tls(NULL)
, conn_status(CHROMECAST_DISCONNECTED)
......@@ -163,7 +328,7 @@ void intf_sys_t::disconnectChromecast()
* @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,
extern "C" int recvPacket(vlc_object_t *p_stream, bool &b_msgReceived,
uint32_t &i_payloadSize, int i_sock_fd, vlc_tls_t *p_tls,
unsigned *pi_received, uint8_t *p_data, bool *pb_pingTimeout,
int *pi_wait_delay, int *pi_wait_retries)
......@@ -509,14 +674,14 @@ void intf_sys_t::msgReceiverLaunchApp()
void intf_sys_t::msgPlayerLoad()
{
char *psz_mime = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "mime");
char *psz_mime = var_InheritString(p_stream, CONTROL_CFG_PREFIX "mime");
if (psz_mime == NULL)
return;
std::stringstream ss;
ss << "{\"type\":\"LOAD\","
<< "\"media\":{\"contentId\":\"http://" << serverIP << ":"
<< var_InheritInteger(p_stream, SOUT_CFG_PREFIX"http-port")
<< var_InheritInteger(p_stream, CONTROL_CFG_PREFIX"http-port")
<< "/stream\","
<< "\"streamType\":\"LIVE\","
<< "\"contentType\":\"" << std::string(psz_mime) << "\"},"
......@@ -555,6 +720,31 @@ int intf_sys_t::sendMessage(const castchannel::CastMessage &msg)
return VLC_EGENERIC;
}
/*****************************************************************************
* Chromecast thread
*****************************************************************************/
static void* ChromecastThread(void* p_data)
{
int canc = vlc_savecancel();
// Not cancellation-safe part.
intf_thread_t *p_stream = reinterpret_cast<intf_thread_t*>(p_data);
intf_sys_t *p_sys = p_stream->p_sys;
p_sys->msgAuth();
vlc_restorecancel(canc);
while (1)
{
p_sys->handleMessages();
vlc_mutex_locker locker(&p_sys->lock);
if ( p_sys->getConnectionStatus() == CHROMECAST_CONNECTION_DEAD )
break;
}
return NULL;
}
void intf_sys_t::handleMessages()
{
unsigned i_received = 0;
......@@ -566,7 +756,7 @@ void intf_sys_t::handleMessages()
bool b_msgReceived = false;
uint32_t i_payloadSize = 0;
int i_ret = recvPacket(p_stream, b_msgReceived, i_payloadSize, i_sock_fd,
int i_ret = recvPacket(VLC_OBJECT(p_stream), b_msgReceived, i_payloadSize, i_sock_fd,
p_tls, &i_received, p_packet, &b_pingTimeout,
&i_waitdelay, &i_retries);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment