#!/usr/bin/perl # # HydraPlayer Copyright (c) 2009 Tor Perkins. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. # # 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. # $|++; $0 =~ s|.*/||; use strict; use warnings; # # HydraPlayer:: <-- read configs, do stand-alone init # Player::HydraPlayer:: <-- the SliMP3 emulator POE sessions # Plugins::HydraPlayer:: <-- in BEGIN block, slim expects this # package HydraPlayer; our $DEBUG; our $VERBOSE; use Getopt::Long qw(:config gnu_getopt); GetOptions( 'd|debug:i' => \$DEBUG, 'v|verbose:i' => \$VERBOSE, 'h|help' => \&usage, ) || usage(); $DEBUG++ if defined $DEBUG && $DEBUG == 0; $VERBOSE++ if defined $VERBOSE && $VERBOSE == 0; $VERBOSE = $DEBUG if $DEBUG; $DEBUG ||= 0; $VERBOSE ||= 0; ############################ # Customize These Defaults # ############################ # # This is the default configuration. It should be adequate for testing # HydraPlayer in stand-alone mode with a single room. Once you are running in # plugin-mode, you should create a HydraPlayer.pm.rc file with a @players # definition containing one curly brace ({}) enclosed stanza per room. Copy # this @players definition as a starting template, but remove the 'our' # function at the begining... # our @players = ( { ALSA_PCM => 'room1', # configured in /etc/asound.conf PLAYERNAME => 'my_office', # descriptive text (no spaces) SYNC_WITH_MAC => '00:04:20:07:35:8d', # squeezebox to sync with VOLUME => '55', # optional initial volume MIX_DEVICE => 'hw:0', # device w/ the amixer control(s) MIX_CONTROL => [ 'DAC,0', 'DAC,1' ], # amixer volume control(s) }, ); our $server_name = '127.0.0.1'; # DNS name or IP addr our $server_port = 3483; # the server's UDP port $ENV{JACK_START_SERVER}='0'; # autostart jackd if needed... my $START_JACK_SERVER = 1 ; # ... or do it ourselves my $jackd_cmd = 'jackd --silent -R -d alsa -P multi_playback -r 44100'; our $REBOOT_OPTION = 1; our $RESTART_OPTION = 0; our $KILL_OTHERS_OPTION = 1; our $ROOM_CLONING_OPTION = 0; our $USE_FLAC_REPLAY_GAIN = 0; our $SET_DISPLAYMODES = 0; our $SET_SCREENSAVER_SNOW = 0; our $SET_HYDRAPLAYER_MENU = 0; my $RC_FILE = "/usr/share/slimserver/Plugins/HydraPlayer.pm.rc"; ################## use Time::HiRes; # use before POE for microsec $kernel->delay() values use POE; use POE::Wheel::Run; use POE::Filter::Stream; our $plugin_mode = 0; # initPlugin() is not invoked on the first pass (just parsed), $plugin_mode = 1 if # so we get a chance to evaluate this prior to initPlugin()... defined $Slim::Display::Display::VERSION; # our Slim::'s do not pull this in if ( -f "$RC_FILE" ) { report("++info: RC file: $RC_FILE") if ( $plugin_mode || $VERBOSE ); eval `cat $RC_FILE`; if ( $@ ) { report("++info: ERROR: RC file: eval result: $@"); } else { report("++info: RC file: eval OK") if ( $plugin_mode || $VERBOSE ); } } else { report("++info: no RC file: $RC_FILE") if ( $plugin_mode || $VERBOSE ); } # # if our config includes jack capture ports, then start w/ capture ports # enabled and sample at a higher rate (for cloning phonograph, etc.)... # There's no reason to use jack's --hwmon feature if using more than one # sound card (and jack's CPU utilization is very low anyway). # if ( $ROOM_CLONING_OPTION ) { for ( 0 .. $#{ @players } ) { next unless defined @{ $players[$_]{JACK_SOCKET} } && grep { /capture/ } @{ $players[$_]{JACK_SOCKET} }; $jackd_cmd =~ s{(?=-P multi_playback)}{-C multi_capture }; $jackd_cmd =~ s{44100}{96000}; last; } } if ( $DEBUG > 1 ) { $jackd_cmd =~ s{--silent}{--verbose}; } unless ( $plugin_mode ) { start_jackd_session(); start_player_sessions(); POE::Kernel->run(); } ############### # Subroutines # ############### sub start_jackd_session { return unless $START_JACK_SERVER; POE::Session->create( inline_states => { _start => \&jackd_start, wheel => \&jackd_wheel, logger => \&jackd_logger, kidhandler => \&jackd_kidhandler, # jackd died diehandler => \&jackd_diehandler, # session died sighandler => \&jackd_sighandler, # we're dying _stop => \&jackd_stop, } ); } sub jackd_start { my ($kernel, $heap) = @_[KERNEL, HEAP]; $kernel->sig( CHLD => 'kidhandler' ); $kernel->sig( DIE => 'diehandler' ); $kernel->sig( HUP => 'sighandler' ); $kernel->sig( INT => 'sighandler' ); $kernel->sig( TERM => 'sighandler' ); $kernel->sig( QUIT => 'sighandler' ); $kernel->alias_set("jackd_session"); $kernel->yield('wheel') } sub jackd_wheel { my ($kernel, $heap) = @_[KERNEL, HEAP]; report("++info: jackd_session: wheel: $jackd_cmd"); $heap->{wheel} && return; $heap->{wheel} = POE::Wheel::Run->new( Program => $jackd_cmd, Priority => 0, # jackd's '-R' switch renders this moot (I think) StdoutEvent => 'logger', # see: `ps -C jackd -cmL` and CHRT(1)... StderrEvent => 'logger', # Lowest to highest priority (niceness|rtprio): ); # 19 ... 10 ... 0 .. -10 .. -20 | 127 ... 100 ... 50 ... 0 } sub jackd_logger { report("++info: jackd: $_[ARG0]"); # see $jackd_cmd for verbosity } sub jackd_kidhandler { my ( $kernel, $heap, $signal, $pid, $child_error ) = @_[ KERNEL, HEAP, ARG0, ARG1, ARG2 ]; $heap->{wheel} || return; $heap->{wheel}->PID == $pid || return; $kernel->sig_handled(); $heap->{stopped} && return; report("++info: jackd_session: caught SIG$signal: starting new wheel"); delete $heap->{wheel}; if ( $plugin_mode ) { # there may be hung decoder pipes for ( 0 .. 1 ) { # out there... fortunatly, this my $action = "power $_"; # does not happen much (ever) for ( 0 .. $#{ @players } ) { my $a = "player_$_"; # alias # power 0/1 all players... my $ID = $players[$_]{SYNC_WITH_MAC} || next; # has hardware controller? report("++info: jackd_kidhandler: $a ($ID): cli_command: $action"); Player::HydraPlayer::cli_command("$ID $action"); } sleep 3; } } $kernel->delay('wheel', 5); # wait for decoder wheels to settle } sub jackd_diehandler { my $signal = $_[ARG0]; my $error = $_[ARG1]->{error_str}; $_[KERNEL]->sig_handled(); report("++error: jackd_session: caught SIG$signal: error: $error"); report("++fatal: jackd_session: caught SIG$signal: kill TERM => $$"); kill TERM => $$; } sub jackd_sighandler { my $signal = $_[ARG0]; $_[KERNEL]->sig_handled(); report("++info: jackd_session: caught SIG$signal: calling _stop()"); $_[KERNEL]->call($_[SESSION], '_stop'); } sub jackd_stop { my ($kernel, $heap) = @_[KERNEL, HEAP]; $heap->{stopped}++ && return; if ( $heap->{wheel} ) { $heap->{wheel}->kill(); sleep 1; $heap->{wheel}->kill(9); sleep 1; delete $heap->{wheel}; # de-wheel } $kernel->alias_remove( $heap->{alias} ); # de-alias } sub start_player_sessions { for ( 0 .. $#{ @players } ) { next unless $players[$_]{ALSA_PCM}; # has a noise maker? next unless $players[$_]{SYNC_WITH_MAC}; # has hardware controller? $HydraPlayer::player_index = $_; POE::Session->create( package_states => [ 'Player::HydraPlayer' => [ qw( _start find_server server_read lost_server sync_player adjust_volume connect_clones decoder_begin decoder_logger decoder_readbuff decoder_stalled decoder_end plugin_event kidhandler diehandler sighandler _stop ) ] ] ); } } our $log_open; sub report { if ( $plugin_mode ) { my $msg = join(" ", @_); $msg =~ s{[\x00-\x20\x7E-\xFF]}{ }g; $msg =~ s{ +$}{}; unless ($log_open) { use Sys::Syslog qw(:DEFAULT setlogsock); setlogsock('unix'); openlog($0, 'cons,pid', 'user'); $log_open++; } eval { # ignore all exceptions this throws syslog('notice', $msg); }; } else { print "@_\n"; } } sub usage { my $version = $Plugins::HydraPlayer::version; my $p = ' ' x length($0); # padding print STDERR <<"EOF"; $0 version $version. SliMP3 emulator written in perl. Can optionally operate in plugin mode to spawn multiple players that sync with real Squeeze devices. Usage: $0 -[h|-help] $p -[v|-verbose=level] -[d|-debug=level] Examples: $0 -d EOF exit 1; } ##################################################################### package Player::HydraPlayer; use POE; use IO::Socket::INET; use Proc::ProcessTable; use Time::HiRes qw(gettimeofday tv_interval); check_path( qw/ amixer buffer flac grep jackd killall madplay ping sox sudo / ); check_path( qw/ jack_lsp jack_connect jack_disconnect / ) if $ROOM_CLONING_OPTION; check_path( qw/ slimserver-restart / ) if $RESTART_OPTION; use constant BUFFSIZE => 131072; # same as slim's (the ring buffer) use constant BUFFERSIZE => 26648; # flac decoder buffer (min: 26648) use constant DECODERCHUNK => 2048; # bytes sent to the decoder per write use constant DECODERPLAYOUT => 3.875; # dying decoder playout delay in secs use constant DECODER_STALLED => 256; # kill decoder if playout stalls (secs) use constant MAX_PACKET_SIZE => 1400; # same as slim's (SliMP3/Stream.pm) use constant DATAGRAM_MAXLEN => 65536; # realisticly it's 1400 or less... use constant DISPLAY_SIZE => 128; # the LCD display... sub _start { my ($kernel, $heap) = @_[KERNEL, HEAP]; $heap->{player_index} = $HydraPlayer::player_index; $heap->{player_port} = 30000 + $heap->{player_index}; $heap->{player_mac} = sprintf "%012d", $heap->{player_index}; $players[$heap->{player_index}]{VOL_SCALING} = 1 # the default is no scaling unless defined $players[$heap->{player_index}]{VOL_SCALING} && $players[$heap->{player_index}]{VOL_SCALING} > 0 && $players[$heap->{player_index}]{VOL_SCALING} < 1; $heap->{alsa_pcm} = $players[$heap->{player_index}]{ALSA_PCM}; $heap->{playername} = $players[$heap->{player_index}]{PLAYERNAME}; $heap->{sync_with_mac} = $players[$heap->{player_index}]{SYNC_WITH_MAC}; $heap->{volume} = $players[$heap->{player_index}]{VOLUME}; $heap->{mix_device} = $players[$heap->{player_index}]{MIX_DEVICE}; $heap->{mix_control} = $players[$heap->{player_index}]{MIX_CONTROL}; $heap->{vol_scaling} = $players[$heap->{player_index}]{VOL_SCALING}; $heap->{clones} = $players[$heap->{player_index}]{CLONES} if $ROOM_CLONING_OPTION; $heap->{server_addr} = inet_ntoa(scalar gethostbyname($server_name)); $heap->{server_sockaddr} = pack_sockaddr_in( $server_port, inet_aton($heap->{server_addr})); $heap->{buff} = 0x00 x BUFFSIZE; # our ring buffer $heap->{rptr} = 0; # our read pointer $heap->{wptr} = 0; # our write pointer $heap->{cursor} = 0; # cursor position in display $heap->{display} = ' ' x DISPLAY_SIZE; # display (two 64 char lines) $heap->{old_wptr} = 0; # the previous write pointer $heap->{muted} = 0; # the sound device mute status $heap->{player_mode} = 'stop'; # play or stop (overall mode) $heap->{decoder_dying} = 0; # decoder got controlcode 1|3 $heap->{decoder_number} = 0; # the n'th audio decoder $heap->{last_ack} = 0; # the last audio packet acked $heap->{audio_acks_ok} = 1; # willing to ack audio packets $heap->{slim_contacted} = 0; # in contact w/ the mother ship $heap->{volume} ||= 50; # the default is 50% volume $kernel->alias_set( ( "player_" . $heap->{player_index} ) ); mute(); $kernel->sig( CHLD => 'kidhandler' ); $kernel->sig( DIE => 'diehandler' ); $kernel->sig( HUP => 'sighandler' ); $kernel->sig( INT => 'sighandler' ); $kernel->sig( TERM => 'sighandler' ); $kernel->sig( QUIT => 'sighandler' ); die("++fatal: whitespace in playername: $heap->{playername}") if $heap->{playername} =~ /\s/; if ( $DEBUG > 1 ) { local *OUT; open OUT, ">/tmp/$heap->{player_index}.stream"; $heap->{STREAMFILE} = *OUT{IO}; } my $LocalAddr = "0.0.0.0:$heap->{player_port}"; if ( $plugin_mode ) { # each player gets a unique ip addr $heap->{server_addr} = "127.2.1.$heap->{player_index}"; $LocalAddr = "$heap->{server_addr}:$heap->{player_port}"; } $heap->{socket} = IO::Socket::INET->new( Proto => 'udp', LocalAddr => $LocalAddr, ); $heap->{socket} || die "Couldn't create server socket: $!"; $kernel->select_read( $heap->{socket}, "server_read" ); $kernel->yield('find_server'); } sub find_server { my ($kernel, $heap) = @_[KERNEL, HEAP]; $heap->{slim_contacted} && return; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $VERBOSE && $report->("++info: $a: sending discovery"); my $socket = $heap->{socket}; my $msgtype = 'd'; my $deviceid = 1; my $revision = 17; # 0x11 my $pkt = pack 'axCCxxxxxxxxH12', # see readUDP() in Slim/Networking/UDP.pm $msgtype, $deviceid, $revision, $heap->{player_mac}; send_packet($socket, $heap->{server_sockaddr}, $pkt); $kernel->delay('find_server', 1); } sub send_packet { my $socket = shift; my $sockaddr = shift; my $pkt = shift; send( $socket, $pkt, 0, $sockaddr ) == length($pkt) or warn "Trouble sending packet: $!"; } sub lost_server { $_[HEAP]->{slim_contacted} = 0; $_[KERNEL]->yield('find_server'); } sub sync_player { # slim's restoreSync, syncPower, etc. not required... my ($kernel, $heap) = @_[KERNEL, HEAP]; $heap->{slim_contacted} || return; $plugin_mode || return; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $VERBOSE && $report->("++info: $a: sync_player: $heap->{sync_with_mac}"); my $next_check = 5; my $player_mac = join(':', unpack("(A2)*", $heap->{player_mac})); my $players_query = "players 0 99"; # 99 max players per reply my $sync_query = "$player_mac sync ?"; my $sync_command = "$player_mac sync $heap->{sync_with_mac}"; my $unsync_command = "$player_mac sync -"; my $setplayername = "hydraplayer setplayername $player_mac $heap->{playername}"; my $sync_with_mac_found; # the new 'player ip' CLI is buggy... PLAYER: for ( cli_command($players_query) ) { my @records = /(playerid:[0-9a-f:]{17} ip:[0-9.]{7,15}):/g; for ( @records ) { my ( $squeeze_mac, $squeeze_ip ) = /playerid:(\S+) ip:(\S+)/; if ( $squeeze_mac eq $heap->{sync_with_mac} ) { $sync_with_mac_found++; if ( host_alive($squeeze_ip) ) { for ( cli_command($sync_query) ) { $DEBUG && $report->("++debug: sync_query reply: $_"); if ( /sync (\S+)/ ) { # a good reply my $synced_with = $1; # '-' or a mac SWITCH: for ($synced_with) { /$heap->{sync_with_mac}/ && do { # already good $next_check = 300; last; }; /-/ && do { # we are not synced for ( cli_command($sync_command) ) { $DEBUG && $report->("++debug: sync_command reply: $_"); if ( /^$sync_command$/ ) { # output eq input $next_check = 300; # we are synced cli_command($setplayername); # now is a good time } } last; }; /./ && do { # we are mal-synced cli_command($unsync_command); last; }; } } } } else { # $squeeze_ip not alive $next_check = 300; } last PLAYER; } } } $next_check = 300 unless $sync_with_mac_found; $kernel->delay('sync_player', $next_check); } sub host_alive { my $ip = shift; return ( system("ping -l3 -c1 -w1 $ip >/dev/null 2>&1") ? 0 : 1 ); } sub cli_command { my $response; my $command = shift || return "error: missing command"; my $cli_port = Slim::Utils::Prefs::get('cliport'); eval { local $SIG{ALRM} = sub { die 'timeout'; }; alarm 1; # we do not want to spend lots of time here... my $sock = IO::Socket::INET->new("127.0.0.1:$cli_port"); print $sock "$command\n"; $response = <$sock>; alarm 0; }; return "error: timeout" if ( $@ && $@ =~ /timeout/ ); return "error: eval corrupted: $@" if $@; $response =~ s/%(..)/pack('c', hex($1))/eg; # percent escaping return $response; } sub plugin_event { my ( $kernel, $heap, $action ) = @_[ KERNEL, HEAP, ARG0 ]; $heap->{slim_contacted} || return; $plugin_mode || return; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $VERBOSE && $report->("++info: $a: plugin_event: $action"); SWITCH: for ($action) { /player mode (.*)/ && do { my $mode = $1; # # The only reason we keep track of $heap->{player_mode} is so that we can # run an external command (if defined) upon mode changes. We do not need # to keep track of this to play audio... This capability is good for # turning on/off amplifiers. This archive includes two scripts that I # use in my setup(architect_signal_sense, krell_power_control). # $heap->{player_mode} eq $mode && return; # no change... $heap->{player_mode} = $mode; # save new mode Plugins::HydraPlayer::player_mode_cmd($player, $mode); last; }; /volume ((?:un)?mute|\d+)( heap_only)?/ && do { my $vol = $1; my $heap_only = $2; if ( $heap_only ) { # do not unmute $heap->{volume} = $vol; } else { $vol =~ /\d/ ? setvol($vol) : $vol eq 'mute' ? mute() : unmute(); } last; }; /((?:un)?clone) (.*)/ && do { my $action = $1; my $room = $2; $ROOM_CLONING_OPTION || last; Plugins::HydraPlayer::toggle_clone_master_flag($room, $players[$player]{PLAYERNAME}); if ( $action eq 'clone' ) { push(@{ $heap->{clones} }, $room); # remember clone on heap's clone list setvol($heap->{volume}); # unmute the new clone } else { @{ $heap->{clones} } = grep {!/^$room$/} @{ $heap->{clones} } if defined @{ $heap->{clones} }; # forget this clone setvol($heap->{volume}, { dead_clone => $room }); # mute the old clone } $kernel->yield('connect_clones'); # connect/disconnect clones... last; }; /power (.*)/ && do { my $power = $1; if ( $power ) { $heap->{audio_acks_ok} = 1; $kernel->yield('sync_player') if $heap->{sync_with_mac}; } else { $heap->{rptr} = 0; $heap->{audio_acks_ok} = 0; $heap->{decoder_wheel} && $kernel->call($_[SESSION], 'decoder_end'); } last; }; /playlist newsong (.*)/ && do { # we do this just for logging purposes my $current_track = $1 || last; $heap->{current_track} eq $current_track && last if defined $heap->{current_track}; $heap->{current_track} = $current_track; $VERBOSE && $report->("++info: $a: current_track: $current_track"); last; }; /./ && do { $report->("++error: $a: plugin_event: unknown action: $action"); last; }; } } sub server_read { my ( $kernel, $heap, $socket ) = @_[ KERNEL, HEAP, ARG0 ]; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias my $remote_address = recv( $socket, my $message = "", DATAGRAM_MAXLEN, 0 ); return unless defined $remote_address; my ( $peer_port, $peer_addr ) = unpack_sockaddr_in($remote_address); my $human_addr = inet_ntoa($peer_addr); return unless $human_addr eq $heap->{server_addr}; my $type = unpack('a',$message); SWITCH: for ($type) { /h|D/ && do { # hello/discovery $VERBOSE && $report->("++info: $a: received hello/discovery: type: $type"); $VERBOSE && $report->("++info: $a: sending hello"); my $msgtype = 'h'; my $deviceid = 1; my $revision = 17; # 0x11 my $pkt = pack 'aCCxxxxxxxxxH12', # see getUdpClient() in SliMP3/Protocol.pm $msgtype, $deviceid, $revision, $heap->{player_mac}; send_packet($socket, $heap->{server_sockaddr}, $pkt); $heap->{slim_contacted}++; $kernel->delay('sync_player', 5) if $heap->{sync_with_mac}; last; }; /l/ && do { # LCD data # # not sure how often we're guaranteed an LCD packet... But # we use it's arrival to signify that contact continues to be # established. We'll wait 20 seconds before panicking... # $kernel->delay('lost_server', 20); # # do not waste cycles here unless somebody cares... # last unless $VERBOSE; # # The server thinks we have a katakana display (see 'sub vfdmodel' in # Player/SLIMP3.pm). For char translations, see katakana in # Display/Lib/TextVFD.pm (we only reverse a subset of these). The # display is comprised of two 64 char lines, but Slim only uses the # first 40 chars per line (see vfdUpdate in Display/Lib/TextVFD.pm). # LCD data starts 18 bytes in to the packet (see 'sub vfd ' in # Player/SLIMP3.pm). # my $lcd_update; my $padding = substr($message, 0, 18, ''); $DEBUG && $report->("++debug: $a: received LCD data"); while ( length $message > 0 ) { my $type = substr($message, 0, 1, ''); my $value = substr($message, 0, 1, ''); SWITCH: for ($type) { /\x02/ && do { # LCD command SWITCH: for ($value) { /\x01/ && do { $DEBUG && $report->("++debug: $a: LCD comm: display clear"); $heap->{display} = ' ' x DISPLAY_SIZE; $heap->{cursor} = 0; last; }; /\x02|\x03/ && do { $DEBUG && $report->("++debug: $a: LCD comm: cursor home"); $heap->{cursor} = 0; last; }; /\x10|\x11|\x12|\x13/ && do { $DEBUG && $report->("++debug: $a: LCD comm: cursor left"); $heap->{cursor} = 0 if --$heap->{cursor} < 0; last; }; /\x14|\x15|\x16|\x17/ && do { $DEBUG && $report->("++debug: $a: LCD comm: cursor right"); $heap->{cursor} = DISPLAY_SIZE if ++$heap->{cursor} > DISPLAY_SIZE; last; }; /./ && do { $value = ord $value; if ( $value & 0x80 ) { # cursor position (0 to 127)... $heap->{cursor} = $value & 0x7F; # ... an index into two 64 char lines $DEBUG && $report->("++debug: $a: LCD comm: cursor position: $heap->{cursor}"); } last; }; } last; }; /\x03/ && do { # LCD character my $oldchar = q{\x5c\x7e\x7f\x80\x81\x82\x83\x84\x86\x87} . q{\x88\x88\x89\x8a\x8c\x8e\x8f\x90\x91\x92} . q{\x94\x95\x98\x99\xa3\xa5\xb0\xb7\xde\xdf} . q{\xe1\xe2\xe4\xe6\xe7\xea\xeb\xec\xee\xef} . q{\xf0\xf1\xf5\xf9\xfd}; my $newchar = q{\xa5\xbb\xab\xc4\xc4\xc3\xc5\xe1\xe5\xd6} . q{\xf6\xd8\xf8\xdc\x5c\x7e\xa7\xc6\xe6\xa3} . q{\xb7\x6f\xa6\xc7\xac\xb7\x2d\xb1\xa8\xb0} . q{\xe4\xdf\xb5\x70\x67\x6a\xa4\xa2\xf1\xf6} . q{\x70\x71\xfc\x79\xf7}; eval "\$value =~ tr/$oldchar/$newchar/"; substr($heap->{display}, $heap->{cursor}++, 1) = $value; $lcd_update++; last; }; /./ && do { # unknown type $DEBUG && $report->( sprintf("++debug: $a: LCD type (0x%02X) unknown", ord $type) ); last; }; } } if ( $VERBOSE && $lcd_update ) { my ($lcd_1, $lcd_2) = $heap->{display} =~ /^(.{64})(.*)/; $report->("++info: $a: LCD 1: $lcd_1"); $report->("++info: $a: LCD 2: $lcd_2"); } last; }; /m/ && do { # audio data (see udpstream() in Player/SLIMP3.pm) $heap->{audio_acks_ok} || last; # talk to the hand... my $header = substr($message, 0, 18, ''); # what remains is audio data my( $controlcode, $wptr, $seq ) = unpack('xCxxxxn xxn xxxxxx', $header); $heap->{last_ack} == $seq && last; # ignore repeats $DEBUG && $report->( sprintf("++debug: $a: audio: seq: %5d wptr: %5d controlcode: %1d", $seq, $wptr, $controlcode) ); # copy audio data to the circular buffer... # the SlimServer never let's us fall off the edge... # the SlimServer expects $wptr to point to type short... substr($heap->{buff}, $wptr * 2, length $message) = $message; # double 'cuz ptr to short $heap->{wptr} = $wptr; # decoder_readbuff() uses this... # # Any fLaC markers will always be found at the start of a new $message... # # The reason why this matters is that the two decoders (madplay and flac) # behave very differently from each other. The mpeg decoder can pretty # much handle whatever we give it (so long as it's mpeg). It does not # care if it sees a new song midstream. The flac decoder is much more # finicky. It wants to get a nice fLaC header at the beginning or it # will abort (can this be remedied in some way?). So, we must be on the # lookout for fLaC headers and open a new flac decoder when we see them. # # We'll look no matter what (it's not too expensive). We only use what # we find ($heap->{embedded_fLaC} = $wptr) when we are not running as a # SlimServer plugin. # # In $plugin_mode we are always synced with a squeezebox. Because of # this, slim always sends us a controlcode 3 (halt w/ $rptr reset). It # does this so that song transitions get re-synced across all players for # each new song. This spares us from ever having to worry about song # transitions and the fLaC marker is always waiting at $rptr == 0. # # If not in $plugin_mode, slim will keep $controlcode pinned to 0 so long # as the stream's format does not change (we still get the nice # controlcode 3 on flac to mpeg and mpeg to flac transitions). This also # means that fLaC markers can be anywhere (not just $rptr == 0). So,... # it's up to us to find the song transitions as they come... # if ( $message =~ /^.?fLaC/s ) { $DEBUG && $report->("++debug: $a: embedded_fLaC: seq: $seq wptr: $wptr"); $heap->{embedded_fLaC} = $wptr; # decoder_readbuff() needs to know this... } # # A note regarding DECODERPLAYOUT... # # This setting can be arbitrarily long as the decoder will stop smoothly # (and without an audible defect) when audio data runs out... The # decoder simply plays out the remaining audio in it's buffer (which is # of unknown temporal duration) and we get a SIGCHILD when it exits. # Simultaneously, the SlimServer is filling the buffer on all players as # fast as it can (with no delay between tracks). Once checkSync() is # satisfied that all players are sufficiently full, we'll immediately # get controlcode 0 (play). If this happens while still in an old # decoder's DECODERPLAYOUT, it's not a problem because we'll just wait # before starting the new decoder (the SlimServer is tolerant of this # (i.e. receiving many acks w/ rptr:0)). Conversely, if DECODERPLAYOUT # expires while the decoder is still playing, we'll just decoder_end() # it (but that can lead to an audible defect)... # SWITCH: for ($controlcode) { # see streamControlCodes in SliMP3/Stream.pm /0/ && do { # play decoder $heap->{decoder_wheel} || $kernel->yield('decoder_begin') || $kernel->yield('plugin_event', 'player mode play'); last; }; /1/ && do { # halt decoder no reset rptr $DEBUG && $report->("++debug: $a: received controlcode 1: halt no reset"); if ( $heap->{decoder_wheel} ) { if ( $heap->{decoder_dying}++ == 0 ) { $heap->{decoder_wheel}->shutdown_stdin(); $kernel->delay('decoder_end', DECODERPLAYOUT); } } last }; /3/ && do { # halt decoder w/ reset rptr $DEBUG && $report->("++debug: $a: received controlcode 3: halt w/ reset"); $heap->{rptr} = 0; if ( $heap->{decoder_wheel} ) { if ( $heap->{decoder_dying}++ == 0 ) { $heap->{decoder_wheel}->shutdown_stdin(); $kernel->delay('decoder_end', DECODERPLAYOUT); } } last }; /./ && do { $VERBOSE && $report->( sprintf("++info: $a: controlcode (0x%02X) unknown", ord $controlcode) ); last; }; } # ack this packet $heap->{last_ack} = $seq; my $msgtype = 'a'; # audio ack my $pkt = pack 'axxxxxn n n H12', # see processMessage() in SliMP3/Protocol.pm $msgtype, $wptr, $heap->{rptr}, $seq, $heap->{player_mac}; send_packet($socket, $heap->{server_sockaddr}, $pkt); $DEBUG && $report->( sprintf("++debug: $a: ack: seq: %5d rptr: %5d", $seq, $heap->{rptr}) ); if ( $DEBUG > 2 ) { # create a file for this message local *OUT; my $pipe = ( $heap->{decoder_wheel} # is the decoder running yet...? ? $heap->{decoder_number} # ... then use it : $heap->{decoder_number} + 1 ); # ... else use what it will be my $f = sprintf("/tmp/$player.$pipe.%09d.message", ++$heap->{message_number}); open OUT, ">$f"; if ( $DEBUG > 3 ) { print OUT "controlcode: $controlcode\n"; print OUT " wptr: $wptr\n"; print OUT " seq: $seq\n"; print OUT "=====================\n"; } print OUT $message; close OUT; } last; }; /2/ && do { # i2c data # see "ignore SLIMP3's i2c acks" in SliMP3/Protocol.pm my $header = substr($message, 0, 18, ''); # what remains is i2c data $DEBUG && $report->("++debug: $a: i2c: $message"); last; }; /./ && do { # unknown type $VERBOSE && $report->( sprintf("++info: $a: packet type (0x%02X) unknown", ord $type) ); last; }; } } sub check_path { my $report = \&HydraPlayer::report; delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; $ENV{PATH} = '/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin'; use File::Which; for ( @_ ) { which($_) && next; $report->("++fatal: $_ not in PATH"); $report->("++error: $_ not in PATH"); die "$_ not in PATH"; } } sub mute { my $heap = $poe_kernel->get_active_session()->get_heap(); unless ( $heap->{muted} ) { setvol(0, { mute => 1 }); } } sub unmute { my $heap = $poe_kernel->get_active_session()->get_heap(); if ( $heap->{muted} ) { setvol($heap->{volume}); } } sub adjust_volume { my ($kernel, $heap) = @_[KERNEL, HEAP]; return unless defined $heap->{vol_cmd_array}; my $throttle_factor = 0.1; # empirically derived stampede limiter... my $time = Time::HiRes::time(); if ( defined $heap->{last_adjust_volume_cmd} && $time - $heap->{last_adjust_volume_cmd} < $throttle_factor ) { $kernel->delay('adjust_volume', $throttle_factor) unless $heap->{adjust_volume_pending}++; return; } $heap->{adjust_volume_pending} = 0; $heap->{last_adjust_volume_cmd} = $time; my $cmd = shift @{ $heap->{vol_cmd_array} }; my ($dev, $vol) = $cmd =~ /(.*) (\d+)%/; defined $vol || die; my @new_vol_cmd_array = (); for my $c ( @{ $heap->{vol_cmd_array} } ) { if ( $c =~ /(.*) (\d+)%/ ) { my $d = $1; my $v = $2; defined $v || die; if ( $d eq $dev ) { # a later cmd for this device... $cmd =~ s{(\d+)(?=%)}{$v}; # ... update using latest volume } else { push(@new_vol_cmd_array, $c); } } } # # Anybody know of a nice Perl XS module that hooks into alsa-lib...? # Using a perl API here would be less expensive and $throttle_factor # could be reduced (zeroed?)... # system("$cmd"); if ( @new_vol_cmd_array ) { @{ $heap->{vol_cmd_array} } = @new_vol_cmd_array; if ( $vol == 0 ) { # no throttle for muting $poe_kernel->yield('adjust_volume'); } else { $kernel->delay('adjust_volume', $throttle_factor); $heap->{adjust_volume_pending}++; } } else { delete $heap->{vol_cmd_array}; } } sub setvol { my $vol = shift; my $opt = shift; return unless -1 < $vol && $vol < 101; my $heap = $poe_kernel->get_active_session()->get_heap(); my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias if ( $heap->{volume} != $vol || $heap->{muted} ) { $heap->{volume} = $vol unless $opt->{mute}; $heap->{muted} = ( $vol ? 0 : 1); my $dev = $heap->{mix_device}; my @ctl = @{ $heap->{mix_control} }; for ( @ctl ) { my $vol_scaled = int ( $vol * $heap->{vol_scaling} ); my $cmd = "amixer -D $dev set $_ $vol_scaled% >/dev/null 2>&1"; $report->("++info: $a: cmd: $cmd") if $VERBOSE; push(@{ $heap->{vol_cmd_array} }, $cmd); $poe_kernel->yield('adjust_volume'); } } if ( $ROOM_CLONING_OPTION ) { my @clones = (); @clones = @{ $heap->{clones} } if defined @{ $heap->{clones} }; if ( $opt->{dead_clone} ) { $vol = 0; # will not affect clone's heap (see: $clone_heap below) @clones = ("$opt->{dead_clone}"); } if ( @clones ) { for ( 0 .. $#{ @players } ) { next if $player == $_; # I'm good... my $playername = $players[$_]{PLAYERNAME}; next unless grep {/^$playername$/} @clones; # found a clone... next if $vol == $players[$_]{vol}; # vol has changed... next if $players[$_]{vol_local_override}; # clone asserts own vol # # Clones might not have a session that controls them... So snag # control info from configs and say we're $other_a whether there is a # real player session (i.e. heap) or not... # my $dev = $players[$_]{MIX_DEVICE}; my @ctl = @{ $players[$_]{MIX_CONTROL} }; my $scale = $players[$_]{VOL_SCALING} ||= 1; $scale = 1 unless 0 < $scale && $scale < 1; my $other_a = "player_$_"; for ( @ctl ) { my $vol_scaled = int ( $vol * $scale ); my $cmd = "amixer -D $dev set $_ $vol_scaled% >/dev/null 2>&1"; $report->("++info: $other_a: cmd: $cmd") if $VERBOSE; push(@{ $heap->{vol_cmd_array} }, $cmd); $poe_kernel->yield('adjust_volume'); } # save vol on non-heap (maybe heap too)... if ( my $session_reference = $poe_kernel->alias_resolve( $other_a ) ) { if ( my $clone_heap = $session_reference->get_heap() ) { if ( $opt->{dead_clone} ) { $clone_heap->{muted} = 1; $clone_heap->{volume} = $players[$_]{vol}; } else { $clone_heap->{volume} = $vol; } } } $players[$_]{vol} = $vol; } } else { # am I a clone asserting local volume control...? if ( $players[$player]{master} ) { $players[$player]{vol_local_override} = 1; } } } } sub kill_decoder_wheel { my $heap = $poe_kernel->get_active_session()->get_heap(); my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $report->("++debug: $a: kill_decoder_wheel: number: $heap->{decoder_number}") if $DEBUG; if ( my $pid = $heap->{decoder_wheel}->PID ) { for my $p ( @{ (Proc::ProcessTable->new())->table } ) { if ( $p->ppid == $pid ) { # kill children kill TERM => $p->pid; } } kill TERM => $pid; } } sub decoder_begin { my ($kernel, $heap) = @_[KERNEL, HEAP]; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $heap->{decoder_wheel} && return; $kernel->alias_resolve('jackd_session')->get_heap()->{wheel} || return; my $dec_cmd = ''; # the command we use to decode the stream my $dec_num = ++$heap->{decoder_number}; my $rptr = $heap->{rptr} * 2; # double 'cuz we access chars (not shorts) my $stream_buffer = 'buffer -p 75 '; $stream_buffer .= '-m ' . BUFFERSIZE . ' '; $stream_buffer .= '-z ' . int( ( DECODERCHUNK + 1 ) / 1024 ) . 'k ' if int( ( DECODERCHUNK + 1 ) / 1024 ) >= 1; $stream_buffer .= ( $DEBUG ? sprintf("-d 2>/tmp/$player.%05d.buffer.out", $dec_num) : '2>/dev/null'); my $alsa_pcm = $heap->{alsa_pcm}; my $pcm_player = qq{ aplay -D $alsa_pcm }; $pcm_player .= ( $DEBUG ? sprintf("-v >/tmp/$player.%05d.aplay.out", $dec_num) : '>/dev/null'); $pcm_player .= ' 2>&1'; my $stream_decoder; my $file_extension; my $flac_replay_gain = ( $USE_FLAC_REPLAY_GAIN ? '--apply-replaygain-which-is-not-lossless=0aln3' : '' ); # # Look for a fLaC marker... It may follow a spurious byte because slim thinks # our circular buffer is an array of type short (not char) and w/ mp3 streams # it would not matter anywho... # if ( substr($heap->{buff}, $rptr, 5) =~ /^.?fLaC/s ) { # # The flac decoder's '-F' switch *should* forgive the crime of including # a spurious byte before the flac marker (and it generally does)... If, # however, that spurious byte is an 'f', flac might die... :^( really? # $heap->{buff} =~ s{^.(?=fLaC)}{\000}s; # clean up that spurious byte $report->("++info: $a: decoder_begin: flac dec_num: $dec_num rptr: $rptr") if $VERBOSE; $file_extension = 'flac'; $stream_buffer = ''; # flac seems to do enough buffering... $stream_decoder = qq{ flac $flac_replay_gain -F -sdc - }; $stream_decoder .= ( $DEBUG ? sprintf("2>/tmp/$player.%05d.flac.out", $dec_num) : '2>/dev/null'); } else { $report->("++info: $a: decoder_begin: mpeg dec_num: $dec_num rptr: $rptr") if $VERBOSE; $report->("++info: $a: decoder_begin: mpeg (not flac)") if $DEBUG; $file_extension = 'mp3'; $stream_buffer = ''; # madplay seems to do enough buffering... $stream_decoder = qq{ madplay -Q -o wave:- - }; $stream_decoder =~ s{-Q}{-v} if $DEBUG; $stream_decoder .= ( $DEBUG ? sprintf("2>/tmp/$player.%05d.madplay.out", $dec_num) : '2>/dev/null'); } if ( $DEBUG > 4 ) { # no decode, just capture to file... my $file = "/tmp/$player.$dec_num.$file_extension"; $report->("++debug: decode to file: $file"); $dec_cmd = "cat >$file"; } else { $dec_cmd = "$stream_buffer | " if $stream_buffer; $dec_cmd .= "$stream_decoder | $pcm_player"; $report->("++debug: $a: decoder: $dec_cmd") if $DEBUG; } unmute(); $heap->{buff_last_read} = undef; $heap->{decoder_stream_type} = $file_extension; $heap->{decoder_wheel} = POE::Wheel::Run->new( Program => $dec_cmd, Priority => -1, StdinFilter => POE::Filter::Stream->new(), # raw input StdinEvent => 'decoder_readbuff', StdoutEvent => 'decoder_logger', StderrEvent => 'decoder_logger', ); # # This is non-ideal... # # It would be much better if we had a POE session that registers as # a jack client and gets a callback from jack as soon as we start # playing (new ports get created). We then nail up our new conns # and give jack a go... Oh well... There's no Perl XS module for # jack at the moment and I did not feel like writing one... # # So instead, we wait a lil' while and start to poll for new conns. # When we see'em, we nail up the rest... # $kernel->delay('connect_clones', 0.40) if $ROOM_CLONING_OPTION; $kernel->yield('decoder_readbuff'); # get the ball rolling... } sub connect_clones { my ($kernel, $heap) = @_[KERNEL, HEAP]; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $ROOM_CLONING_OPTION || return; $heap->{decoder_wheel} || return; my @clones = (); @clones = @{ $heap->{clones} } if defined @{ $heap->{clones} }; return unless @clones or $heap->{clone_count}; $heap->{clone_count} = @clones; $report->("++info: $a: connect_clones: clones: ( @clones )") if $VERBOSE; my @linestack = (); my @OUT = `jack_lsp -c`; my %connections; $report->("++debug: connect_clones: jack_lsp: @OUT") if $DEBUG; OUTPUT: while ($_ = shift(@linestack) || shift @OUT) { if ( /^\S/ ) { my $output_port = $_; $output_port =~ s{\s+$}{}; INPUT: while ( my $input_port = shift @OUT ) { if ( $input_port =~ s{^\s+}{} ) { $input_port =~ s{\s+$}{}; push(@{ $connections{ $output_port } }, $input_port); next INPUT; } else { push(@linestack, $input_port); next OUTPUT; } } } } my $found_my_jack_socket = 0; my %primary_connection; # store our alsa --> jack connection my %connections_reverse; # store any jack --> alsa connections for my $port ( keys %connections ) { if ( grep {/^$port$/} @{ $players[$player]{JACK_SOCKET} } ) { # me...? my @alsa_output_ports = @{ $connections{ $port } }; return if @alsa_output_ports > 1; # don't do that... my $alsa_output_port = $alsa_output_ports[0]; return unless $alsa_output_port =~ /^alsa-jack/; for my $jack_input_port ( @{ $connections{ $alsa_output_port } } ) { # connected playbacks if ( grep {/^$jack_input_port$/} @{ $players[$player]{JACK_SOCKET} } ) { # me (again)...? $found_my_jack_socket++; $primary_connection{$alsa_output_port} = $jack_input_port; } $connections_reverse{$jack_input_port} = $alsa_output_port; } } } if ( $found_my_jack_socket == 2 ) { # alsa connects to my socket (at least)... if ( $VERBOSE ) { # ... let's see what else is connected for ( keys %connections_reverse ) { $report->("++info: $a: connections: $connections_reverse{$_} --> $_"); } } for ( 0 .. $#{ @players } ) { next if $player == $_; # I'm connected my $playername = $players[$_]{PLAYERNAME}; my @jack_socket = @{ $players[$_]{JACK_SOCKET} }; if ( grep {/^$jack_socket[0]$/} keys %connections_reverse ) { # already cloned...? next if grep {/^$playername$/} @clones; # should be cloned...? my $port = 0; # line up w/ the... for my $alsa_port ( sort keys %primary_connection ) { # ... numeric sort my $jack_port = $jack_socket[$port++]; system("jack_disconnect $alsa_port $jack_port"); $report->("++info: $a: disconnect: $alsa_port < > $jack_port") if $VERBOSE; } } else { # nope, not cloned next unless grep {/^$playername$/} @clones; # should be cloned...? my $port = 0; # line up w/ the... for my $alsa_port ( sort keys %primary_connection ) { # ... numeric sort my $jack_port = $jack_socket[$port++]; system("jack_connect $alsa_port $jack_port"); $report->("++info: $a: connect: $alsa_port --> $jack_port") if $VERBOSE; } } } } else { # poll... $report->("++info: $a: connect_clones: repost: found_my_jack_socket: 0") if $VERBOSE; $kernel->delay('connect_clones', 0.20); } } sub decoder_logger { # never used (everything is redirected) my ($kernel, $heap) = @_[KERNEL, HEAP]; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $report->("++info: $a: decoder_wheel: $_[ARG0]"); } sub decoder_readbuff { my ($kernel, $heap) = @_[KERNEL, HEAP]; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $heap->{decoder_wheel} || return; $heap->{decoder_dying} && return; my $wheel_putTimeStart; my $wheel_putTimeDelta; my $wptr = $heap->{wptr} * 2; # slim thinks we point to short (16 bit) my $rptr = $heap->{rptr} * 2; # ... but we really access chars (8 bit) my $max_possible_read; # the most we can read in the circular buff my $desired_read; # the most we actually want to read... unless ( $plugin_mode ) { # stand_alone_mode...? if ( defined $heap->{embedded_fLaC} ) { # found a fLaC marker...? if ( $rptr == $heap->{embedded_fLaC} * 2 ) { # already played up to it...? $kernel->call($_[SESSION], 'decoder_end'); # stop decoder... $heap->{embedded_fLaC} = undef; # cleanup... sleep 1 && return; } $wptr = $heap->{embedded_fLaC} * 2; # else make marker our cap } } if ( $wptr >= $rptr ) { # rptr may never pass wptr! $max_possible_read = $wptr - $rptr; } else { $max_possible_read = BUFFSIZE - ( $rptr - $wptr ); } if ( $max_possible_read > DECODERCHUNK ) { $desired_read = DECODERCHUNK; } else { $desired_read = $max_possible_read; } if ( defined $heap->{read_buffering} ) { if ( $max_possible_read > BUFFSIZE / 2 ) { delete $heap->{read_buffering}; } else { $desired_read = 0; } } my $time = Time::HiRes::time(); unless ( $desired_read ) { # anything to do...? if ( $heap->{decoder_stream_type} =~ /mp3/ ) { # remote stream underrun...? if ( defined $heap->{buff_last_read} && $time - $heap->{buff_last_read} > 2 ) { # 2 seconds of no joy... $heap->{buff_last_read} = $time; # ... whack decoder and buffer $kernel->call($_[SESSION], 'decoder_end') unless $heap->{read_buffering}++; $report->("++info: $a: readbuff: large underrun: read_buffering") if $VERBOSE; } } $report->("++debug: $a: readbuff: re-posting: decoder ahead of stream") if $DEBUG; $kernel->delay('decoder_readbuff', 0.05); # keep the ball rolling return; # ... and bail for now } $heap->{buff_last_read} = $time; my $chunk = ''; if ( $rptr + $desired_read > BUFFSIZE ) { # we would fall off... $chunk .= substr($heap->{buff}, $rptr); # ... just read to end $desired_read -= BUFFSIZE - $rptr; # now wrap around... $rptr = 0; # now wrap around... } $chunk .= substr($heap->{buff}, $rptr, $desired_read); print { $heap->{STREAMFILE} } $chunk if $DEBUG > 1; $wheel_putTimeStart = [gettimeofday] if $DEBUG > 2; $kernel->delay('decoder_stalled', DECODER_STALLED); $heap->{decoder_wheel}->put($chunk); # bypass buffered IO w/ syswrite(), see: if ( $wheel_putTimeStart ) { # vi POE/Driver/SysRW.pm +/'sub flush' $wheel_putTimeDelta = tv_interval ( $wheel_putTimeStart ); $report->("++debug: $a: wheel_putTimeDelta: $wheel_putTimeDelta"); } $rptr += $desired_read; # update rptr $rptr = 0 if $rptr == BUFFSIZE; # update rptr $DEBUG && $report->( sprintf("++debug: $a: buff: wptr: %5d rptr: %5d --> %5d", $heap->{wptr}, $heap->{rptr}, $rptr / 2) ); $heap->{rptr} = $rptr / 2; # update rptr for our acks to slim... } sub decoder_stalled { my ($kernel, $heap) = @_[KERNEL, HEAP]; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $heap->{decoder_wheel} || return; $report->("++info: $a: decoder_stalled: killing decoder_wheel"); $heap->{decoder_wheel}->kill(); sleep 3; $heap->{decoder_wheel}->kill(9); } sub decoder_end { my ($kernel, $heap) = @_[KERNEL, HEAP]; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $heap->{decoder_wheel} || return; mute(); $report->("++info: $a: decoder_end: killing decoder") if $VERBOSE; kill_decoder_wheel(); # this is fast... delete $heap->{decoder_wheel}; # cleanup and make SIGCHLD a no-op $kernel->delay('decoder_end'); # clear any pending events $kernel->delay('decoder_stalled'); # clear any pending events $heap->{decoder_dying} = 0; # clear the Sword of Damocles } sub kidhandler { my ( $kernel, $heap, $signal, $pid, $child_error ) = @_[ KERNEL, HEAP, ARG0, ARG1, ARG2 ]; my $report = \&HydraPlayer::report; my $player = $heap->{player_index}; my $a = "player_$player"; # alias $heap->{decoder_wheel} || return; $heap->{decoder_wheel}->PID == $pid || return; $kernel->sig_handled(); $heap->{stopped} && return; $report->("++info: $a: caught SIG$signal: decoder_wheel: number: $heap->{decoder_number}") if $VERBOSE; $kernel->call($_[SESSION], 'decoder_end'); if ( $child_error ) { # we only see the first process my $exit_value = $child_error >> 8; # ... but if decoder_stalled() my $signal_num = $child_error & 127; # we'll get here... my $dumped_core = $child_error & 128; $report->("++error: $a: decoder_wheel: exit_value: $exit_value"); $report->("++error: $a: decoder_wheel: signal_num: $signal_num"); $report->("++error: $a: decoder_wheel: dumped_core: $dumped_core"); $report->("++info: $a: caught SIG$signal: PLUGIN_HYDRAPLAYER_REINIT"); system('killall -q -r "aplay|jackd"'); sleep 3; system('killall -9 -q -r "aplay|jackd"'); } else { $report->("++info: $a: decoder_wheel: exit_value: 0") if $VERBOSE; } } sub diehandler { my $report = \&HydraPlayer::report; my $player = $_[HEAP]->{player_index}; my $a = "player_$player"; # alias my $signal = $_[ARG0]; my $error = $_[ARG1]->{error_str}; $_[KERNEL]->sig_handled(); $report->("++error: $a: caught SIG$signal: error: $error"); $report->("++fatal: $a: caught SIG$signal: kill TERM => $$"); kill TERM => $$; } sub sighandler { my $report = \&HydraPlayer::report; my $player = $_[HEAP]->{player_index}; my $a = "player_$player"; # alias my $signal = $_[ARG0]; $report->("++info: $a: caught SIG$signal: calling _stop()"); $_[KERNEL]->sig_handled(); $_[KERNEL]->call($_[SESSION], '_stop'); } sub _stop { my ($kernel, $heap) = @_[KERNEL, HEAP]; my $player = $heap->{player_index}; $heap->{stopped}++ && return; $kernel->alarm_remove_all(); # de-queue $kernel->select_read( $heap->{socket} ); # de-select $kernel->alias_remove( $heap->{alias} ); # de-alias if ( $heap->{decoder_wheel} ) { kill_decoder_wheel(); delete $heap->{decoder_wheel}; # de-wheel } $plugin_mode ? mute() : unmute(); Plugins::HydraPlayer::player_mode_cmd($player, 'stop'); } ##################################################################### BEGIN { # # This part of code is actually run within the slim server. # Therefore, we have full access to slim functions, e.g.: # Slim::Player::Client::getClient() # The HydraPlayer:: and Player::HydraPlayer packages have no # such access, as it either runs in stand-alone mode or as a # forked process (with multiple POE sesions). # Also see notes in initPlugin()... # package Plugins::HydraPlayer; # slim looks for this based on $0 our $version = '0.95'; # see: /usr/share/slimserver/HTML/EN/html/docs/plugins.html my $report = \&HydraPlayer::report; use POE qw(Pipe::OneWay); use POSIX qw(:sys_wait_h); eval "use Slim::Utils::Misc qw( msg )"; # eval - fails in stand-alone mode... eval "use Slim::Control::Request"; # eval - fails in stand-alone mode... # # Override slim's fade_volume() so it's a no-op... # # Firstly, we do not currently know how to do it (see i2c)... # # Secondly (and more importantly), this fixes a subtle timing bug. Sometimes # when Slim/Player/Source.pm invoked fade_volume w/ a callback function # of pauseSynced, the pause would occur just after a couple of state=play # packets got sent... This caused HydraPlayer's rptr to track past the # embedded flac header so that once play started again, we got static... # eval "use Slim::Player::Player"; eval "no warnings qw(redefine); *Slim::Player::Player::fade_volume = sub { 1 }"; my $VERSION = "$version "; # slim looks for this my $cliSet = 0; my $callbackSet = 0; my $pluginEnabled = 0; my $fifo_read; my $fifo_write; my $player_pid; my $log_open; my %player_mode_cmd_play_count; sub enabled { # are we enable-able... return ($::VERSION ge '6.5'); } sub setMode { # # see setMode() in Plugins/Rescan.pm and 'List' in docs/input.html # my $client = shift; my $method = shift; if ( defined ( my $player = getPlayer($client) ) ) { # with a software player...? my @browseMenuChoices; if ( $ROOM_CLONING_OPTION ) { $players[$player]{master} ? push(@browseMenuChoices, 'PLUGIN_HYDRAPLAYER_UNCLONE_THIS_ROOM') : Slim::Player::Source::playmode($client) eq 'play' || ( defined @{ $players[$player]{CLONES} } && scalar @{ $players[$player]{CLONES} } ) ? push(@browseMenuChoices, 'PLUGIN_HYDRAPLAYER_ROOM_FORWARD_CLONING') : push(@browseMenuChoices, 'PLUGIN_HYDRAPLAYER_ROOM_REVERSE_CLONING'); } push(@browseMenuChoices, 'PLUGIN_HYDRAPLAYER_KILL_OTHER_CLIENTS') if $KILL_OTHERS_OPTION; push(@browseMenuChoices, 'PLUGIN_HYDRAPLAYER_REINIT') if $START_JACK_SERVER; push(@browseMenuChoices, 'PLUGIN_HYDRAPLAYER_RESTART') if $RESTART_OPTION; push(@browseMenuChoices, 'PLUGIN_HYDRAPLAYER_REBOOT') if $REBOOT_OPTION; push(@browseMenuChoices, 'PLUGIN_HYDRAPLAYER_NOUSER') unless @browseMenuChoices; if ( $method eq 'pop' ) { Slim::Buttons::Common::popMode($client); } else { my %params = ( header => 'PLUGIN_HYDRAPLAYER', # the upper line stringHeader => 1, # use string() headerAddCount => 1, # (m of n) counter listRef => \@browseMenuChoices, # the internal list externRef => sub { return $_[0]->string($_[1]); }, externRefArgs => 'CV', # $client $value callback => \&modeCallback, parentMode => Slim::Buttons::Common::mode($client), ); Slim::Buttons::Common::pushMode( $client, 'INPUT.List', \%params ); } } else { Slim::Buttons::Common::popMode($client); } } my $modeCallback_value; sub modeCallback { my ( $client, $exittype ) = @_; $exittype = uc($exittype); if ( $exittype eq 'LEFT' ) { Slim::Buttons::Common::popModeRight($client); } elsif ( $exittype eq 'RIGHT' ) { my $valueRef = $client->param('valueRef'); $modeCallback_value = $$valueRef; SWITCH: for ($modeCallback_value) { /PLUGIN_HYDRAPLAYER_ROOM_REVERSE_CLONING | PLUGIN_HYDRAPLAYER_ROOM_FORWARD_CLONING/x && do { load_clone_pick_list($client); my $player = getPlayer($client); my %params = ( 'header' => \&clone_pick_upper_line, 'headerArgs' => 'C', 'listRef' => [ ( @{ $players[$player]{clone_pick_list} } ) ], 'listIndex' => $players[$player]{clone_selection}, 'overlayRef' => sub { return (undef, Slim::Display::Display::symbol('rightarrow')) }, 'overlayRefArgs' => '', 'callback' => \&clone_menu_exit_handler, 'onChange' => sub { $players[$player]{clone_selection} = $_[0]; }, 'onChangeArgs' => 'I' ); if ( @{ $players[$player]{clone_pick_list} } ) { Slim::Buttons::Common::pushModeLeft($client, 'INPUT.List', \%params); } else { $client->showBriefly( { 'line1' => $client->string('PLUGIN_HYDRAPLAYER'), 'line2' => $client->string('PLUGIN_HYDRAPLAYER_NO_CLONES') } ); } last; }; /PLUGIN_HYDRAPLAYER_UNCLONE_THIS_ROOM/ && do { $client->showBriefly( { 'line1' => $client->string('PLUGIN_HYDRAPLAYER'), 'line2' => $client->string('PLUGIN_HYDRAPLAYER_OK') } ); my $player = getPlayer($client); my $master = $players[$player]{master}; my $room = $players[$player]{PLAYERNAME}; for ( 0 .. $#{ @players } ) { if ( $players[$_]{PLAYERNAME} eq $master ) { add_remove_clone($_, $room); last; } } Slim::Buttons::Common::popModeRight($client); last; }; /PLUGIN_HYDRAPLAYER_RESTART/ && do { $client->showBriefly( { 'line1' => $client->string('PLUGIN_HYDRAPLAYER'), 'line2' => $client->string('PLUGIN_HYDRAPLAYER_OK') } ); open(DIE, "sudo slimserver-restart /tmp/slimserver-restart.log 2>&1 |"); last; }; /PLUGIN_HYDRAPLAYER_REBOOT/ && do { $client->showBriefly( { 'line1' => $client->string('PLUGIN_HYDRAPLAYER'), 'line2' => $client->string('PLUGIN_HYDRAPLAYER_OK') } ); system('sudo /sbin/init 6'); last; }; /PLUGIN_HYDRAPLAYER_KILL_OTHER_CLIENTS/ && do { $client->showBriefly( { 'line1' => $client->string('PLUGIN_HYDRAPLAYER'), 'line2' => $client->string('PLUGIN_HYDRAPLAYER_OK') } ); foreach my $other_client (Slim::Player::Client::clients()) { next if $other_client->id() eq $client->id(); next if $other_client->id() =~ /^00:00:00/; # harware only $other_client->execute(['power', 0]); } last; }; /PLUGIN_HYDRAPLAYER_REINIT/ && do { # # nuke jackd and any lingering aplay instances... # jackd_kidhandler() will power cycle all players... # players replay 'cuz the play state survives for a bit... # system('killall -q -r "aplay|jackd"'); $client->showBriefly( { 'line1' => $client->string('PLUGIN_HYDRAPLAYER'), 'line2' => $client->string('PLUGIN_HYDRAPLAYER_OK') } ); last; }; /PLUGIN_HYDRAPLAYER_NOUSER/ && do { $client->bumpRight(); last; }; /./ && do { $report->("++error: modeCallback: unknown value: $modeCallback_value"); $client->bumpRight(); last; }; } } else { $client->bumpRight(); } } sub getPlayer { # # This function returns the player number of a room (an index into @players). # my $thingy = shift; if ( my $package_name = ref $thingy ) { if ( $package_name =~ /Slim::Player::.*/ ) { if ( my $clientid = $thingy->id() ) { return getPlayer($clientid); } } } else { if ( $thingy =~ /[0-9A-F:]{17}/i ) { # a client id if ( $thingy =~ /^00:00:00/ ) { # software client id $thingy =~ s/://g; $thingy += 0; # string --> number return $thingy; } else { # a hardware client id for ( 0 .. $#{ @players } ) { if ( defined $players[$_]{SYNC_WITH_MAC} && $thingy eq $players[$_]{SYNC_WITH_MAC} ) { return $_; } } } } else { # ... it's a room name for ( 0 .. $#{ @players } ) { if ( $thingy eq $players[$_]{PLAYERNAME} ) { return $_; } } } } return undef; } sub get_hardware_ID { # # This function returns the MAC addr of a squeezebox... Software # players also have MAC addrs, but this function will not return # a value for those clients. See also get_software_ID()... # my $thingy = shift; if ( my $package_name = ref $thingy ) { if ( $package_name =~ /Slim::Player::.*/ ) { if ( my $clientid = $thingy->id() ) { if ( $clientid !~ /^00:00:00/ ) { return $clientid; } else { return get_hardware_ID($clientid); } } } } else { if ( $thingy =~ /[0-9A-F:]{17}/i ) { # a client id if ( $thingy !~ /^00:00:00/ ) { # hardware or software...? return $thingy; } else { return get_hardware_ID( getPlayer($thingy) ); } } elsif ( $thingy =~ /^\d+$/ ) { return $players[$thingy]{SYNC_WITH_MAC} if defined $players[$thingy]{SYNC_WITH_MAC}; } else { for ( 0 .. $#{ @players } ) { if ( $players[$_]{PLAYERNAME} eq $thingy ) { return $players[$_]{SYNC_WITH_MAC} if defined $players[$_]{SYNC_WITH_MAC}; last; } } } } return undef; } sub get_software_ID { # # This function returns the MAC addr of a HydraPlayer SliMP3 session. # squeezebox players also have MAC addrs, but this function will not return # a value for those clients. See also get_hardware_ID()... # my $thingy = shift; if ( my $package_name = ref $thingy ) { if ( $package_name =~ /Slim::Player::.*/ ) { if ( my $clientid = $thingy->id() ) { if ( $clientid =~ /^00:00:00/ ) { return $clientid; } else { return get_software_ID($clientid); } } } } else { if ( $thingy =~ /[0-9A-F:]{17}/i ) { # a client id if ( $thingy =~ /^00:00:00/ ) { # hardware or software...? return $thingy; } else { return get_software_ID(getPlayer($thingy)); } } elsif ( $thingy =~ /^\d+$/ ) { return join(':', unpack("(A2)*", sprintf("%012d", $thingy))); } else { for ( 0 .. $#{ @players } ) { if ( $players[$_]{PLAYERNAME} eq $thingy ) { return join(':', unpack("(A2)*", sprintf("%012d", $_))); } } } } return undef; } sub clone_squeeze_vols { my $master = shift; my $clone = shift; my @clones = (); if ( defined $clone ) { if ( defined ( my $player = getPlayer($clone) ) ) { push(@clones, $players[$player]{PLAYERNAME}); } } elsif ( defined ( my $player = getPlayer($master) ) ) { @clones = @{ $players[$player]{CLONES} } if defined @{ $players[$player]{CLONES} }; } if ( my $master_id = get_hardware_ID($master) ) { if ( my $master = Slim::Player::Client::getClient($master_id) ) { for my $room ( @clones ) { next if $players[ getPlayer($room) ]{vol_local_override}; if ( my $clone_id = get_hardware_ID( $room ) ) { if ( my $clone = Slim::Player::Client::getClient($clone_id) ) { $clone->volume( $master->volume() ); } } } } } } sub load_clone_pick_list { my $client = shift; my $player = getPlayer($client); return unless defined $player; @{ $players[$player]{clone_pick_list} } = (); for ( 0 .. $#{ @players } ) { next if $player == $_; next unless $players[$_]{JACK_SOCKET}; if ( $modeCallback_value =~ /FORWARD/ ) { next if defined @{ $players[$_]{CLONES} } && scalar @{ $players[$_]{CLONES} }; # skip if dest already has clones, or... next if $players[$_]{master} # ... if dest is some other player's clone && $players[$_]{master} ne $players[$player]{PLAYERNAME}; next if grep { /capture/ } @{ $players[$_]{JACK_SOCKET} }; } else { # doing REVERSE cloning next if $players[$_]{master}; # skip if source is some player's clone } push(@{ $players[$player]{clone_pick_list} }, $players[$_]{PLAYERNAME}); } return unless @{ $players[$player]{clone_pick_list} }; if ( !defined( $players[$player]{clone_selection} ) || $players[$player]{clone_selection} >= @{ $players[$player]{clone_pick_list} } ) { $players[$player]{clone_selection} = 0; } } sub clone_pick_upper_line { my $client = shift; my $player = getPlayer($client); my $selection_index = $players[$player]{clone_selection}; my $selection_value = $players[$player]{clone_pick_list}[$selection_index]; if ( $modeCallback_value =~ /FORWARD/ ) { if ( grep { /^$selection_value$/ } @{ $players[$player]{CLONES} } ) { return $client->string('PLUGIN_HYDRAPLAYER_UNCLONE_WITH'); } else { return $client->string('PLUGIN_HYDRAPLAYER_CLONE_WITH'); } } else { return $client->string('PLUGIN_HYDRAPLAYER_CLONE_WITH'); } } sub clone_menu_exit_handler { my ($client, $exittype) = @_; my $player = getPlayer($client); my $selection_index = $players[$player]{clone_selection}; my $selection_value = $players[$player]{clone_pick_list}[$selection_index]; $exittype = uc($exittype); if ( $exittype eq 'LEFT' ) { Slim::Buttons::Common::popModeRight($client); } elsif ($exittype eq 'RIGHT') { if ( $modeCallback_value =~ /FORWARD/ ) { my @oldlines = $client->curLines(); add_remove_clone($player, $selection_value); $client->pushLeft(\@oldlines, $client->curLines()); } else { Slim::Buttons::Common::popModeRight($client); $client->showBriefly( { 'line1' => $client->string('PLUGIN_HYDRAPLAYER'), 'line2' => $client->string('PLUGIN_HYDRAPLAYER_OK') } ); my $room = $players[$player]{PLAYERNAME}; add_remove_clone(getPlayer($selection_value), $room); Slim::Buttons::Common::popModeRight($client); } } else { return; } } sub add_remove_clone { (my $player = shift) =~ /^\d+$/ || die; my $room = shift || die; my $master_id = get_hardware_ID($player); my $room_hardware_id = get_hardware_ID($room); my $clone_action = ''; toggle_clone_master_flag($room, $players[$player]{PLAYERNAME}); if ( grep { /^$room$/ } @{ $players[$player]{CLONES} } ) { $clone_action = 'unclone'; @{ $players[$player]{CLONES} } = grep {!/^$room$/} @{ $players[$player]{CLONES} }; } else { $clone_action = 'clone'; push(@{ $players[$player]{CLONES} }, $room); } # # So far, we're just manipulating variables... Now we need to actually # do some patching in jackd. For standard room cloning, we do this in # the player session for the master room. This is triggered by sending # a message to the appropriate session via $fifo_write. This way, every # time a new decoder is started, the player session can re-patch the # clones. # # With the phonograph (jack capture ports), it's different. We can nail # up the connection and forget about it. So we do that here... # if ( grep { /capture/ } @{ $players[$player]{JACK_SOCKET} } ) { my $capture_vol = undef; if ( scalar @{ $players[$player]{CLONES} } == 1 ) { $capture_vol = 100; } elsif ( scalar @{ $players[$player]{CLONES} } == 0 ) { $capture_vol = 0; } if ( defined $capture_vol ) { # amixer ADC input volume for capture my $dev = $players[$player]{MIX_DEVICE}; my @ctl = @{ $players[$player]{MIX_CONTROL} }; for ( @ctl ) { my $cmd = "amixer -D $dev set $_ $capture_vol% >/dev/null 2>&1"; msg("HydraPlayer: player_$player: cmd: $cmd\n") if $::d_plugins; system("$cmd"); } } my @capture_ports = @{ $players[ $player ]{JACK_SOCKET} }; my @playback_ports = @{ $players[ getPlayer($room) ]{JACK_SOCKET} }; for my $port ( 0 .. 1 ) { my $capture_port = $capture_ports[$port]; my $playback_port = $playback_ports[$port]; if ( $capture_port && $playback_port ) { if ( $clone_action eq 'unclone' ) { system("jack_disconnect $capture_port $playback_port"); msg("HydraPlayer: player_$player: connect: $capture_port < > $playback_port\n") if $::d_plugins; } else { system("jack_connect $capture_port $playback_port"); msg("HydraPlayer: player_$player: connect: $capture_port --> $playback_port\n") if $::d_plugins; } } } # turn on/off amplifier when playing phono, etc.... $clone_action eq 'unclone' ? print $fifo_write "client $room_hardware_id mute\n" . "client $room_hardware_id player mode stop\n" : print $fifo_write "client $room_hardware_id unmute\n" . "client $room_hardware_id player mode play\n"; } else { # no jack capture ports # # If the clone room happens to have a squeezebox, set the volume for that # squeezebox to match the volume of the master room's squeezebox... # We do the same in the /mixer volume/ callback (this is the initial set). # clone_squeeze_vols($master_id, $room); if ( $room_hardware_id ) { # turn on/off amplifiers, etc.... $clone_action eq 'unclone' ? print $fifo_write "client $room_hardware_id player mode stop\n" : print $fifo_write "client $room_hardware_id player mode play\n"; } else { # ... even if dest room has no conttoller... $clone_action eq 'unclone' ? print $fifo_write "room $room player mode stop\n" : print $fifo_write "room $room player mode play\n"; } print $fifo_write "client $master_id $clone_action $room\n"; } } sub toggle_clone_master_flag { # # This subroutine is invoked from both clone_menu_exit_handler() and # plugin_event(). The purpose is to keep track of a room's cloned status # so that a room can easily tell if it's some other room's clone (and what # room's clone). Also, if being uncloned, to strip the vol_local_override # flag... # # We keep track of these flags on the player config hash (%players) # because, here, in package Plugins::HydraPlayer, we have no heaps. # # When invoked from plugin_event(), we do have heaps but do not use them # as alias_resolve()->get_heap() is a bit cumbersome and it's nice to be # able to re-use this subroutine. # # Remember that both plugin_event() and clone_menu_exit_handler() have # access to %players and agree on it's value at startup. But, any mods # to %players (as we are doing here) will not be seen in the other's copy # of %players because our player sessions are fork()'ed away from slim's # pid (where we are a plugin)... So, these settings end up getting # duplicated in both processes as both pids end up in here on clone # status changes... # my $room = shift || die; my $master = shift || die; if ( defined ( my $player = getPlayer($room) ) ) { if ( $players[$player]{master} ) { delete $players[$player]{vol_local_override}; # ... autonomy is lost delete $players[$player]{master}; # ... unflag it as a clone } else { $players[$player]{master} = $master; # ... flag it as a clone } } } sub undo_all_cloning_for_player { my $player = shift; my $opt = shift; my $do_not_undo = ( defined $opt->{do_not_undo} ? $opt->{do_not_undo} : '' ); if ( $do_not_undo !~ /master/ && $players[$player]{master} ) { my $master = $players[$player]{master}; my $room = $players[$player]{PLAYERNAME}; add_remove_clone(getPlayer($master), $room); } if ( $do_not_undo !~ /CLONES/ && defined @{ $players[$player]{CLONES} } ) { my @clones = @{ $players[$player]{CLONES} }; map { add_remove_clone($player, $_) } @clones; } } sub player_mode_cmd { # # Here we run an external command (if defined) when a player mode # change occurs. We keep a counter to avoid repeat invocations if # multiple players share the same "on" command and to avoid doing # the "off" command until all players that share that command are # in stop mode... # my $player = shift; my $mode = shift || return; my $a = "player_$player"; # alias defined $players[$player]{MODE_PLAY_CMD} || return; $mode eq 'play' ? ++$player_mode_cmd_play_count{ $players[$player]{MODE_PLAY_CMD} } : --$player_mode_cmd_play_count{ $players[$player]{MODE_PLAY_CMD} }; if ( ( $player_mode_cmd_play_count{ $players[$player]{MODE_PLAY_CMD} } == 0 && $mode eq 'stop' ) || ( $player_mode_cmd_play_count{ $players[$player]{MODE_PLAY_CMD} } == 1 && $mode eq 'play' ) ) { my $cmd = ( $mode eq 'play' ? $players[$player]{MODE_PLAY_CMD} : $players[$player]{MODE_STOP_CMD} ) || return; $cmd .= ( $DEBUG ? " >/tmp/$a.player_mode_cmd_$mode.out 2>&1" : ' >/dev/null 2>&1' ); { no strict 'refs'; open(int(rand($^T)), "$cmd |"); } # a throw-away fork... $DEBUG && $report->("++debug: $a: player_mode_cmd: cmd: $cmd"); } } sub getFunctions { # # It is not required that we return something here... It's not clear to me # when this should be used versus using setMode() and modeCallback(). In # fact, this can be used in conjunction w/ that methodology (see # Plugins/Rescan.pm for an example of this). Also, see defaultMap() for # how to override the IR button mapping. # return undef; } sub strings { return qq{\n} . qq{PLUGIN_HYDRAPLAYER\n} . qq{\tEN\tHydraPlayer\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_KILL_OTHER_CLIENTS\n} . qq{\tEN\tKill all other players\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_UNCLONE_THIS_ROOM\n} . qq{\tEN\tUnclone this room\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_ROOM_FORWARD_CLONING\n} . qq{\tEN\tClone audio to other rooms\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_ROOM_REVERSE_CLONING\n} . qq{\tEN\tClone audio from other room\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_NO_CLONES\n} . qq{\tEN\tThere are no other rooms\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_CLONE_WITH\n} . qq{\tEN\tgo right to clone with\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_UNCLONE_WITH\n} . qq{\tEN\tgo right to unclone with\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_RESTART\n} . qq{\tEN\tRestart the Slim server\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_REBOOT\n} . qq{\tEN\tReboot the Slim platform\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_REINIT\n} . qq{\tEN\tRe-init the Jack subsystem\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_NOUSER\n} . qq{\tEN\tNo UI available...\n} . qq{\n} . qq{PLUGIN_HYDRAPLAYER_OK\n} . qq{\tEN\tOK...\n} . qq{\n}; } sub getDisplayName { return 'PLUGIN_HYDRAPLAYER'; } sub shutdownPlugin { $::d_plugins && msg("HydraPlayer: shutdownPlugin()\n"); close $fifo_read; close $fifo_write; if ( $player_pid ) { for ( 1 .. 10 ) { # why do I need multiple kills...? kill TERM => $player_pid; my $pid = waitpid($player_pid, WNOHANG); last if $pid == $player_pid; sleep 1; } undef $player_pid; } $pluginEnabled = 0; } sub initPlugin { # # The Slim server expects this function to return, so we have to fork in # order to run our player sessions. Before forking, we'll create a fifo # for IPC. After forking, we'll request interesting events from the Slim # server and send them to the player session via a fifo handler session. # $::d_plugins && msg("HydraPlayer: initPlugin()\n"); if ( -f '/etc/debian_version' ) { # running debian...? if ( system('grep -q audio.*slimserver /etc/group') ) { my $msg = "++fatal: user slimserver not in group audio, try: " . "'adduser slimserver audio'"; $report->("$msg"); die "$msg"; } } ( $fifo_read, $fifo_write ) = POE::Pipe::OneWay->new(); die "could not make fifo pipe: $!" unless defined $fifo_read and defined $fifo_write; $player_pid = fork(); unless ($player_pid) { # child die "couldn't fork: $!" unless defined $player_pid; umask 0; chdir("/") || die "$0: ERROR: cannot change directory: $!\n"; # do not setpgrp() because we want slim's kill signals... # see $SIG{'CHLD'} = \&REAPER; # in /usr/share/perl5/Slim/bootstrap.pm delete $SIG{CHLD}; # allow POE to handle this in its own inimitable way # slim also sets these... map { delete $SIG{$_} } qw / HUP INT QUIT TERM __WARN__ /; # do not close STDIN because it horks POE::Wheel::Run... unless ( $DEBUG ) { # any output is logged by slim... close STDOUT; close STDERR; } $report->("++info: starting sessions in plugin_mode"); HydraPlayer::start_jackd_session(); HydraPlayer::start_player_sessions(); start_fifo_session(); POE::Kernel->run(); exit 0; } unless ( $cliSet ) { # create a custom CLI command because... # ... the existing "player name" command is read-only (really?) Slim::Control::Request::addDispatch( ['hydraplayer', 'setplayername', '_id', '_name'], [0, 0, 0, \&cliSetPlayerName] ); # create a custom CLI command to adjust DEBUG level Slim::Control::Request::addDispatch( ['hydraplayer', 'setdebuglevel', '_level'], [0, 0, 0, \&cliSetDebugLevel] ); $cliSet = 1; } unless ( $callbackSet ) { # see: `perldoc /usr/share/perl5/Slim/Control/Request.pm` Slim::Control::Request::subscribe( \&myCallbackFunction, [[ qw/ button client mixer pause play playerpref playlist power show sleep stop / ]] ); $callbackSet = 1; } $pluginEnabled = 1; } sub cliSetPlayerName { my $request = shift; my $id = $request->getParam('_id'); my $name = $request->getParam('_name'); $::d_plugins && msg("HydraPlayer: cliSetPlayerName: $id $name\n"); my $client = Slim::Player::Client::getClient($id); $client->name($name); $request->setStatusDone(); } sub cliSetDebugLevel { my $request = shift; my $level = $request->getParam('_level'); $::d_plugins && msg("HydraPlayer: cliSetDebugLevel: $level\n"); SWITCH: for ($level) { /^\d+$/ && do { last; }; /^-$/ && do { $level = 0; last; }; /./ && do { $report->("++error: cliSetDebugLevel: unknown level: $level"); undef $level; last; }; } if ( defined $level ) { $DEBUG = $level; $VERBOSE = $level; $report->("++info: cliSetDebugLevel: $level"); print $fifo_write "eval_snippet \$DEBUG=\$VERBOSE=$level"; } $request->setStatusDone(); } sub myCallbackFunction { # IPC_get_message handles $fifo_read... return unless $pluginEnabled; my $request = shift; my $client = $request->client(); # object my $clientid = $request->clientid(); # string my $command = $request->getRequestString(); my $button_code = $request->getParam('_buttoncode'); $button_code ||= $client->lastirbutton() if $command =~ /button/; my $currentSong = Slim::Player::Playlist::url($client); my $currentMode = Slim::Player::Source::playmode($client); my @messages = (); if ( $DEBUG ) { $report->("++debug: control request: $clientid: command: $command") if $command; $report->("++debug: control request: $clientid: button_code: $button_code") if $button_code; $report->("++debug: control request: $clientid: currentSong: $currentSong") if $currentSong; $report->("++debug: control request: $clientid: currentMode: $currentMode") if $currentMode; } SWITCH: for ($command) { /button/ && do { SWITCH: for ($button_code) { # see /usr/share/slimserver/IR/Default.map /^play$/ && do { # # If a room is presently the clone of some other room (i.e. it has # a "master"), and the human is now pressing the play button for # that room's squeezebox contoller, then it's game over for that # clone relationship (if the room is a master (ie. it has CLONES), # that's no problem)... # undo_all_cloning_for_player(getPlayer($client), { do_not_undo => 'CLONES' }) if $ROOM_CLONING_OPTION; last; }; /./ && do { $DEBUG && $report->("++debug: control request: $clientid: $button_code (not used)"); last; }; } last; }; /playlist jump/ && do { push(@messages, "mute"); last; }; /playlist newsong/ && do { # get the details of the current track, send to player for logging... my $formatString = 'TITLE by ARTIST from ALBUM'; my $song = Slim::Player::Playlist::song($client); my $current_track = Slim::Music::Info::displayText($client, $song, $formatString); push(@messages, "playlist newsong $current_track"); last; }; /mixer volume/ && do { push(@messages, "volume " . $client->volume()); if ( $ROOM_CLONING_OPTION ) { # # Some rooms have squeezebox controllers, others may not... All rooms get their # actual volume (amixer) settings applied in setvol(). The purpose here is to set # squeezebox volume preferences for clones that have them. This is to prevent # jarring volume jumps once cloning is undone and the room is used independently # again... Invoking a client's volume() method does not seem to cause "mixer # volume" callbacks so we do not have to worry about returning back to here # inappropriatly... # my $player = getPlayer($client); if ( $players[$player]{master} ) { # clone asserting local volume control...? $players[$player]{vol_local_override} = 1; } else { # master changing volume (update all clones) clone_squeeze_vols($client); } } last; }; /client new/ && do { my $menu_found = 0; my $updateMenu = 0; if ( $clientid =~ /^00:00:00/ ) { # a HydraPlayer $client->prefSet('maxBitrate', 0); # see underMax() in TranscodingHelper.pm $client->prefSet('autobrightness', 0); } else { # a hardware client push(@messages, "volume " . $client->volume() . ' heap_only'); if ( $SET_DISPLAYMODES ) { # see 'my @modes' in Slim/Display/Squeezebox2.pm # index 5 gets mode 8: Spectrum Analyzer and Remaining Time $client->prefSet('playingDisplayModes', [0,1,2,3,7,8]); $client->prefSet('playingDisplayMode', 5); } if ( $SET_SCREENSAVER_SNOW ) { $client->prefSet('snowStyle', 4); # just plain snow $client->prefSet('snowStyleOff', 4); # just plain snow $client->prefSet('screensavertimeout', 30); $client->prefSet('idlesaver', 'SCREENSAVER.snow'); $client->prefSet('offsaver', 'SCREENSAVER.snow'); } if ( defined ( my $player = getPlayer($client) ) ) { # with a software player...? # SqueezeNetwork and HydraPlayer don't mix (because squeezebox is control only)... for (my $i = $client->prefGetArrayMax('menuItem'); $i >= 0; $i--) { my $value = $client->prefGet('menuItem', $i); $DEBUG && $report->("++debug: control request: $clientid: client new: menuItem: $value"); if ( $value =~ /SQUEEZENETWORK_CONNECT/ ) { $DEBUG && $report->("++debug: control request: $clientid: client new: delete: SQUEEZENETWORK_CONNECT"); $client->prefDelete('menuItem', $i); $updateMenu++; } if ( $value =~ /PLUGIN_HYDRAPLAYER/ ) { $DEBUG && $report->("++debug: control request: $clientid: client new: found: HydraPlayer"); $menu_found++; } } if ( $SET_HYDRAPLAYER_MENU ) { unless ( $menu_found ) { $DEBUG && $report->("++debug: control request: $clientid: client new: adding: PLUGIN_HYDRAPLAYER"); $client->prefPush('menuItem', 'PLUGIN_HYDRAPLAYER'); $updateMenu++; } } if ( $updateMenu ) { Slim::Buttons::Home::updateMenu($client); $client->update(); } } } last; }; /^play$/ && do { # warning - this does not always show up when it should last; }; # ... so "player mode play" happens in server_read()... /pause/ && do { # this is like x2 play (scan_rew|scan_fwd)... no funcionar! $client->execute(['stop']); # ... anybody know how to make flac streams pause/scan-able? last; }; /stop/ && do { push(@messages, "mute"); push(@messages, "player mode stop"); last; }; /power/ && do { if ( $clientid !~ /^00:00:00/ ) { # this is a hardware client if ( defined ( my $player = getPlayer($client) ) ) { # with a software player...? my $player_soft_mac = get_software_ID($player); if ( my $synced_soft_client = Slim::Player::Client::getClient($player_soft_mac) ) { $DEBUG && $report->("++debug: control request: $clientid: $player_soft_mac execute stop"); $synced_soft_client->execute(['stop']); # no resurection on sync_player() } undo_all_cloning_for_player($player); # unclone everything... } } push(@messages, "power " . ($client->power ? '1' : '0' )); last; }; /./ && do { $DEBUG && $report->("++debug: control request: $clientid: $command (not used)"); last; }; } for ( @messages ) { $DEBUG && $report->("++debug: control request: $clientid: fifo_write: $_"); print $fifo_write "client $clientid $_\n"; } } sub start_fifo_session { POE::Session->create( inline_states => { _start => \&IPC_start, get_message => \&IPC_get_message, diehandler => \&IPC_diehandler, sighandler => \&IPC_sighandler, _stop => \&IPC_stop, } ); } sub IPC_start { $_[KERNEL]->select_read( $fifo_read, "get_message" ); $_[KERNEL]->sig( DIE => 'diehandler' ); $_[KERNEL]->sig( HUP => 'sighandler' ); $_[KERNEL]->sig( INT => 'sighandler' ); $_[KERNEL]->sig( TERM => 'sighandler' ); $_[KERNEL]->sig( QUIT => 'sighandler' ); } sub IPC_get_message { # myCallbackFunction handles $fifo_write... my ( $kernel, $heap, $handle ) = @_[ KERNEL, HEAP, ARG0 ]; while ( <$handle> ) { chomp(my $message = $_); my $action = ''; $DEBUG && $report->("++info: fifo_read: $message"); SWITCH: for ($message) { /client ([:0-9a-f]{17})/ && do { my $clientid = $1; if ( defined ( my $player = getPlayer($clientid) ) ) { my $a = "player_$player"; # alias SWITCH: for ($message) { /playlist newsong (.*)/ && do { $action = "playlist newsong $1"; last; }; /player mode (.*)/ && do { $action = "player mode $1"; last; }; /((?:un)?clone) (.*)/ && do { $action = "$1 $2"; last; }; /volume (.*)/ && do { $action = "volume $1"; last; }; /unmute/ && do { $action = "volume unmute"; last; }; /mute/ && do { $action = "volume mute"; last; }; /power (.*)/ && do { $action = "power $1"; last; }; /./ && do { $report->("++error: IPC_get_message: unknown client action: $message"); last; }; } if ( $action ) { $VERBOSE && $report->("++info: fifo_read: posting: $a ($clientid): $action"); $kernel->post($a, 'plugin_event', $action); } last; } last; }; /room (\S*)/ && do { my $room = "$1"; SWITCH: for ($message) { /player mode (.*)/ && do { # # We see /room .* player mode .*/ messages when the player has no # associated squeezebox (i.e. no player session), so, we handle # running mode stop/start (e.g. amplifier power toggle) here... # These messages are sent from within add_remove_clone(). See # case "player mode" in plugin_event() for more comments. # my $mode = $1; $heap->{"${room}_mode"} eq $mode && return; # no change... $heap->{"${room}_mode"} = $mode; # save new mode player_mode_cmd(getPlayer($room), $mode); last; }; /./ && do { $report->("++error: IPC_get_message: unknown room action: $message"); last; }; } last; }; /eval_snippet (.*)/ && do { my $snippet = "$1"; eval "$snippet"; last; }; /./ && do { $report->("++error: IPC_get_message: unknown message: $message"); last; }; } } } sub IPC_diehandler { my $report = \&HydraPlayer::report; my $signal = $_[ARG0]; my $error = $_[ARG1]->{error_str}; $_[KERNEL]->sig_handled(); $report->("++error: IPC session: caught SIG$signal: error: $error"); $report->("++fatal: IPC session: caught SIG$signal: kill TERM => $$"); kill TERM => $$; } sub IPC_sighandler { my $report = \&HydraPlayer::report; my $signal = $_[ARG0]; $_[KERNEL]->sig_handled(); $report->("++info: IPC session: caught SIG$signal: calling _stop()"); $_[KERNEL]->call($_[SESSION], '_stop'); } sub IPC_stop { my ($kernel, $heap) = @_[KERNEL, HEAP]; $heap->{stopped}++ && return; $kernel->alarm_remove_all(); # de-queue $kernel->select_read( $fifo_read ); # de-select close $fifo_read; close $fifo_write; } } 1;