/***************************************************************************** * cast.cpp: Chromecast sout module for vlc ***************************************************************************** * Copyright © 2014-2015 VideoLAN * * Authors: Adrien Maglo * Jean-Baptiste Kempf * Steve Lhomme * * 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 #include "chromecast.h" #include #include #include #include struct sout_stream_sys_t { sout_stream_sys_t(intf_sys_t * const intf, bool has_video, int port, const char *psz_default_muxer, const char *psz_default_mime) : p_out(NULL) , default_muxer(psz_default_muxer) , default_mime(psz_default_mime) , p_intf(intf) , b_supports_video(has_video) , i_port(port) , es_changed( true ) , transcode_attempt_idx( 0 ) { assert(p_intf != NULL); } ~sout_stream_sys_t() { sout_StreamChainDelete(p_out, NULL); delete p_intf; } bool canDecodeVideo( vlc_fourcc_t i_codec ) const; bool canDecodeAudio( sout_stream_t* p_stream, vlc_fourcc_t i_codec, const audio_format_t* p_fmt ) const; bool startSoutChain(sout_stream_t* p_stream); sout_stream_t *p_out; std::string sout; const std::string default_muxer; const std::string default_mime; intf_sys_t * const p_intf; const bool b_supports_video; const int i_port; sout_stream_id_sys_t *GetSubId( sout_stream_t*, sout_stream_id_sys_t* ); bool es_changed; std::vector streams; bool stream_started; unsigned int transcode_attempt_idx; private: bool UpdateOutput( sout_stream_t * ); vlc_fourcc_t transcodeAudioFourCC( sout_stream_t* p_stream, const audio_format_t* p_fmt ); }; #define SOUT_CFG_PREFIX "sout-chromecast-" static const vlc_fourcc_t DEFAULT_TRANSCODE_VIDEO = VLC_CODEC_H264; static const unsigned int MAX_TRANSCODE_PASS = 3; static const char DEFAULT_MUXER[] = "avformat{mux=matroska,options={live=1}}}"; /***************************************************************************** * Local prototypes *****************************************************************************/ static int Open(vlc_object_t *); static void Close(vlc_object_t *); static const char *const ppsz_sout_options[] = { "ip", "port", "http-port", "mux", "mime", "video", NULL }; /***************************************************************************** * Module descriptor *****************************************************************************/ #define HTTP_PORT_TEXT N_("HTTP port") #define HTTP_PORT_LONGTEXT N_("This sets the HTTP port of the local server " \ "used to stream the media to the Chromecast.") #define HAS_VIDEO_TEXT N_("Video") #define HAS_VIDEO_LONGTEXT N_("The Chromecast receiver can receive video.") #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.") #define PERF_TEXT N_( "Performance warning" ) #define PERF_LONGTEXT N_( "Display a performance warning when transcoding" ) #define AUDIO_PASSTHROUGH_TEXT N_( "Enable Audio passthrough" ) #define AUDIO_PASSTHROUGH_LONGTEXT N_( "Disable if your receiver does not support Dolby®" ) #define MULTICHANNEL_PCM_TEXT N_( "Multichannel PCM" ) #define MULTICHANNEL_PCM_LONGTEXT N_( "Use PCM for multichannel audio." ) #define IP_ADDR_TEXT N_("IP Address") #define IP_ADDR_LONGTEXT N_("IP Address of the Chromecast.") #define PORT_TEXT N_("Chromecast port") #define PORT_LONGTEXT N_("The port used to talk 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", NULL, IP_ADDR_TEXT, IP_ADDR_LONGTEXT, false) add_integer(SOUT_CFG_PREFIX "port", CHROMECAST_CONTROL_PORT, PORT_TEXT, PORT_LONGTEXT, false) add_integer(SOUT_CFG_PREFIX "http-port", HTTP_PORT, HTTP_PORT_TEXT, HTTP_PORT_LONGTEXT, false) add_bool(SOUT_CFG_PREFIX "video", true, HAS_VIDEO_TEXT, HAS_VIDEO_LONGTEXT, false) add_string(SOUT_CFG_PREFIX "mux", DEFAULT_MUXER, MUX_TEXT, MUX_LONGTEXT, false) add_string(SOUT_CFG_PREFIX "mime", "video/x-matroska", MIME_TEXT, MIME_LONGTEXT, false) add_integer(SOUT_CFG_PREFIX "show-perf-warning", 1, PERF_TEXT, PERF_LONGTEXT, true ) add_bool(SOUT_CFG_PREFIX "audio-passthrough", true, AUDIO_PASSTHROUGH_TEXT, AUDIO_PASSTHROUGH_LONGTEXT, false ) add_bool(SOUT_CFG_PREFIX "multichannel-pcm", true, MULTICHANNEL_PCM_TEXT, MULTICHANNEL_PCM_LONGTEXT, false ); vlc_module_end () struct sout_stream_id_sys_t { es_format_t fmt; sout_stream_id_sys_t *p_sub_id; }; /***************************************************************************** * Sout callbacks *****************************************************************************/ static sout_stream_id_sys_t *Add(sout_stream_t *p_stream, const es_format_t *p_fmt) { sout_stream_sys_t *p_sys = p_stream->p_sys; if (!p_sys->b_supports_video) { if (p_fmt->i_cat != AUDIO_ES) return NULL; } sout_stream_id_sys_t *p_sys_id = (sout_stream_id_sys_t *)malloc( sizeof(sout_stream_id_sys_t) ); if (p_sys_id != NULL) { es_format_Copy( &p_sys_id->fmt, p_fmt ); p_sys_id->p_sub_id = NULL; p_sys->streams.push_back( p_sys_id ); p_sys->es_changed = true; } return p_sys_id; } static void Del(sout_stream_t *p_stream, sout_stream_id_sys_t *id) { sout_stream_sys_t *p_sys = p_stream->p_sys; for (size_t i=0; istreams.size(); i++) { if ( p_sys->streams[i] == id ) { if ( p_sys->streams[i]->p_sub_id != NULL ) sout_StreamIdDel( p_sys->p_out, p_sys->streams[i]->p_sub_id ); es_format_Clean( &p_sys->streams[i]->fmt ); free( p_sys->streams[i] ); p_sys->streams.erase( p_sys->streams.begin() + i ); p_sys->es_changed = true; break; } } if ( p_sys->streams.empty() ) { p_sys->p_intf->requestPlayerStop(); sout_StreamChainDelete( p_sys->p_out, NULL ); p_sys->p_out = NULL; p_sys->sout = ""; p_sys->stream_started = false; p_sys->transcode_attempt_idx = 0; } } /** * Transcode steps: * 0: Accept HEVC/VP9 & all supported audio formats * 1: Transcode to h264 & accept all supported audio formats if the video codec * was HEVC/VP9 * 2: Transcode to H264 & MP3 * * Additionally: * - Allow (E)AC3 passthrough depending on the audio-passthrough * config value, except for the final step, where we just give up and transcode * everything. * - Disallow multichannel AAC * * Supported formats: https://developers.google.com/cast/docs/media */ bool sout_stream_sys_t::canDecodeVideo( vlc_fourcc_t i_codec ) const { if ( transcode_attempt_idx == MAX_TRANSCODE_PASS - 1 ) return false; if ( i_codec == VLC_CODEC_HEVC || i_codec == VLC_CODEC_VP9 ) return transcode_attempt_idx == 0; return i_codec == VLC_CODEC_H264 || i_codec == VLC_CODEC_VP8; } bool sout_stream_sys_t::canDecodeAudio( sout_stream_t *p_stream, vlc_fourcc_t i_codec, const audio_format_t* p_fmt ) const { if ( transcode_attempt_idx == MAX_TRANSCODE_PASS - 1 ) return false; if ( i_codec == VLC_CODEC_A52 || i_codec == VLC_CODEC_EAC3 ) { return var_InheritBool( p_stream, SOUT_CFG_PREFIX "audio-passthrough" ); } if ( i_codec == VLC_FOURCC('h', 'a', 'a', 'c') || i_codec == VLC_FOURCC('l', 'a', 'a', 'c') || i_codec == VLC_FOURCC('s', 'a', 'a', 'c') || i_codec == VLC_CODEC_MP4A ) { return p_fmt->i_channels <= 2; } return i_codec == VLC_CODEC_VORBIS || i_codec == VLC_CODEC_OPUS || i_codec == VLC_CODEC_MP3; } vlc_fourcc_t sout_stream_sys_t::transcodeAudioFourCC( sout_stream_t *p_stream, const audio_format_t* p_fmt ) { if ( p_fmt->i_channels > 2 && var_InheritBool( p_stream, SOUT_CFG_PREFIX "multichannel-pcm" ) ) return VLC_CODEC_S16L; return VLC_CODEC_MP3; } bool sout_stream_sys_t::startSoutChain( sout_stream_t *p_stream ) { if ( unlikely( p_out != NULL ) ) { for ( size_t i = 0; i < streams.size(); i++ ) { if ( streams[i]->p_sub_id != NULL ) sout_StreamIdDel( p_out, streams[i]->p_sub_id ); } sout_StreamChainDelete( p_out, NULL ); } msg_Dbg( p_stream, "Creating chain %s", sout.c_str() ); p_out = sout_StreamChainNew( p_stream->p_sout, sout.c_str(), NULL, NULL); if (p_out == NULL) { msg_Dbg(p_stream, "could not create sout chain:%s", sout.c_str()); return false; } /* check the streams we can actually add */ for (std::vector::iterator it = streams.begin(); it != streams.end(); ) { sout_stream_id_sys_t *p_sys_id = *it; p_sys_id->p_sub_id = sout_StreamIdAdd( p_out, &p_sys_id->fmt ); if ( p_sys_id->p_sub_id == NULL ) { msg_Err( p_stream, "can't handle %4.4s stream", (char *)&p_sys_id->fmt.i_codec ); es_format_Clean( &p_sys_id->fmt ); free( p_sys_id ); it = streams.erase( it ); } else ++it; } return streams.empty() == false; } bool sout_stream_sys_t::UpdateOutput( sout_stream_t *p_stream ) { assert( p_stream->p_sys == this ); if ( !es_changed ) return true; es_changed = false; bool canRemux = true; vlc_fourcc_t i_codec_video = 0, i_codec_audio = 0; const es_format_t *p_original_audio = NULL; const es_format_t *p_original_video = NULL; for (std::vector::iterator it = streams.begin(); it != streams.end(); ++it) { const es_format_t *p_es = &(*it)->fmt; if (p_es->i_cat == AUDIO_ES && p_original_audio == NULL) { if ( !canDecodeAudio( p_stream, p_es->i_codec, &p_es->audio ) ) { msg_Dbg( p_stream, "can't remux audio track %d codec %4.4s", p_es->i_id, (const char*)&p_es->i_codec ); canRemux = false; } else if (i_codec_audio == 0) i_codec_audio = p_es->i_codec; p_original_audio = p_es; } else if (b_supports_video && p_es->i_cat == VIDEO_ES && p_original_video == NULL ) { if (!canDecodeVideo( p_es->i_codec )) { msg_Dbg( p_stream, "can't remux video track %d codec %4.4s", p_es->i_id, (const char*)&p_es->i_codec ); canRemux = false; } else if (i_codec_video == 0) i_codec_video = p_es->i_codec; p_original_video = p_es; } } if ( transcode_attempt_idx == 1 && p_original_video != NULL && ( p_original_video->i_codec != VLC_CODEC_HEVC && p_original_video->i_codec != VLC_CODEC_VP9 ) ) { msg_Dbg( p_stream, "Video format wasn't HEVC/VP9; skipping 2nd step and" " transcoding to h264/mp3" ); transcode_attempt_idx++; } std::stringstream ssout; if ( !canRemux ) { if ( var_InheritInteger( p_stream, SOUT_CFG_PREFIX "show-perf-warning" ) ) { int res = vlc_dialog_wait_question( p_stream, VLC_DIALOG_QUESTION_WARNING, _("Cancel"), _("OK"), _("Ok, Don't warn me again"), _("Performance warning"), _("Casting this video requires conversion. " "This conversion can use all the available power and " "could quickly drain your battery." ) ); if ( res <= 0 ) return false; if ( res == 2 ) config_PutInt(p_stream, SOUT_CFG_PREFIX "show-perf-warning", 0 ); } /* TODO: provide audio samplerate and channels */ ssout << "transcode{"; char s_fourcc[5]; if ( i_codec_audio == 0 && p_original_audio ) { i_codec_audio = transcodeAudioFourCC( p_stream, &p_original_audio->audio ); msg_Dbg( p_stream, "Converting audio to %.4s", (const char*)&i_codec_audio ); ssout << "acodec="; vlc_fourcc_to_char( i_codec_audio, s_fourcc ); s_fourcc[4] = '\0'; ssout << s_fourcc << ','; } if ( b_supports_video && i_codec_video == 0 ) { i_codec_video = DEFAULT_TRANSCODE_VIDEO; msg_Dbg( p_stream, "Converting video to %.4s", (const char*)&i_codec_video ); /* TODO: provide maxwidth,maxheight */ ssout << "vcodec="; vlc_fourcc_to_char( i_codec_video, s_fourcc ); s_fourcc[4] = '\0'; ssout << s_fourcc; } ssout << "}:"; } std::string mime; if ( !b_supports_video && default_muxer == DEFAULT_MUXER ) mime = "audio/x-matroska"; else if ( i_codec_audio == VLC_CODEC_VORBIS && i_codec_video == VLC_CODEC_VP8 && default_muxer == DEFAULT_MUXER ) mime = "video/webm"; else mime = default_mime; ssout << "http{dst=:" << i_port << "/stream" << ",mux=" << default_muxer << ",access=http{mime=" << mime << "}}"; sout = ssout.str(); if ( startSoutChain( p_stream ) ) { /* tell the chromecast to load the content */ p_intf->setHasInput( mime ); stream_started = false; } else { p_intf->requestPlayerStop(); sout_StreamChainDelete( p_out, NULL ); p_out = NULL; sout = ""; } return true; } sout_stream_id_sys_t *sout_stream_sys_t::GetSubId( sout_stream_t *p_stream, sout_stream_id_sys_t *id ) { size_t i; assert( p_stream->p_sys == this ); if ( UpdateOutput( p_stream ) == false ) return NULL; for (i = 0; i < streams.size(); ++i) { if ( id == (sout_stream_id_sys_t*) streams[i] ) return streams[i]->p_sub_id; } msg_Err( p_stream, "unknown stream ID" ); return NULL; } 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; id = p_sys->GetSubId( p_stream, id ); if ( id == NULL ) { block_Release( p_buffer ); return VLC_EGENERIC; } /** * Check if the chromecast refused to handle the current configuration. * Once the State switched to started, we know it has been accepted and don't * need to monitor state changes anymore. */ if ( p_sys->stream_started == false ) { States s = p_sys->p_intf->state(); if ( s == LoadFailed && p_sys->es_changed == false ) { if ( p_sys->transcode_attempt_idx > MAX_TRANSCODE_PASS - 1 ) { msg_Err( p_stream, "All attempts failed. Giving up." ); block_Release( p_buffer ); return VLC_EGENERIC; } p_sys->transcode_attempt_idx++; p_sys->es_changed = true; msg_Dbg( p_stream, "Load failed detected. Switching to next " "configuration index: %u", p_sys->transcode_attempt_idx ); } else if ( s == Playing || s == Paused ) { msg_Dbg( p_stream, "Playback started: Current configuration (%u) " "accepted", p_sys->transcode_attempt_idx ); p_sys->stream_started = true; } } return sout_StreamIdSend(p_sys->p_out, id, p_buffer); } static void Flush( sout_stream_t *p_stream, sout_stream_id_sys_t *id ) { sout_stream_sys_t *p_sys = p_stream->p_sys; id = p_sys->GetSubId( p_stream, id ); if ( id == NULL ) return; /* a seek on the Chromecast flushes its buffers */ p_sys->p_intf->requestPlayerSeek( VLC_TS_INVALID ); sout_StreamFlush( p_sys->p_out, id ); } static int Control(sout_stream_t *p_stream, int i_query, va_list args) { sout_stream_sys_t *p_sys = p_stream->p_sys; if (i_query == SOUT_STREAM_EMPTY) { bool *b = va_arg( args, bool * ); /* check if the Chromecast to be done playing */ *b = p_sys->p_intf == NULL || p_sys->p_intf->isFinishedPlaying(); return VLC_SUCCESS; } if ( !p_sys->p_out->pf_control ) return VLC_EGENERIC; return p_sys->p_out->pf_control( p_sys->p_out, i_query, args ); } /***************************************************************************** * Open: connect to the Chromecast and initialize the sout *****************************************************************************/ static int Open(vlc_object_t *p_this) { sout_stream_t *p_stream = reinterpret_cast(p_this); sout_stream_sys_t *p_sys = NULL; intf_sys_t *p_intf = NULL; char *psz_ip = NULL; char *psz_mux = NULL; char *psz_var_mime = NULL; sout_stream_t *p_sout = NULL; bool b_supports_video = true; int i_local_server_port; int i_device_port; std::stringstream ss; vlc_interrupt_t *p_interrupt = vlc_interrupt_create(); if (unlikely(p_interrupt == NULL)) goto error; config_ChainParse(p_stream, SOUT_CFG_PREFIX, ppsz_sout_options, p_stream->p_cfg); psz_ip = var_GetNonEmptyString( p_stream, SOUT_CFG_PREFIX "ip"); if ( psz_ip == NULL ) { msg_Err( p_this, "missing Chromecast IP address" ); goto error; } i_device_port = var_InheritInteger(p_stream, SOUT_CFG_PREFIX "port"); i_local_server_port = var_InheritInteger(p_stream, SOUT_CFG_PREFIX "http-port"); try { p_intf = new intf_sys_t( p_this, i_local_server_port, psz_ip, i_device_port, p_interrupt ); } catch (const std::runtime_error& err ) { msg_Err( p_this, "cannot load the Chromecast controller (%s)", err.what() ); goto error; } catch (const std::bad_alloc& ) { p_intf = NULL; goto error; } p_interrupt = NULL; psz_mux = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "mux"); if (psz_mux == NULL) { goto error; } psz_var_mime = var_GetNonEmptyString(p_stream, SOUT_CFG_PREFIX "mime"); if (psz_var_mime == NULL) goto error; /* check if we can open the proper sout */ ss << "http{dst=:" << i_local_server_port << "/stream" << ",mux=" << psz_mux << ",access=http{mime=" << psz_var_mime << "}}"; 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; } sout_StreamChainDelete( p_sout, NULL ); b_supports_video = var_GetBool(p_stream, SOUT_CFG_PREFIX "video"); p_sys = new(std::nothrow) sout_stream_sys_t( p_intf, b_supports_video, i_local_server_port, psz_mux, psz_var_mime ); if (unlikely(p_sys == NULL)) goto error; // Set the sout callbacks. p_stream->pf_add = Add; p_stream->pf_del = Del; p_stream->pf_send = Send; p_stream->pf_flush = Flush; p_stream->pf_control = Control; p_stream->p_sys = p_sys; free(psz_ip); free(psz_mux); free(psz_var_mime); return VLC_SUCCESS; error: if (p_interrupt) vlc_interrupt_destroy(p_interrupt); delete p_intf; free(psz_ip); free(psz_mux); free(psz_var_mime); delete p_sys; return VLC_EGENERIC; } /***************************************************************************** * Close: destroy interface *****************************************************************************/ static void Close(vlc_object_t *p_this) { sout_stream_t *p_stream = reinterpret_cast(p_this); delete p_stream->p_sys; }