This is my own implementation of an autoplay plugin – based on the WaveInput plugin – for the squeezebox (LMS) software, written using literate programming so it’s easy to publish. It’s not quite a tutorial, but maybe someone will find it helpful.
Use case
I would like to automatically play a particular stream whenever a player is idle. I need this, so that the player connects to the output from amazon’s echo dot once playback of e.g. a radio stream has stopped.
Requirements:
- Detect idle players
- Start playing a stream after X seconds of idleness
- Provide means to specify…
- which players to match
- which stream to play
- after how many idle seconds the stream should be played
- whether players should be synchronized if more than one match
Autoplay plugin for squeezebox
The following are the required files for this plugin to work. Simply put them in a new directory inside your squeezebox’s plugin directory. Mine is at: /var/lib/squeezeserver/cache/InstalledPlugins/Autoplay
Plugin installation file
This is copied and adjusted from the WaveInput plugin. Apparently it is necessary.
install.xml
<?xml version="1.0"?> <extension> <name>PLUGIN_AUTOPLAY</name> <module>Plugins::Autoplay::Plugin</module> <playerMenu>RADIO</playerMenu> <version>0.01</version> <description>PLUGIN_AUTOPLAY_DESC</description> <creator>bpa</creator> <defaultState>enabled</defaultState> <homepageURL></homepageURL> <optionsURL>plugins/Autoplay/settings/basic.html</optionsURL> <!-- <icon>plugins/Autoplay/html/images/waveinput.png</icon> --> <type>2</type><!-- type=extension --> <targetApplication> <id>Squeezecenter</id> <minVersion>7.3</minVersion> <maxVersion>7.*</maxVersion> </targetApplication> <targetPlatform>Linux</targetPlatform> </extension>
Plugin strings file
The strings file contains a mapping from uppercase identifiers to translations for various languages.
strings.txt
# String file for Autoplay plugin PLUGIN_AUTOPLAY EN Autoplay PLUGIN_AUTOPLAY_DESC EN Autoplay is a plugin that automatically starts playing a stream after a player is idle for a certain time PLUGIN_AUTOPLAY_CLIENTREGEX EN Client regex PLUGIN_AUTOPLAY_CLIENTREGEX_DESC EN Regular expression that matches the client name which should automatically be started. PLUGIN_AUTOPLAY_IDLETIME EN Idle time PLUGIN_AUTOPLAY_IDLETIME_DESC EN Amount of time that must pass before auto-starting the specified stream. PLUGIN_AUTOPLAY_STREAMURL EN Stream URL PLUGIN_AUTOPLAY_STREAMURL_DESC EN The stream to start playing when idle for too long. PLUGIN_AUTOPLAY_SYNC EN Synchronization PLUGIN_AUTOPLAY_SYNC_DESC EN If enabled, syncronize all matching players.
Plugin settings file
The settings file is responsible for:
- serving and handling the HTTP based settings page
- providing defaults for settings
Settings.pm
package Plugins::Autoplay::Settings; use strict; use base qw(Slim::Web::Settings); use Slim::Utils::Log; use Slim::Utils::Prefs; use Slim::Player::Client; use Slim::Utils::OSDetect; my $prefs = preferences('plugin.autoplay'); my $log = logger('plugin.autoplay'); my $osdetected = Slim::Utils::OSDetect::OS(); my %defaults = ( clientregex => "Wohnzimmer", sync => 0, streamurl => "wavin:LoopbackAudioRecording", idletime => 5 ); $log->debug("Settings called"); sub new { my $class = shift; $log->debug("New Settings"); $class->SUPER::new; } sub name { # assumes at least SC 7.0 if ( substr($::VERSION,0,3) lt 7.4 ) { return Slim::Web::HTTP::protectName('PLUGIN_AUTOPLAY'); } else { # $::noweb to detect TinySC or user with no web interface if (!$::noweb) { return Slim::Web::HTTP::CSRF->protectName('PLUGIN_AUTOPLAY'); } } } sub page { # assumes at least SC 7.0 if ( substr($::VERSION,0,3) lt 7.4 ) { return Slim::Web::HTTP::protectURI('plugins/Autoplay/settings/basic.html'); } else { # $::noweb to detect TinySC or user with no web interface if (!$::noweb) { return Slim::Web::HTTP::CSRF->protectURI('plugins/Autoplay/settings/basic.html'); } } } sub prefs { $log->debug("Prefs called"); return ($prefs, qw( clientregex ), qw( streamurl ), qw( idletime ), qw( sync )); } sub handler { my ($class, $client, $params) = @_; $log->debug("Handler called"); if ($params->{'saveSettings'}) { $prefs->set('clientregex', qr/$params->{'clientregex'}/); $prefs->set('sync', $params->{'sync'}); $prefs->set('streamurl', $params->{'streamurl'}); $prefs->set('idletime', $params->{'idletime'}); } return $class->SUPER::handler( $client, $params ); } sub setDefaults { my $force = shift; foreach my $key (keys %defaults) { if (!defined($prefs->get($key)) || $force) { $log->debug("Missing pref value: Setting default value for $key: " . $defaults{$key}); $prefs->set($key, $defaults{$key}); } } } sub init { my $self = shift; $log->debug("Initializing settings"); setDefaults(0); } 1;
Plugin main file
The main plugin file is responsible for the actual autoplay logic. It…
- sets a timer which gets periodically refreshed to look for clients
- starts playing a selected stream for every client that is idle and matches a setting. It optionally syncs players if multiple match
This must reside inside a Plugin.pm file according to: http://wiki.slimdevices.com/index.php/SqueezeCenter_7_Plugins
Plugin.pm
# # A plugin to automatically start playing a certain stream # use strict; package Plugins::Autoplay::Plugin; use base qw(Slim::Plugin::OPMLBased); use Slim::Utils::Log; use Slim::Utils::Prefs; use Slim::Utils::Timers; use Slim::Player::Client; use Plugins::Autoplay::Settings; # create log categogy before loading other modules my $log = Slim::Utils::Log->addLogCategory({ 'category' => 'plugin.autoplay', 'defaultLevel' => 'ERROR', # 'defaultLevel' => 'INFO', 'description' => getDisplayName(), }); use Slim::Utils::Misc; my $prefs = preferences('plugin.autoplay'); ## -- settings -- my $checkInterval = 2; my $lastIdleTimeCheck = 0; $prefs->setValidate({ "validator" => 'intlimit', 'low' => 1, 'high' => 6000}, 'idletime'); sub isMatchingIdleClient { my $client = shift; my $clientRegex = $prefs->get("clientregex"); if ( ($client->name() =~ ${clientRegex}) && $client->power() && !$client->isPlaying() ) { return 1; } else { return 0; } } sub getMatchingIdleClients { my @clients; for my $client (Slim::Player::Client::clients()) { if( isMatchingIdleClient( $client ) ) { push @clients, $client; } } return @clients; } sub checkClients { my $streamUrl = $prefs->get("streamurl"); my $idleTimeBeforeAutoplay = $prefs->get("idletime"); my @clients = getMatchingIdleClients(); if ( scalar ( @clients ) > 0 ) { if ( $lastIdleTimeCheck == 0 ) { $lastIdleTimeCheck = Time::HiRes::time(); } else { my $elapsedTime = Time::HiRes::time() - $lastIdleTimeCheck; if ( $idleTimeBeforeAutoplay < $elapsedTime ) { # sync clients if( $prefs->get('sync') ) { my $mainClient = $clients[0]; my @otherClients = @clients[1 .. scalar(@clients) - 1]; $log->debug("Syncing first player " . $mainClient->name() . " with " . scalar(@otherClients) . " more."); for my $otherClient (@otherClients) { if(!$otherClient->isSynced()) { $log->debug("Syncing " . $mainClient->name() . " with " . $otherClient->name()); $mainClient->controller()->sync($otherClient, 1); } } } for my $client (@clients) { $log->debug("Player " . $client->name() . " currently powered, but idle for " . $elapsedTime); if( !$prefs->get('sync') || $client == $clients[0] ) { # power on/off because sometimes the stream gets corrupted after a while $client->power(0); $client->power(1); $log->info("Auto-starting stream: " . $streamUrl . " on client " . $client->name()); $client->execute(["playlist", "play", $streamUrl]); } $lastIdleTimeCheck = 0; # interesting: # $client->controller()->activePlayers(); } } } } Slim::Utils::Timers::setTimer(undef, Time::HiRes::time() + $checkInterval, \&checkClients); } ################################ ### Plugin Interface ########### ################################ sub initPlugin { my $class = shift; $log->info("Initialising " . $class->_pluginDataFor('version')); $class->SUPER::initPlugin(@_); Plugins::Autoplay::Settings->new($class); Plugins::Autoplay::Settings->init(); # Slim::Control::Request::subscribe( \&pauseCallback, [['pause']] ); Slim::Utils::Timers::killTimers( undef, \&checkClients ); Slim::Utils::Timers::setTimer(undef, Time::HiRes::time() + $checkInterval, \&checkClients); return 1; } sub shutdownPlugin { # Slim::Control::Request::unsubscribe(\&pauseCallback); Slim::Utils::Timers::killTimers( undef, \&checkClients ); return; } sub getDisplayName() { return('PLUGIN_AUTOPLAY') } 1; # Local Variables: # tab-width:4 # indent-tabs-mode:t # End:
HTML Setting page template
The HTML template is displayed as the setting page for the plugin.
HTML/EN/plugins/Autoplay/settings/basic.html
[% PROCESS settings/header.html %] [% WRAPPER settingSection %] [% WRAPPER setting title="PLUGIN_AUTOPLAY_CLIENTREGEX" desc="PLUGIN_AUTOPLAY_CLIENTREGEX_DESC" %] <input type="text" name="pref_clientregex" value="[% prefs.clientregex %]" /> [% END %] [% WRAPPER setting title="PLUGIN_AUTOPLAY_SYNC" desc="PLUGIN_AUTOPLAY_SYNC_DESC" %] <input type="checkbox" name="pref_sync" [% IF prefs.sync == 1 %] checked="checked" [% END %] value="1" /> [% END %] [% WRAPPER setting title="PLUGIN_AUTOPLAY_STREAMURL" desc="PLUGIN_AUTOPLAY_STREAMURL_DESC" %] <input type="text" name="pref_streamurl" value="[% prefs.streamurl %]" /> [% END %] [% WRAPPER setting title="PLUGIN_AUTOPLAY_IDLETIME" desc="PLUGIN_AUTOPLAY_IDLETIME_DESC" %] <input type="text" name="pref_idletime" value="[% prefs.idletime %]" /> [% END %] [% END %] [% PROCESS settings/footer.html %]