2022-01-15 12:24:49 +00:00
/*
* Copyright ( C ) 2022 Ray < whatdoineed2do @ gmail . com >
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 2 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 General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program ; if not , write to the Free Software
* Foundation , Inc . , 59 Temple Place , Suite 330 , Boston , MA 02111 - 1307 USA
*/
# ifdef HAVE_CONFIG_H
# include <config.h>
# endif
# include <sys/types.h>
# include <sys/socket.h>
# include <arpa/inet.h>
# include <net/if.h>
# include <netinet/in.h>
# include <ifaddrs.h>
# include <unistd.h>
# include <fcntl.h>
# include <stdio.h>
# include <stdlib.h>
# include <stdbool.h>
# include <unistd.h>
# include <string.h>
# include <errno.h>
# include <stdint.h>
# include <inttypes.h>
# include <assert.h>
# include "misc.h"
# include "conffile.h"
# include "logger.h"
# include "player.h"
# include "outputs.h"
# include "mdns.h"
/* RCP is the Roku Soundbridge control protocol; we can request a Soundbridge to
* play data from our internal . mp3 stream and the Soundbridge will act as an
* output
*
* References to the RCP spec are related to the Roku Functional Specification
* dated 09 - Aug - 2007 , document version 2.4 and software versions SoundBridge 3.0 .44
*
* RCP spec page 7 - Overview , What is RCP ?
* [ . . . ] the Roku Control Protocol ( RCP ) . RCP is a control
* protocol implemented by the Roku SoundBridge line of digital audio players with
* software version 2.3 or later , and the Roku Wi - Fi Media Module ( WMM ) , a drop -
* in hardware solution for implementing digital audio functionality targeted for
* OEMs . Remote applications can use RCP to access the digital - media
* functionality of those device to automate repetitive tasks , initiate and control
* media playback , or extend the user interface to other network - connected
* devices .
*
* RCP spec page 10 - Protocol Summary
* RCP was designed with simplicity and completeness as primary requirements .
* Commands and results are exchanged as short transmissions across a high -
* speed interface like a serial port , telnet connection , or parallel interface . Each
* command is composed of a short ASCII command id string , generally just zero or
* one parameters , and the two - byte terminator CRLF . All command results from
* the RCP host are composed of the command - id of the client command that
* caused this result followed by a result string and the two - byte CRLF terminator
*
* . . .
*
* RCP commands can be loosely categorized by the way in which they execute :
* synchronous commands , transacted commands , and subscription commands .
* Synchronous commands return their results immediately , and do not block the
* host device during execution . Transacted commands are commands that might
* require a long extent of time to complete , some as long as ten seconds or more !
* These commands generally depend on sending and receiving data from the
* network , which is why their completion time is non - deterministic . Transacted
* commands run asynchronously , or “ in the background , ” in an RCP session , and
* allow the client to issue other commands or cancel the command while it is in
* process .
*
* Communcations within this module only use the sync subset of commands from
* the RCP spec
*
* RCP / Roku devices only support ipv4
*/
enum rcp_state
{
RCP_STATE_SETUP , // 0
RCP_STATE_SETUP_WAKEUP ,
RCP_STATE_SETUP_GET_CONNECTED_SERVER ,
RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_INIT ,
RCP_STATE_SETUP_SERVER_DISCONNECT_DISCONNECTED ,
RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_END , // 5
RCP_STATE_SETUP_SERVER_DISCONNECT ,
RCP_STATE_SETUP_SET_SERVER_FILTER ,
RCP_STATE_SETUP_LIST_SERVERS_RESULT_SIZE ,
RCP_STATE_SETUP_LIST_SERVERS_INTERNET_RADIO ,
RCP_STATE_SETUP_LIST_SERVERS_RESULTS_END , // 10
RCP_STATE_SETUP_LIST_SERVERS ,
RCP_STATE_SETUP_SERVER_CONNECT_TRANS_INIT ,
RCP_STATE_SETUP_SERVER_CONNECT_CONNECTED ,
RCP_STATE_SETUP_SERVER_CONNECT_TRANS_END ,
RCP_STATE_SETUP_SERVER_CONNECT , // 15
2022-05-08 13:09:59 +01:00
RCP_STATE_SETUP_VOL_SET ,
2022-01-15 12:24:49 +00:00
RCP_STATE_QUEUING_CLEAR ,
RCP_STATE_QUEUING_SET_TITLE ,
RCP_STATE_QUEUING_SET_PLAYLIST_URL ,
RCP_STATE_QUEUING_SET_REMOTE_STREAM , // 20
RCP_STATE_QUEUING_PLAY ,
RCP_STATE_STREAMING ,
RCP_STATE_VOL_GET ,
RCP_STATE_VOL_SET ,
RCP_STATE_STOPPING , // 25
RCP_STATE_SHUTDOWN_STOPPED ,
RCP_STATE_SHUTDOWN_GET_CONNECTED_SERVER ,
RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_INIT ,
RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_DISCONNECTED ,
RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_END , // 30
RCP_STATE_SHUTDOWN_SERVER_DISCONNECT ,
// grouped order
RCP_STATE_STANDBY ,
RCP_STATE_DISCONNECTED ,
RCP_STATE_FAILED , // 35
RCP_STATE_MAX
} ;
struct rcp_state_map
{
enum rcp_state state ;
char * cmd ;
bool has_arg ;
} ;
// direct mapping to cmds against state, if applicable
static const struct rcp_state_map rcp_state_send_map [ ] =
{
{ RCP_STATE_SETUP , NULL } ,
{ RCP_STATE_SETUP_WAKEUP , " SetPowerState on no " } ,
{ RCP_STATE_SETUP_GET_CONNECTED_SERVER , " GetConnectedServer " } ,
{ RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_INIT , " ServerDisconnect " } ,
{ RCP_STATE_SETUP_SERVER_DISCONNECT_DISCONNECTED , NULL } ,
{ RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_END , NULL } ,
{ RCP_STATE_SETUP_SERVER_DISCONNECT , NULL } ,
{ RCP_STATE_SETUP_SET_SERVER_FILTER , " SetServerFilter radio " } ,
{ RCP_STATE_SETUP_LIST_SERVERS_RESULT_SIZE , " ListServers " } ,
{ RCP_STATE_SETUP_LIST_SERVERS_INTERNET_RADIO , NULL } ,
{ RCP_STATE_SETUP_LIST_SERVERS_RESULTS_END , NULL } ,
{ RCP_STATE_SETUP_LIST_SERVERS , NULL } ,
{ RCP_STATE_SETUP_SERVER_CONNECT_TRANS_INIT , " ServerConnect 0 " } ,
{ RCP_STATE_SETUP_SERVER_CONNECT_CONNECTED , NULL } ,
{ RCP_STATE_SETUP_SERVER_CONNECT_TRANS_END , NULL } ,
{ RCP_STATE_SETUP_SERVER_CONNECT , NULL } ,
2022-05-08 13:09:59 +01:00
{ RCP_STATE_SETUP_VOL_SET , " SetVolume " , true } ,
2022-01-15 12:24:49 +00:00
{ RCP_STATE_QUEUING_CLEAR , " ClearWorkingSong " } ,
{ RCP_STATE_QUEUING_SET_TITLE , " SetWorkingSongInfo title " , true } ,
{ RCP_STATE_QUEUING_SET_PLAYLIST_URL , " SetWorkingSongInfo playlistURL " , true } , // set from session's own url
{ RCP_STATE_QUEUING_SET_REMOTE_STREAM , " SetWorkingSongInfo remoteStream 1 " } ,
{ RCP_STATE_QUEUING_PLAY , " QueueAndPlayOne working " } ,
{ RCP_STATE_STREAMING , NULL } ,
{ RCP_STATE_VOL_GET , " GetVolume " } ,
{ RCP_STATE_VOL_SET , " SetVolume " , true } ,
{ RCP_STATE_STOPPING , NULL } ,
{ RCP_STATE_SHUTDOWN_STOPPED , " Stop " } ,
{ RCP_STATE_SHUTDOWN_GET_CONNECTED_SERVER , " GetConnectedServer " } ,
{ RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_INIT , " ServerDisconnect " } ,
{ RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_DISCONNECTED , NULL } ,
{ RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_END , NULL } ,
{ RCP_STATE_SHUTDOWN_SERVER_DISCONNECT , NULL } ,
{ RCP_STATE_STANDBY , " SetPowerState standby " } ,
{ RCP_STATE_DISCONNECTED , NULL } ,
{ RCP_STATE_FAILED , " Reboot " } ,
{ RCP_STATE_MAX , NULL } ,
} ;
struct rcp_session
{
// enum output_device_state state;
enum rcp_state state ;
int callback_id ;
char * devname ;
char * address ;
unsigned short port ;
int sock ;
char * stream_url ; // usues ip4 addr that the Roku believes we're on
bool clear_on_close ;
unsigned close_timeout ;
// the rcp cmds are limited length - used to build response
# define RCP_RESP_BUF_SIZE 256
char respbuf [ RCP_RESP_BUF_SIZE + 1 ] ;
// pointer into our buffer, next available location for data resp
char * respptr ;
// 0..100 incl
unsigned short volume ;
struct output_device * device ;
struct event * ev ;
struct event * reply_timeout ;
struct rcp_session * next ;
} ;
static struct rcp_session * rcp_sessions ;
/* From player.c */
extern struct event_base * evbase_player ;
// fwd
static int rcp_send ( struct rcp_session * s , enum rcp_state next_state , const char * cmd ) ;
/* ---------------------------- STATE MACHINE ------------------------------- */
/* Uses current state to determine valid response; returns
* - 1 invalid req - resp reply
* 0 valid req - resp reply
* 1 valid req - resp reply but failed request
*/
static int
rcp_state_verify ( struct rcp_session * s , const char * resp )
{
int ret ;
switch ( s - > state )
{
case RCP_STATE_SETUP :
if ( strcmp ( resp , " roku: ready \r \n " ) = = 0 )
return 0 ;
goto resp_err ;
case RCP_STATE_SHUTDOWN_GET_CONNECTED_SERVER :
case RCP_STATE_SETUP_GET_CONNECTED_SERVER :
if ( strcmp ( resp , " GetConnectedServer: OK \r \n " ) = = 0 | | strcmp ( resp , " GetConnectedServer: GenericError \r \n " ) = = 0 )
return 0 ;
goto resp_err ;
case RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_INIT :
case RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_INIT :
if ( strcmp ( resp , " ServerDisconnect: TransactionInitiated \r \n " ) = = 0 )
{
if ( s - > state = = RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_INIT )
s - > state = RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_DISCONNECTED ;
else
s - > state = RCP_STATE_SETUP_SERVER_DISCONNECT_DISCONNECTED ;
return 0 ;
}
// roku doesnt think its connected, no other resp for this state
// seen that it goes directly into ErrorDisconnected without the xact init
if ( strcmp ( resp , " ServerDisconnect: ErrorDisconnected \r \n " ) = = 0 )
{
if ( s - > state = = RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_INIT )
s - > state = RCP_STATE_SHUTDOWN_SERVER_DISCONNECT ;
else
s - > state = RCP_STATE_SETUP_SERVER_DISCONNECT ;
return 0 ;
}
if ( strcmp ( resp , " ServerDisconnect: ResourceAllocationError \r \n " ) = = 0 )
{
// this state seems like a lockup on the roku, only clearable rebooting
rcp_send ( s , RCP_STATE_FAILED , NULL ) ;
}
if ( strcmp ( resp , " ServerDisconnect: GenericError \r \n " ) = = 0 )
{
s - > state = RCP_STATE_FAILED ;
return - 1 ;
}
goto resp_err ;
case RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_DISCONNECTED :
case RCP_STATE_SETUP_SERVER_DISCONNECT_DISCONNECTED :
if ( strcmp ( resp , " ServerDisconnect: Disconnected \r \n " ) = = 0 | |
strcmp ( resp , " ServerDisconnect: ErrorDisconnected \r \n " ) = = 0 )
{
if ( s - > state = = RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_DISCONNECTED )
s - > state = RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_END ;
else
s - > state = RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_END ;
return 0 ;
}
// drop through .. reported directly xact complete after init
case RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_END :
case RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_END :
if ( strcmp ( resp , " ServerDisconnect: TransactionComplete \r \n " ) = = 0 )
{
if ( s - > state = = RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_END )
s - > state = RCP_STATE_SHUTDOWN_SERVER_DISCONNECT ;
else
s - > state = RCP_STATE_SETUP_SERVER_DISCONNECT ;
return 0 ;
}
goto resp_err ;
case RCP_STATE_SHUTDOWN_SERVER_DISCONNECT :
case RCP_STATE_SETUP_SERVER_DISCONNECT :
break ;
case RCP_STATE_SETUP_SET_SERVER_FILTER :
if ( strcmp ( resp , " SetServerFilter: OK \r \n " ) = = 0 )
return 0 ;
goto resp_err ;
case RCP_STATE_SETUP_LIST_SERVERS_RESULT_SIZE :
if ( strcmp ( resp , " ListServers: ListResultSize 1 \r \n " ) = = 0 )
{
s - > state = RCP_STATE_SETUP_LIST_SERVERS_INTERNET_RADIO ;
return 0 ;
}
goto resp_err ;
case RCP_STATE_SETUP_LIST_SERVERS_INTERNET_RADIO :
if ( strcmp ( resp , " ListServers: Internet Radio \r \n " ) = = 0 )
{
s - > state = RCP_STATE_SETUP_LIST_SERVERS_RESULTS_END ;
return 0 ;
}
goto resp_err ;
case RCP_STATE_SETUP_LIST_SERVERS_RESULTS_END :
if ( strcmp ( resp , " ListServers: ListResultEnd \r \n " ) = = 0 )
{
s - > state = RCP_STATE_SETUP_LIST_SERVERS ;
return 0 ;
}
goto resp_err ;
case RCP_STATE_SETUP_LIST_SERVERS :
break ;
case RCP_STATE_SETUP_SERVER_CONNECT_TRANS_INIT :
if ( strcmp ( resp , " ServerConnect: TransactionInitiated \r \n " ) = = 0 )
{
s - > state = RCP_STATE_SETUP_SERVER_CONNECT_CONNECTED ;
return 0 ;
}
goto resp_err ;
case RCP_STATE_SETUP_SERVER_CONNECT_CONNECTED :
if ( strcmp ( resp , " ServerConnect: Connected \r \n " ) = = 0 )
{
s - > state = RCP_STATE_SETUP_SERVER_CONNECT_TRANS_END ;
return 0 ;
}
// drop through incase theres no response on this
case RCP_STATE_SETUP_SERVER_CONNECT_TRANS_END :
if ( strcmp ( resp , " ServerConnect: TransactionComplete \r \n " ) = = 0 )
{
s - > state = RCP_STATE_SETUP_SERVER_CONNECT ;
return 0 ;
}
goto resp_err ;
case RCP_STATE_SETUP_SERVER_CONNECT :
break ;
case RCP_STATE_SETUP_WAKEUP :
case RCP_STATE_STANDBY :
if ( strcmp ( resp , " SetPowerState: OK \r \n " ) = = 0 )
return 0 ;
goto resp_err ;
case RCP_STATE_QUEUING_CLEAR :
if ( strcmp ( resp , " ClearWorkingSong: OK \r \n " ) = = 0 )
return 0 ;
goto resp_err ;
case RCP_STATE_QUEUING_SET_TITLE :
case RCP_STATE_QUEUING_SET_PLAYLIST_URL :
case RCP_STATE_QUEUING_SET_REMOTE_STREAM :
if ( strcmp ( resp , " SetWorkingSongInfo: OK \r \n " ) = = 0 )
return 0 ;
goto resp_err ;
case RCP_STATE_QUEUING_PLAY :
if ( strcmp ( resp , " QueueAndPlayOne: OK \r \n " ) = = 0 )
return 0 ;
/* this means the address we used in request to play in
* RCP_STATE_QUEUING_SET_PLAYLIST_URL is invalid
*/
if ( strcmp ( resp , " QueueAndPlayOne: ParameterError \r \n " ) = = 0 )
{
DPRINTF ( E_LOG , L_RCP , " Failed to start stream, remote unable to reach '%s' from '%s' at %s \n " , s - > stream_url , s - > devname , s - > address ) ;
return 1 ;
}
goto resp_err ;
case RCP_STATE_SHUTDOWN_STOPPED :
if ( strcmp ( resp , " Stop: OK \r \n " ) = = 0 )
return 0 ;
goto resp_err ;
case RCP_STATE_VOL_GET :
if ( strncmp ( resp , " GetVolume: " , strlen ( " GetVolume: " ) ) = = 0 )
{
ret = sscanf ( resp , " GetVolume: %hd " , & s - > volume ) ;
if ( ret < 0 )
goto resp_err ;
s - > device - > volume = s - > volume ;
return 0 ;
}
goto resp_err ;
2022-05-08 13:09:59 +01:00
case RCP_STATE_SETUP_VOL_SET :
2022-01-15 12:24:49 +00:00
case RCP_STATE_VOL_SET :
if ( strcmp ( resp , " SetVolume: OK \r \n " ) = = 0 | |
strcmp ( resp , " SetVolume: ParameterError \r \n " ) = = 0 )
return 0 ;
goto resp_err ;
case RCP_STATE_STREAMING :
case RCP_STATE_DISCONNECTED :
case RCP_STATE_FAILED :
// no resp in this state
break ;
default :
goto resp_err ;
}
return 0 ;
resp_err :
return - 1 ;
}
void
rcp_session_shutdown ( struct rcp_session * s , enum rcp_state state ) ;
/* Handle current state, action and move to next state
* returns - 1 when machine is done
*/
static int
rcp_state_transition ( struct rcp_session * s )
{
2022-05-08 13:09:59 +01:00
char buf [ 128 ] ;
2022-01-15 12:24:49 +00:00
switch ( s - > state )
{
case RCP_STATE_SETUP :
rcp_send ( s , RCP_STATE_SETUP_WAKEUP , NULL ) ;
break ;
/* RCP spec - "Usage Scenario: Testing an Internet Radio URL", page 176:
* To play back an arbitrary Internet Radio URL from RCP , you must set the
* “ working ” song to identify the URL you want to play , make sure you are
* connected to an appropriate music server , and then execute the
* QueueAndPlayOne command
*
* First , we ensure that we ’ re connected to the Internet Radio music server . Note
* that we set the server filter to “ radio ” before invoking the ListServers command ,
* ensuring that the only list result is the built - in Internet Radio server [ . . . ]
*
* - > GetConnectedServer
* < < " GetConnectedServer: OK "
* - > ServerDisconnect
* < < " ServerDisconnect: TransactionInitiated "
* < < " ServerDisconnect: Disconnected "
* < < " ServerDisconnect: TransactionComplete "
* - > SetServerFilter radio
* < < " SetServerFilter: OK "
* - > ListServers
* < < " ListServers: ListResultSize 1 "
* < < " ListServers: Internet Radio "
* < < " ListServers: ListResultEnd "
* - > ServerConnect 0
* < < " ServerConnect: TransactionInitiated "
* < < " ServerConnect: Connected "
* < < " ServerConnect: TransactionComplete "
*
* - > ClearWorkingSong
* < < " ClearWorkingSong: OK "
* - > SetWorkingSongInfo playlistURL http : //ownetone.local:3689/stream.mp3
* < < " SetWorkingSongInfo: OK "
* - > SetWorkingSongInfo remoteStream 1
* < < " SetWorkingSongInfo: OK "
* - > QueueAndPlayOne working
* < < " QueueAndPlayOne: OK "
*
* alternative but ICY meta not displayed
*
* RCP spec - " Usage Scenario: Playing a music File on the local network " , page 177 :
* The RCP client may wish to play back a music files stored on the
* local network that is not servedby amusic server . This is possible
* using the RCP session ’ s working song variable . The RCP client must
* setup the working song ’ s url and format fields and then call
* QueueAndPlayOne . It is recommended to not set the remoteStream
* field , as this will cause the file to be played back over
* automatically once it reaches the end of the file
*
*
* Note that the RCP spec has a copy / paste error ; it refers to
* ' ClearWorkingSongInfo ' which is an invalid command
*
* - > ClearWorkingSong
* < < " ClearWorkingSong: OK "
* - > SetWorkingSongInfo title xxx
* < < " SetWorkingSongInfo: OK "
* - > SetWorkingSongInfo url http : //ownetone.local:3689/stream.mp3
* < < " SetWorkingSongInfo: OK "
* - > SetWorkingSongInfo format MP3
* < < " SetWorkingSongInfo: OK "
* - > QueueAndPlayOne working
* < < " QueueAndPlayOne: OK "
*/
case RCP_STATE_SETUP_WAKEUP :
rcp_send ( s , RCP_STATE_SETUP_GET_CONNECTED_SERVER , NULL ) ;
break ;
case RCP_STATE_SHUTDOWN_STOPPED :
rcp_send ( s , RCP_STATE_SHUTDOWN_GET_CONNECTED_SERVER , NULL ) ;
break ;
case RCP_STATE_SHUTDOWN_GET_CONNECTED_SERVER :
rcp_send ( s , RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_INIT , NULL ) ;
break ;
case RCP_STATE_SETUP_GET_CONNECTED_SERVER :
rcp_send ( s , RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_INIT , NULL ) ;
break ;
case RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_INIT :
case RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_DISCONNECTED :
case RCP_STATE_SHUTDOWN_SERVER_DISCONNECT_TRANS_END :
case RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_INIT :
case RCP_STATE_SETUP_SERVER_DISCONNECT_DISCONNECTED :
case RCP_STATE_SETUP_SERVER_DISCONNECT_TRANS_END :
// no response, multistage response
break ;
case RCP_STATE_SETUP_SERVER_DISCONNECT :
rcp_send ( s , RCP_STATE_SETUP_SET_SERVER_FILTER , NULL ) ;
break ;
case RCP_STATE_SETUP_SET_SERVER_FILTER :
rcp_send ( s , RCP_STATE_SETUP_LIST_SERVERS_RESULT_SIZE , NULL ) ;
break ;
case RCP_STATE_SETUP_LIST_SERVERS_RESULT_SIZE :
case RCP_STATE_SETUP_LIST_SERVERS_INTERNET_RADIO :
case RCP_STATE_SETUP_LIST_SERVERS_RESULTS_END :
// no response, multistage response
break ;
case RCP_STATE_SETUP_LIST_SERVERS :
rcp_send ( s , RCP_STATE_SETUP_SERVER_CONNECT_TRANS_INIT , NULL ) ;
break ;
case RCP_STATE_SETUP_SERVER_CONNECT_TRANS_INIT :
case RCP_STATE_SETUP_SERVER_CONNECT_CONNECTED :
case RCP_STATE_SETUP_SERVER_CONNECT_TRANS_END :
break ;
case RCP_STATE_SETUP_SERVER_CONNECT :
2022-05-08 13:09:59 +01:00
snprintf ( buf , sizeof ( buf ) , " %d " , s - > device - > volume ) ;
rcp_send ( s , RCP_STATE_SETUP_VOL_SET , buf ) ;
2022-01-15 12:24:49 +00:00
break ;
2022-05-08 13:09:59 +01:00
case RCP_STATE_SETUP_VOL_SET :
2022-01-15 12:24:49 +00:00
rcp_send ( s , RCP_STATE_QUEUING_CLEAR , NULL ) ;
break ;
case RCP_STATE_QUEUING_CLEAR :
rcp_send ( s , RCP_STATE_QUEUING_SET_TITLE , cfg_getstr ( cfg_getsec ( cfg , " library " ) , " name " ) ) ;
break ;
case RCP_STATE_QUEUING_SET_TITLE :
rcp_send ( s , RCP_STATE_QUEUING_SET_PLAYLIST_URL , s - > stream_url ) ;
break ;
case RCP_STATE_QUEUING_SET_PLAYLIST_URL :
rcp_send ( s , RCP_STATE_QUEUING_SET_REMOTE_STREAM , NULL ) ;
break ;
case RCP_STATE_QUEUING_SET_REMOTE_STREAM :
rcp_send ( s , RCP_STATE_QUEUING_PLAY , NULL ) ;
break ;
case RCP_STATE_QUEUING_PLAY :
DPRINTF ( E_INFO , L_RCP , " Ready '%s' volume at %d \n " , s - > devname , s - > volume ) ;
event_del ( s - > reply_timeout ) ;
// fall through
case RCP_STATE_VOL_GET :
case RCP_STATE_VOL_SET :
s - > state = RCP_STATE_STREAMING ;
break ;
case RCP_STATE_STOPPING :
s - > state = RCP_STATE_SHUTDOWN_STOPPED ;
break ;
case RCP_STATE_STREAMING :
break ;
case RCP_STATE_SHUTDOWN_SERVER_DISCONNECT :
rcp_send ( s , RCP_STATE_STANDBY , NULL ) ;
break ;
case RCP_STATE_STANDBY :
rcp_session_shutdown ( s , RCP_STATE_DISCONNECTED ) ;
goto done ;
break ;
case RCP_STATE_DISCONNECTED :
goto done ;
break ;
default :
DPRINTF ( E_WARN , L_RCP , " Unhandled state transition %d '%s' \n " , s - > state , s - > devname ) ;
}
return 0 ;
done :
return - 1 ;
}
static void
rcp_status ( struct rcp_session * s ) ;
// send to remote and transition to next state
static int
rcp_send ( struct rcp_session * s , enum rcp_state next_state , const char * arg )
{
struct iovec iov [ ] = {
{ NULL , 0 } , // cmd
{ " " , 0 } , // arg spacer
{ " " , 0 } , // arg
{ ( void * ) " \r \n " , 2 }
} ;
const struct rcp_state_map * map = NULL ;
int ret ;
// ensure the state has a mapping
for ( int i = 0 ; i < RCP_STATE_MAX ; + + i )
{
if ( rcp_state_send_map [ i ] . state = = next_state & &
rcp_state_send_map [ i ] . cmd )
{
map = & rcp_state_send_map [ i ] ;
break ;
}
}
if ( ! map | | ( map & & map - > cmd = = NULL ) )
{
DPRINTF ( E_WARN , L_RCP , " BUG - state machine has no cmd for state %d on '%s' \n " , s - > state , s - > devname ) ;
return - 1 ;
}
iov [ 0 ] . iov_base = ( void * ) map - > cmd ;
iov [ 0 ] . iov_len = strlen ( map - > cmd ) ;
if ( map - > has_arg )
{
iov [ 1 ] . iov_base = ( void * ) " " ;
iov [ 1 ] . iov_len = 1 ;
iov [ 2 ] . iov_base = ( void * ) ( arg ? arg : " " ) ;
iov [ 2 ] . iov_len = arg ? strlen ( arg ) : 0 ;
}
2022-02-11 22:09:14 +01:00
// DPRINTF(E_DBG, L_RCP, "Device %" PRIu64 " state %d send '%s%s%s'\n", s->device->id, s->state, (char*)(iov[0].iov_base), (char*)(iov[1].iov_base), (char*)(iov[2].iov_base));
2022-01-15 12:24:49 +00:00
if ( s - > sock < = 0 ) {
DPRINTF ( E_LOG , L_RCP , " Ignoring send request on %s, state = %d \n " , s - > address , s - > state ) ;
return - 1 ;
}
ret = writev ( s - > sock , iov , 4 ) ;
if ( ret < 0 )
{
s - > state = RCP_STATE_FAILED ;
return - 1 ;
}
if ( ret = = 0 )
{
s - > state = RCP_STATE_DISCONNECTED ;
return - 1 ;
}
s - > state = next_state ;
return 0 ;
}
/* Returns:
* - 1 - failure of some kind on link
* 0 - recv ' d data
*
* reads data
*/
static int
rcp_recv ( struct rcp_session * s )
{
/* The RCP responses are of finite size so we can
* limit the input buf
*/
ssize_t recvd ;
const size_t avail = RCP_RESP_BUF_SIZE - ( s - > respptr - s - > respbuf ) ;
if ( avail = = 0 )
{
2022-02-11 22:09:14 +01:00
DPRINTF ( E_WARN , L_RCP , " Protocol BUG, cmd buf (%d) exhausted % " PRIu64 " state %d \n " , RCP_RESP_BUF_SIZE , s - > device - > id , s - > state ) ;
2022-01-15 12:24:49 +00:00
s - > state = RCP_STATE_FAILED ;
return - 1 ;
}
recvd = read ( s - > sock , s - > respptr , avail ) ;
2022-02-11 22:09:14 +01:00
// DPRINTF(E_DBG, L_RCP, "Device %" PRIu64 " state %d recv'd %zd bytes '%s'\n", s->device->id, s->state, recvd, s->respptr);
2022-01-15 12:24:49 +00:00
if ( recvd < = 0 )
{
DPRINTF ( E_LOG , L_RCP , " Failed to read response from '%s' - %s \n " , s - > devname , strerror ( recvd = = 0 ? ECONNRESET : errno ) ) ;
s - > state = RCP_STATE_DISCONNECTED ;
return - 1 ;
}
s - > respptr + = recvd ;
return 0 ;
}
/* Returns non-null ptr to a single response
* Roku can send multiple responses in packet
*/
static const char *
rcp_state_1resp ( char * resp , struct rcp_session * s )
{
char * p , * q ;
int len ;
char * ret = NULL ;
// find termination sequence of '\r\n' and adjust the incoming write position
// s->respptr, accordingly
// verify response termination sequence of '\r\n' but expected at least cmd + ':'
// ie min response is 'A: OK\r\n'
if ( s - > respptr = = s - > respbuf | | s - > respptr - s - > respbuf < 6 )
return NULL ;
p = s - > respbuf ;
q = p + 1 ;
while ( q < s - > respptr )
{
if ( * p = = ' \r ' & & * q = = ' \n ' )
{
len = q + 1 - s - > respbuf ;
memcpy ( resp , s - > respbuf , len ) ;
resp [ len ] = ' \0 ' ;
// now slide the rest of the s->respbuf to begining
memmove ( s - > respbuf , s - > respbuf + len , s - > respptr - s - > respbuf - len ) ;
s - > respptr - = len ;
memset ( s - > respptr , 0 , len ) ;
ret = & resp [ 0 ] ;
}
+ + p ;
+ + q ;
if ( ret )
break ;
}
return ret ;
}
/* ---------------------------- SESSION HANDLING ---------------------------- */
void
rcp_disconnect ( int fd )
{
/* no more receptions */
shutdown ( fd , SHUT_RDWR ) ;
close ( fd ) ;
}
void
rcp_session_shutdown ( struct rcp_session * s , enum rcp_state state )
{
event_del ( s - > ev ) ;
event_del ( s - > reply_timeout ) ;
rcp_disconnect ( s - > sock ) ;
s - > sock = - 1 ;
DPRINTF ( E_INFO , L_RCP , " Disconnected '%s' \n " , s - > devname ) ;
// we've shutdown, ensure state is valid
switch ( state )
{
case RCP_STATE_STANDBY . . . RCP_STATE_FAILED :
break ;
default :
state = RCP_STATE_FAILED ;
}
s - > state = state ;
rcp_status ( s ) ;
}
static void
rcp_reply_shutdown_timeout_cb ( int fd , short what , void * arg )
{
struct rcp_session * s ;
s = ( struct rcp_session * ) arg ;
if ( what ! = EV_TIMEOUT )
{
DPRINTF ( E_INFO , L_RCP , " Unexpected non timeout event (%d) %s at %s \n " , what , s - > devname , s - > address ) ;
return ;
}
DPRINTF ( E_LOG , L_RCP , " No response from '%s' (state %d), forcing shutting down \n " , s - > devname , s - > state ) ;
rcp_session_shutdown ( s , RCP_STATE_DISCONNECTED ) ;
}
static void
rcp_session_shutdown_init ( struct rcp_session * s )
{
struct timeval clear_timeout = { 15 , 0 } ;
if ( s - > reply_timeout )
event_free ( s - > reply_timeout ) ;
s - > reply_timeout = evtimer_new ( evbase_player , rcp_reply_shutdown_timeout_cb , s ) ;
if ( ! s - > reply_timeout )
{
DPRINTF ( E_WARN , L_RCP , " Out of memory for shutdown reply_timeout on session \n " ) ;
rcp_session_shutdown ( s , RCP_STATE_DISCONNECTED ) ;
}
else
{
DPRINTF ( E_DBG , L_RCP , " Limiting shutdown timeout %ld sec '%s' at %s \n " , clear_timeout . tv_sec , s - > devname , s - > address ) ;
// ensure we're not blocked forever on responses
event_add ( s - > reply_timeout , & clear_timeout ) ;
/* force the Roku into a non-library connected state, otherwise re-power
* will put Roku into pre - powerdown state which will cause it to reconnect
* to this server
*
* some users prefer non Roku connected state
*/
rcp_send ( s ,
s - > clear_on_close ?
RCP_STATE_SHUTDOWN_GET_CONNECTED_SERVER :
RCP_STATE_STANDBY ,
NULL ) ;
}
}
/* The core of this module. Libevent makes a callback to this function whenever
* there is new data to be read on the fd from the Soundbridge .
* Process data based on state machine
*/
static void
rcp_listen_cb ( int fd , short what , void * arg )
{
struct rcp_session * s ;
const char * p ;
char cmd [ RCP_RESP_BUF_SIZE + 1 ] = { 0 } ;
int ret ;
for ( s = rcp_sessions ; s ; s = s - > next )
{
if ( s = = ( struct rcp_session * ) arg )
break ;
}
if ( ! s )
{
DPRINTF ( E_INFO , L_RCP , " Callback on dead session, ignoring \n " ) ;
return ;
}
if ( what = = EV_TIMEOUT )
{
DPRINTF ( E_LOG , L_RCP , " Unexpected timeout event on '%s', shutting down \n " , s - > devname ) ;
goto fail ;
}
/* response from Soundbridge can be chunked: even simple initial response msg
* can arrive a ' r ' ' oku : read ' ' y \ r \ n '
*
* concat the response into the cmd buffer
*/
ret = rcp_recv ( s ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_RCP , " Failed to recv/construct response from '%s' \n " , s - > devname ) ;
goto fail ;
}
// process all full responses in s->respbuf
while ( ( p = rcp_state_1resp ( cmd , s ) ) )
{
// ensure respose matches state otherwise state machine is out of whack
ret = rcp_state_verify ( s , cmd ) ;
if ( ret < 0 )
goto resp_fail ;
if ( ret > 0 )
goto fail ;
memset ( cmd , 0 , sizeof ( cmd ) ) ;
ret = rcp_state_transition ( s ) ;
if ( ret < 0 )
// all done
break ;
rcp_status ( s ) ;
}
return ;
resp_fail :
DPRINTF ( E_WARN , L_RCP , " Unexpected response (parsed cmd '%s' remaining buf '%s') in state %d '%s' at %s \n " , cmd , s - > respbuf , s - > state , s - > devname , s - > address ) ;
fail :
// Downgrade state to make rcp_session_shutdown perform an exit which is
// quick and won't require a reponse from remote
s - > state = RCP_STATE_FAILED ;
rcp_session_shutdown ( s , RCP_STATE_FAILED ) ;
}
static void
rcp_reply_timeout_cb ( int fd , short what , void * arg )
{
struct rcp_session * s ;
s = ( struct rcp_session * ) arg ;
if ( what = = EV_TIMEOUT & & s - > state ! = RCP_STATE_STREAMING )
{
DPRINTF ( E_LOG , L_RCP , " Slow response from '%s' (state %d), shutting down \n " , s - > devname , s - > state ) ;
s - > state = RCP_STATE_FAILED ;
rcp_session_shutdown ( s , RCP_STATE_FAILED ) ;
event_del ( s - > reply_timeout ) ;
}
}
/* RCP spec - "RCP Sessions" #2, page 8:
* Telnet ( TCP port 5555 ) – SoundBridge and WMM devices listen on TCP port
* 5555 at their configured IPaddress for incoming connections , and expose the
* RCP shell directly on this connection . Once connected , the device will
* answer with the RCP initiation sequence , “ roku : ready ” , indicating that the
* connection is ready for commands .
*/
static struct rcp_session *
rcp_session_make ( struct output_device * device , int callback_id )
{
struct rcp_session * s ;
cfg_t * cfgrcp ;
struct timeval rcp_resp_timeout = { 20 , 0 } ;
int flags ;
struct sockaddr_storage ss = { 0 } ;
ev_socklen_t socklen = sizeof ( ss ) ;
char addrbuf [ 128 ] = { 0 } ;
void * inaddr ;
const char * addr ;
int httpd_port ;
int ret ;
CHECK_NULL ( L_RCP , s = calloc ( 1 , sizeof ( struct rcp_session ) ) ) ;
s - > state = RCP_STATE_SETUP ;
s - > callback_id = callback_id ;
s - > device = device ;
s - > respptr = s - > respbuf ;
cfgrcp = cfg_gettsec ( cfg , " rcp " , device - > name ) ;
s - > clear_on_close = cfgrcp ? cfg_getbool ( cfgrcp , " clear_on_close " ) : false ;
s - > sock = net_connect ( device - > v4_address , device - > v4_port , SOCK_STREAM , " RCP control " ) ;
if ( s - > sock < 0 )
{
DPRINTF ( E_LOG , L_RCP , " Could not connect to %s \n " , device - > name ) ;
goto out_free_session ;
}
ret = getsockname ( s - > sock , ( struct sockaddr * ) & ss , & socklen ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_RCP , " Could not determine client's connected address %s \n " , device - > name ) ;
goto out_close_connection ;
}
inaddr = & ( ( struct sockaddr_in * ) & ss ) - > sin_addr ;
addr = evutil_inet_ntop ( ss . ss_family , inaddr , addrbuf , sizeof ( addrbuf ) ) ;
if ( ! addr )
{
DPRINTF ( E_LOG , L_RCP , " Could not determine client's connected address %s \n " , device - > name ) ;
goto out_close_connection ;
}
httpd_port = cfg_getint ( cfg_getsec ( cfg , " library " ) , " port " ) ;
s - > stream_url = safe_asprintf ( " http://%s:%d/stream.mp3 " , addr , httpd_port ) ;
s - > ev = event_new ( evbase_player , s - > sock , EV_READ | EV_PERSIST , rcp_listen_cb , s ) ;
if ( ! s - > ev )
{
DPRINTF ( E_LOG , L_RCP , " Out of memory for listener event \n " ) ;
goto out_close_connection ;
}
s - > reply_timeout = evtimer_new ( evbase_player , rcp_reply_timeout_cb , s ) ;
if ( ! s - > reply_timeout )
{
DPRINTF ( E_LOG , L_RCP , " Out of memory for reply_timeout \n " ) ;
goto out_free_ev ;
}
flags = fcntl ( s - > sock , F_GETFL , 0 ) ;
fcntl ( s - > sock , F_SETFL , flags | O_NONBLOCK ) ;
event_add ( s - > ev , NULL ) ;
event_add ( s - > reply_timeout , & rcp_resp_timeout ) ;
s - > devname = strdup ( device - > name ) ;
s - > address = strdup ( device - > v4_address ) ;
2022-05-08 13:09:59 +01:00
s - > volume = device - > volume ;
2022-01-15 12:24:49 +00:00
s - > next = rcp_sessions ;
rcp_sessions = s ;
2022-02-11 22:09:14 +01:00
DPRINTF ( E_DBG , L_RCP , " Make session device % " PRIu64 " %s at %s stream url '%s' \n " , s - > device - > id , s - > devname , s - > address , s - > stream_url ) ;
2022-01-15 12:24:49 +00:00
// s is now the official device session
outputs_device_session_add ( device - > id , s ) ;
DPRINTF ( E_INFO , L_RCP , " Connection to '%s' established \n " , s - > devname ) ;
rcp_status ( s ) ;
return s ;
out_free_ev :
event_free ( s - > reply_timeout ) ;
event_free ( s - > ev ) ;
out_close_connection :
rcp_disconnect ( s - > sock ) ;
out_free_session :
free ( s ) ;
return NULL ;
}
static void
rcp_session_free ( struct rcp_session * s )
{
if ( ! s )
return ;
if ( s - > sock > = 0 )
rcp_disconnect ( s - > sock ) ;
if ( s - > ev )
event_free ( s - > ev ) ;
if ( s - > reply_timeout )
event_free ( s - > reply_timeout ) ;
free ( s - > stream_url ) ;
free ( s - > address ) ;
free ( s - > devname ) ;
free ( s ) ;
}
2022-04-24 14:39:46 +01:00
2022-01-15 12:24:49 +00:00
static void
rcp_session_cleanup ( struct rcp_session * rs )
{
struct rcp_session * s ;
2022-04-24 14:39:46 +01:00
if ( ! rs )
return ;
2022-01-15 12:24:49 +00:00
if ( rs = = rcp_sessions )
rcp_sessions = rcp_sessions - > next ;
else
{
for ( s = rcp_sessions ; s & & ( s - > next ! = rs ) ; s = s - > next )
; /* EMPTY */
if ( ! s )
2022-04-24 14:39:46 +01:00
{
DPRINTF ( E_WARN , L_RCP , " WARNING: struct rcp_session (%s at %s) not found in list; BUG! \n " , rs - > devname , rs - > address ) ;
return ;
}
s - > next = rs - > next ;
2022-01-15 12:24:49 +00:00
}
outputs_device_session_remove ( rs - > device - > id ) ;
rcp_session_free ( rs ) ;
}
/* ---------------------------- STATUS HANDLERS ----------------------------- */
static void
rcp_status ( struct rcp_session * s )
{
enum output_device_state state ;
switch ( s - > state )
{
case RCP_STATE_SETUP :
case RCP_STATE_SETUP_WAKEUP :
state = OUTPUT_STATE_STARTUP ;
break ;
2022-05-08 13:09:59 +01:00
case RCP_STATE_SETUP_GET_CONNECTED_SERVER . . . RCP_STATE_SETUP_VOL_SET :
2022-01-15 12:24:49 +00:00
case RCP_STATE_QUEUING_CLEAR . . . RCP_STATE_QUEUING_PLAY :
case RCP_STATE_VOL_GET :
case RCP_STATE_VOL_SET :
case RCP_STATE_STOPPING . . . RCP_STATE_STANDBY :
state = OUTPUT_STATE_CONNECTED ;
break ;
case RCP_STATE_STREAMING :
state = OUTPUT_STATE_STREAMING ;
break ;
case RCP_STATE_DISCONNECTED :
state = OUTPUT_STATE_STOPPED ;
break ;
case RCP_STATE_FAILED :
default :
state = OUTPUT_STATE_FAILED ;
}
DPRINTF ( E_DBG , L_RCP , " Mapping state from (internal) %d -> (output) %d \n " , s - > state , state ) ;
outputs_cb ( s - > callback_id , s - > device - > id , state ) ;
s - > callback_id = - 1 ;
if ( state = = OUTPUT_STATE_STOPPED | | state = = OUTPUT_STATE_FAILED )
rcp_session_cleanup ( s ) ;
}
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
static int
rcp_device_start ( struct output_device * device , int callback_id )
{
struct rcp_session * s ;
s = rcp_session_make ( device , callback_id ) ;
if ( ! s )
return - 1 ;
return 1 ;
}
static int
rcp_device_stop ( struct output_device * device , int callback_id )
{
struct rcp_session * s = device - > session ;
/* force these devices as deselected (auto state saves in db later) since
* these need use to select ( and cause the device probe to start connection to
* remote side
*/
s - > callback_id = callback_id ;
// tear this session down, incl free'ing it
rcp_session_shutdown_init ( s ) ;
return 1 ;
}
static int
rcp_device_flush ( struct output_device * device , int callback_id )
{
struct rcp_session * s = device - > session ;
s - > callback_id = callback_id ;
rcp_status ( s ) ;
return 1 ;
}
static int
rcp_device_probe ( struct output_device * device , int callback_id )
{
struct rcp_session * s ;
s = rcp_session_make ( device , callback_id ) ;
if ( ! s )
return - 1 ;
return 1 ;
}
static int
rcp_device_volume_set ( struct output_device * device , int callback_id )
{
struct rcp_session * s = device - > session ;
char cmd [ 4 ] ;
int ret ;
if ( s - > state ! = RCP_STATE_STREAMING )
return 0 ;
s - > callback_id = callback_id ;
ret = snprintf ( cmd , sizeof ( cmd ) , " %d " , device - > volume ) ;
if ( ret < 0 ) {
return 0 ;
}
rcp_send ( s , RCP_STATE_VOL_SET , cmd ) ;
return 1 ;
}
static void
rcp_device_cb_set ( struct output_device * device , int callback_id )
{
struct rcp_session * s = device - > session ;
s - > callback_id = callback_id ;
}
static void
rcp_mdns_device_cb ( const char * name , const char * type , const char * domain , const char * hostname , int family , const char * address , int port , struct keyval * txt )
{
struct output_device * device ;
bool exclude ;
int ret ;
/* $ avahi-browse -vrt _roku-rcp._tcp
Server version : avahi 0.7 ; Host name : foo . local
E Ifce Prot Name Type Domain
+ eth0 IPv4 SoundBridge _roku - rcp . _tcp local
= eth0 IPv4 SoundBridge _roku - rcp . _tcp local
hostname = [ SoundBridge . local ]
address = [ 192.168 .0 .3 ]
port = [ 5555 ]
txt = [ ]
: Cache exhausted
: All for now
*/
exclude = cfg_getbool ( cfg_gettsec ( cfg , " rcp " , name ) , " exclude " ) ;
DPRINTF ( E_DBG , L_RCP , " Event for %sRCP/SoundBridge device '%s' (address %s, port %d) \n " , exclude ? " excluded " : " " , name , address , port ) ;
if ( exclude )
{
DPRINTF ( E_INFO , L_RCP , " Excluding discovered RCP/SoundBridge device '%s' at %s \n " , name , address ) ;
return ;
}
CHECK_NULL ( L_RCP , device = calloc ( 1 , sizeof ( struct output_device ) ) ) ;
device - > id = djb_hash ( name , strlen ( name ) ) ;
device - > name = strdup ( name ) ;
device - > type = OUTPUT_TYPE_RCP ;
device - > type_name = outputs_name ( device - > type ) ;
if ( port < 0 | | ! address )
{
ret = player_device_remove ( device ) ;
}
else
{
// RCP/Roku Soundbridges only support ipv4
device - > v4_address = strdup ( address ) ;
device - > v4_port = port ;
DPRINTF ( E_INFO , L_RCP , " Adding RCP output device '%s' at '%s' \n " , name , address ) ;
ret = player_device_add ( device ) ;
}
if ( ret < 0 )
outputs_device_free ( device ) ;
}
static int
rcp_init ( void )
{
cfg_t * cfg_rcp ;
int i ;
int ret ;
// validate best we can rcp_state_send_map has all the rcp_states
assert ( ARRAY_SIZE ( rcp_state_send_map ) = = RCP_STATE_MAX + 1 ) ;
//DPRINTF(E_FATAL, L_RCP, "BUG: rcp_state_send_map[] (%d) out of sync with rcp_states (%d)\n", ARRAY_SIZE(rcp_state_send_map), RCP_STATE_MAX);
for ( i = 0 ; i < RCP_STATE_MAX ; i + + )
{
assert ( rcp_state_send_map [ i ] . state = = ( enum rcp_state ) i ) ;
//DPRINTF(E_FATAL, L_RCP, "BUG: rcp_state_send_map[%d] out of sync with enum rcp_states\n", i);
}
cfg_rcp = cfg_gettsec ( cfg , " rcp " , " * " ) ;
if ( cfg_rcp & & cfg_getbool ( cfg_rcp , " exclude " ) )
{
DPRINTF ( E_LOG , L_RCP , " Excluding all RCP/SoundBridges \n " ) ;
return 0 ;
}
ret = mdns_browse ( " _roku-rcp._tcp " , rcp_mdns_device_cb , 0 ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_RCP , " Could not add mDNS browser for RCP/SoundBridge devices \n " ) ;
return - 1 ;
}
return 0 ;
}
static void
rcp_deinit ( void )
{
struct rcp_session * s ;
for ( s = rcp_sessions ; rcp_sessions ; s = rcp_sessions )
2022-04-24 14:39:46 +01:00
rcp_session_cleanup ( s ) ;
2022-01-15 12:24:49 +00:00
}
struct output_definition output_rcp =
{
. name = " RCP/SoundBridge " ,
. type = OUTPUT_TYPE_RCP ,
. priority = 99 ,
. disabled = 0 ,
. init = rcp_init ,
. deinit = rcp_deinit ,
. device_start = rcp_device_start ,
. device_stop = rcp_device_stop ,
. device_flush = rcp_device_flush ,
. device_probe = rcp_device_probe ,
. device_volume_set = rcp_device_volume_set ,
. device_cb_set = rcp_device_cb_set ,
} ;