Просмотр исходного кода

Merge branch 'master' of github.com:profplump/UberZach-Integration

Zachary Kotlarek 13 лет назад
Родитель
Сommit
50b7587372

+ 161 - 213
dmx/Audio.pm

@@ -2,17 +2,22 @@
 use strict;
 use warnings;
 use POSIX;
-use File::Touch;
 use File::Basename;
-use Time::HiRes qw( usleep sleep time );
 use IPC::System::Simple;
+use Time::HiRes qw( usleep sleep time );
 
 # Package name
 package Audio;
 
 # Config
 my $MEDIA_PATH = `~/bin/video/mediaPath`;
-my %FILES      = ();
+
+# App config
+my $DATA_DIR   = DMX::dataDir();
+my $QT_FILE    = $DATA_DIR . 'QT_PLAYER';
+my $QT_SOCK    = $QT_FILE . '.socket';
+my $WAIT_DELAY = 0.1;
+my $MAX_WAIT   = 5 / $WAIT_DELAY;
 
 # Debug
 my $DEBUG = 0;
@@ -20,131 +25,101 @@ if ($ENV{'DEBUG'}) {
 	$DEBUG = 1;
 }
 
-# Execute the provided string with osascript
-sub runApplescript($) {
-	my ($script) = @_;
-	if ($DEBUG) {
-		print STDERR 'AppleScript: ' . $script . "\n";
-	}
+# State
+my %FILES = ();
+my $SOCK  = undef();
 
-	my $retval = IPC::System::Simple::capture('osascript', '-e', $script);
-	$retval =~ s/[\r\n]$//;
-	if ($DEBUG) {
-		print STDERR "\tAppleScript result: " . $retval . "\n";
-	}
-
-	return $retval;
-}
+# Send the provided arguments to the QT socket
+sub sendCmd(@) {
 
-# Set or get the system output volume
-sub systemVolume($) {
-	my ($new) = @_;
-	if ($DEBUG) {
-		print STDERR "Audio::systemVolume()\n";
+	# Connect the socket as needed
+	if (!$SOCK) {
+		$SOCK = DMX::clientSock($QT_SOCK);
 	}
 
-	if ($new) {
-		runApplescript('set volume output volume ' . $new);
-	} else {
-		return runApplescript('get output volume of (get volume settings)');
+	# Ensure the data is compatible with the encoding scheme
+	foreach my $part (@_) {
+		if ($part =~ /\|/) {
+			die('Invalid QT command component: ' . $part . "\n");
+		}
 	}
-}
 
-# Set or get a document's volume
-sub volume($$) {
-	my ($name, $new) = @_;
+	# Build the command stirng
+	my $str = join('|', @_);
 	if ($DEBUG) {
-		print STDERR 'Audio::volume(): ' . $name . "\n";
-	}
-	if (!loaded($name)) {
-		die('Invalid QT document: ' . $name . "\n");
+		print STDERR 'Sending QT_PLAYER command: ' . $str . "\n";
 	}
 
-	if ($new) {
-		runApplescript('tell application "QuickTime Player" to set audio volume of document ' . $FILES{$name}->{'name'} . ' to ' . $new);
-	} else {
-		return runApplescript('tell application "QuickTime Player" to get audio volume of document ' . $FILES{$name}->{'name'});
-	}
+	# Send the command
+	$SOCK->send($str)
+	  or die("Unable to send to QT socket\n");
 }
 
-# Add an load a document
-sub addLoad($$) {
-	my ($name, $path) = @_;
+# Execute the provided string with osascript
+sub runApplescript($) {
+	my ($script) = @_;
 	if ($DEBUG) {
-		print STDERR 'Audio::addLoad(): ' . $name . "\n";
+		print STDERR 'AppleScript: ' . $script . "\n";
 	}
-	add($name, $path);
-	load($name);
-}
 
-# Add a document to the available list
-sub add($$) {
-	my ($name, $path) = @_;
+	my $retval = IPC::System::Simple::capture('osascript', '-e', $script);
+	$retval =~ s/[\r\n]$//;
 	if ($DEBUG) {
-		print STDERR 'Audio::add(): ' . $name . ', ' . $path . "\n";
-	}
-
-	# Construct an absolute file path
-	if (!($path =~ /^\//)) {
-		$path = $MEDIA_PATH . '/' . $path;
-	}
-
-	# Validate
-	if (!-r $path) {
-		die('Invalid audio path: ' . $path . "\n");
+		print STDERR "\tAppleScript result: " . $retval . "\n";
 	}
 
-	# We only support one document per name
-	drop($name);
-
-	# Append the array
-	my %tmp = ('path' => $path);
-	$FILES{$name} = \%tmp;
+	return $retval;
 }
 
-# Drop a document from the available list, unloaded as necessary
-sub drop($) {
+# Is the named document loaded?
+sub loaded($) {
 	my ($name) = @_;
-	if ($DEBUG) {
-		print STDERR 'Audio::drop(): ' . $name . "\n";
-	}
+	parseList();
+	return exists($FILES{$name});
+}
 
-	# Stop and unload as necessary
+# Load a document
+sub load($$) {
+	my ($name, $path) = @_;
+
+	# Unload as needed
 	if (loaded($name)) {
-		stop($name);
 		unload($name);
 	}
 
-	# Drop as necessary
-	if (available($name)) {
-		delete($FILES{$name});
-	}
-}
+	# Request load
+	sendCmd('LOAD', $name, $path);
 
-# Is the named document loaded?
-sub loaded($) {
-	my ($name) = @_;
-	if ($DEBUG) {
-		print STDERR 'Audio::loaded(): ' . $name . "\n";
-	}
-
-	if (available($name) && defined($FILES{$name}->{'name'})) {
-		return 1;
+	# Wait for the load to register
+	my $count = 0;
+	while (!loaded($name)) {
+		Time::HiRes::sleep($WAIT_DELAY);
+		$count++;
+		if ($count > $MAX_WAIT) {
+			die('Unable to open file: ' . $name . "\n");
+		}
 	}
-	return 0;
 }
 
-# Is the named document configured?
-sub available($) {
+# Unload the named document
+sub unload($) {
 	my ($name) = @_;
 	if ($DEBUG) {
-		print STDERR 'Audio::available(): ' . $name . "\n";
+		print STDERR 'Audio::loadAudio(): ' . $name . "\n";
 	}
 
-	if (defined($name) && defined($FILES{$name})) {
-		return 1;
+	# Request unload
+	sendCmd('UNLOAD', $name);
+
+	# Wait for the unload to register
+	my $count = 0;
+	while (loaded($name)) {
+		Time::HiRes::sleep($WAIT_DELAY);
+		$count++;
+		if ($count > $MAX_WAIT) {
+			die('Unable to close file: ' . $name . "\n");
+		}
 	}
-	return 0;
 }
 
 # Play the named document, waiting for completion
@@ -154,12 +129,12 @@ sub play($) {
 		print STDERR 'Audio::play(): ' . $name . "\n";
 	}
 
-	# Start playback
-	background($name);
+	# Ensure the file is loaded
+	dieUnloaded($name);
 
-	# Wait for it to complete
 	my @cmd = ('tell application "QuickTime Player"');
-	push(@cmd, 'repeat while playing of document ' . $FILES{$name}->{'name'} . ' = true');
+	push(@cmd, 'play document ' . $FILES{$name});
+	push(@cmd, 'repeat while playing of document ' . $FILES{$name} . ' = true');
 	push(@cmd, 'delay 0.1');
 	push(@cmd, 'end repeat');
 	push(@cmd, 'end tell');
@@ -172,12 +147,39 @@ sub background($) {
 	if ($DEBUG) {
 		print STDERR 'Audio::background(): ' . $name . "\n";
 	}
-	if (!loaded($name)) {
-		die('Invalid QT document: ' . $name . "\n");
+
+	# Ensure the file is loaded
+	dieUnloaded($name);
+
+	runApplescript('tell application "QuickTime Player" to play document ' . $FILES{$name});
+}
+
+# Pause the named document
+sub pause($) {
+	my ($name) = @_;
+	if ($DEBUG) {
+		print STDERR 'Audio::pause(): ' . $name . "\n";
 	}
 
-	# Start playback
-	runApplescript('tell application "QuickTime Player" to play document ' . $FILES{$name}->{'name'});
+	# Ensure the file is loaded
+	dieUnloaded($name);
+
+	runApplescript('tell application "QuickTime Player" to pause document ' . $FILES{$name});
+}
+
+# Stop playback of the named document
+sub stop($) {
+	my ($name) = @_;
+	if ($DEBUG) {
+		print STDERR "Audio::stopAudio()\n";
+	}
+
+	# Ensure the file is loaded
+	dieUnloaded($name);
+
+	# Stop and rewind
+	runApplescript('tell application "QuickTime Player" to stop document ' . $FILES{$name});
+	runApplescript('tell application "QuickTime Player" to set current time of document ' . $FILES{$name} . ' to 0');
 }
 
 # Is the named document playing?
@@ -187,8 +189,10 @@ sub playing($) {
 		print STDERR 'Audio::playing(): ' . $name . "\n";
 	}
 
-	# Ask QT if the document is playing
-	# Note that while QT has a "playing" property it is only true when rate == 1.0
+	# Ensure the file is loaded
+	dieUnloaded($name);
+
+	# QT has a "playing" property but it is only true when rate == 1.0
 	my $rate = rate($name);
 	if ($rate > 0) {
 		return 1;
@@ -202,14 +206,14 @@ sub position($$) {
 	if ($DEBUG) {
 		print STDERR 'Audio::position(): ' . $name . "\n";
 	}
-	if (!loaded($name)) {
-		die('Invalid QT document: ' . $name . "\n");
-	}
+
+	# Ensure the file is loaded
+	dieUnloaded($name);
 
 	if ($new) {
-		runApplescript('tell application "QuickTime Player" to set current time of document ' . $FILES{$name}->{'name'} . ' to ' . $new);
+		runApplescript('tell application "QuickTime Player" to set current time of document ' . $FILES{$name} . ' to ' . $new);
 	} else {
-		return runApplescript('tell application "QuickTime Player" to get current time of document ' . $FILES{$name}->{'name'});
+		return runApplescript('tell application "QuickTime Player" to get current time of document ' . $FILES{$name});
 	}
 }
 
@@ -219,134 +223,78 @@ sub rate($$) {
 	if ($DEBUG) {
 		print STDERR 'Audio::rate(): ' . $name . "\n";
 	}
-	if (!loaded($name)) {
-		die('Invalid QT document: ' . $name . "\n");
-	}
+
+	# Ensure the file is loaded
+	dieUnloaded($name);
 
 	if ($new) {
-		runApplescript('tell application "QuickTime Player" to set rate of document ' . $FILES{$name}->{'name'} . ' to ' . $new);
+		runApplescript('tell application "QuickTime Player" to set rate of document ' . $FILES{$name} . ' to ' . $new);
 	} else {
-		return runApplescript('tell application "QuickTime Player" to get rate of document ' . $FILES{$name}->{'name'});
+		return runApplescript('tell application "QuickTime Player" to get rate of document ' . $FILES{$name});
 	}
 }
 
-# Pause the named document
-sub pause($) {
-	my ($name) = @_;
+# Set or get a document's volume
+sub volume($$) {
+	my ($name, $new) = @_;
 	if ($DEBUG) {
-		print STDERR 'Audio::pause(): ' . $name . "\n";
-	}
-	if (!loaded($name)) {
-		die('Invalid QT document: ' . $name . "\n");
+		print STDERR 'Audio::volume(): ' . $name . "\n";
 	}
 
-	# Pause playback
-	runApplescript('tell application "QuickTime Player" to pause document ' . $FILES{$name}->{'name'});
-}
-
-# Stop playback of the named document or all documents
-sub stop($) {
-	my ($name) = @_;
-	if ($DEBUG) {
-		print STDERR "Audio::stopAudio()\n";
-	}
+	# Ensure the file is loaded
+	dieUnloaded($name);
 
-	# Allow control of all documents or a specific document
-	my $doc = 'every document';
-	if (loaded($name)) {
-		$doc = 'document ' . $FILES{$name}->{'name'};
+	if ($new) {
+		runApplescript('tell application "QuickTime Player" to set audio volume of document ' . $FILES{$name} . ' to ' . $new);
+	} else {
+		return runApplescript('tell application "QuickTime Player" to get audio volume of document ' . $FILES{$name});
 	}
-
-	# Stop and rewind all QT documents
-	runApplescript('tell application "QuickTime Player" to stop ' . $doc);
-	runApplescript('tell application "QuickTime Player" to set current time of ' . $doc . ' to 0');
-
-	# Bring Plex back to the front
-	runApplescript('tell application "Plex" to activate');
 }
 
-# Unload the named document
-sub unload($) {
-	my ($name) = @_;
+# Set or get the system output volume
+sub systemVolume($) {
+	my ($new) = @_;
 	if ($DEBUG) {
-		print STDERR 'Audio::loadAudio(): ' . $name . "\n";
-	}
-	if (!available($name)) {
-		die('Invalid QT document: ' . $name . "\n");
+		print STDERR "Audio::systemVolume()\n";
 	}
 
-	# Nothing to do if the file isn't loaded
-	if (!loaded($name)) {
-		return;
+	if ($new) {
+		runApplescript('set volume output volume ' . $new);
+	} else {
+		return runApplescript('get output volume of (get volume settings)');
 	}
-
-	# Close to document
-	runApplescript('tell application "QuickTime Player" to close document ' . $FILES{$name}->{'name'});
-
-	# Delete our local handle
-	delete($FILES{$name}->{'name'});
 }
 
-# Load the named document
-sub load($) {
-	my ($name) = @_;
-	if ($DEBUG) {
-		print STDERR 'Audio::loadAudio(): ' . $name . "\n";
-	}
-	if (!available($name)) {
-		die('Invalid QT document: ' . $name . "\n");
-	}
-
-	# Unload first, if we're already loaded
-	if (loaded($name)) {
-		unload($name);
-	}
-
-	# Find out how many files are already open
-	my $count = runApplescript('tell application "QuickTime Player" to count items of every document');
-
-	# Open the file
-	IPC::System::Simple::system('open', '-a', 'QuickTime Player', $FILES{$name}->{'path'});
-
-	# Wait for QT to load the new file
-	my @cmd = ('tell application "QuickTime Player"');
-	push(@cmd, 'set endDate to current date + (3/60 * minutes)');
-	push(@cmd, 'repeat while (count items of every document) <= ' . $count . ' and current date < endDate');
-	push(@cmd, 'delay 0.1');
-	push(@cmd, 'end repeat');
-	push(@cmd, 'get document 1');
-	push(@cmd, 'end tell');
-	my $doc = runApplescript(join("\n", @cmd));
-
-	# Clean up the document name
-	$doc =~ s/^\s*document //;
-	$doc =~ s/\s+$//;
-	$doc =~ s/\"/\\\"/g;
-	$doc = '"' . $doc . '"';
+# Stop playback on all documents
+sub stopAll() {
+	sendCmd('STOP');
+}
 
-	# Save the document handle
-	$FILES{$name}->{'name'} = $doc;
+# Reset the entire QT Player process
+sub reset() {
+	sendCmd('RESET');
+}
 
-	# Bring Plex back to the front
-	runApplescript('tell application "Plex" to activate');
+# Read and parse the qt_player file list
+sub parseList() {
+	%FILES = ();
+	open(my $fh, $QT_FILE)
+	  or die('Unable to open QT_PLAYER file: ' . $! . "\n");
+	while (<$fh>) {
+		my ($name, $handle) = $_ =~ /^([^|]+)\|\s*(\".+\")\s*$/;
+		if (!$name || !$handle) {
+			die('Invalid QT_PLAYER line: ' . $_);
+		}
+		$FILES{$name} = $handle;
+	}
+	close($fh);
 }
 
-# Get things into a reasonable state
-sub init() {
-	if ($DEBUG) {
-		print STDERR "Audio::init()\n";
+sub dieUnloaded($) {
+	my ($name) = @_;
+	if (!loaded($name)) {
+		die('File not loaded: ' . $name . "\n");
 	}
-
-	# Close all QT documents and relaunch QT Player
-	runApplescript('tell application "QuickTime Player" to close every document');
-	runApplescript('tell application "QuickTime Player" to quit');
-	sleep(1);
-	IPC::System::Simple::system('open', '-a', 'QuickTime Player');
-
-	# Always load and play the SILENCE file (for sanity and general init)
-	add('SILENCE', 'DMX/Silence.wav');
-	load('SILENCE');
-	play('SILENCE');
 }
 
 # Always return true

+ 1 - 10
dmx/DMX.pm

@@ -5,7 +5,6 @@ use POSIX;
 use File::Temp;
 use IO::Select;
 use IO::Socket::UNIX;
-use Sys::Hostname;
 
 # Package name
 package DMX;
@@ -32,15 +31,7 @@ my %CHANNEL_ADJ = (
 
 # Sanity check
 if (!-d $DATA_DIR) {
-
-	# Allow use on dev systems without the full environment
-	my $hostname = Sys::Hostname::hostname();
-	if ($hostname =~ /baldwin/i || $hostname =~ /archer/i) {
-		print STDERR 'Ignoring missing DATA_DIR due to hostname: ' . $hostname . ". Using TEMP_DIR directly...\n";
-		$DATA_DIR = $TEMP_DIR;
-	} else {
-		die('Data directory not available: ' . $DATA_DIR . "\n");
-	}
+	die('Data directory not available: ' . $DATA_DIR . "\n");
 }
 
 # State

+ 2 - 6
dmx/launchd/com.uberzach.ola.rave.plist

@@ -8,10 +8,6 @@
 	<array>
 		<string>Loki.local</string>
 		<string>loki.uberzach.com</string>
-		<string>Beddy.local</string>
-		<string>beddy.uberzach.com</string>
-		<string>Heady.local</string>
-		<string>heady.uberzach.com</string>
 	</array>
 	<key>RunAtLoad</key>
 	<true/>
@@ -21,10 +17,10 @@
 	<dict>
 		<key>OtherJobEnabled</key>
 		<dict>
-			<key>com.uberzach.ola.dmx</key>
-			<true/>
 			<key>com.uberzach.state</key>
 			<true/>
+			<key>com.uberzach.qt_player</key>
+			<true/>
 		</dict>
 	</dict>
 	<key>ProgramArguments</key>

+ 42 - 0
dmx/launchd/com.uberzach.qt_player.plist

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>LimitLoadToSessionType</key>
+	<string>Aqua</string>
+	<key>LimitLoadToHosts</key>
+	<array>
+		<string>Loki.local</string>
+		<string>loki.uberzach.com</string>
+		<string>Beddy.local</string>
+		<string>beddy.uberzach.com</string>
+		<string>Heady.local</string>
+		<string>heady.uberzach.com</string>
+	</array>
+	<key>RunAtLoad</key>
+	<true/>
+	<key>Label</key>
+	<string>com.uberzach.qt_player</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>OtherJobEnabled</key>
+		<dict>
+			<key>com.uberzach.state</key>
+			<true/>
+		</dict>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/dmx/qt_player.pl</string>
+	</array>
+	<!--
+		<key>EnvironmentVariables</key>
+		<dict>
+			<key>DEBUG</key>
+			<string>1</string>
+		</dict>
+		<key>StandardErrorPath</key>
+		<string>/Users/tv/com.uberzach.qt_player_errlog.txt</string>
+	-->
+</dict>
+</plist>

+ 2 - 0
dmx/launchd/com.uberzach.rifftrax.plist

@@ -23,6 +23,8 @@
 		<dict>
 			<key>com.uberzach.state</key>
 			<true/>
+			<key>com.uberzach.qt_player</key>
+			<true/>
 		</dict>
 	</dict>
 	<key>ProgramArguments</key>

+ 1 - 7
dmx/launchd/com.uberzach.state.plist

@@ -18,13 +18,7 @@
 	<key>Label</key>
 	<string>com.uberzach.state</string>
 	<key>KeepAlive</key>
-	<dict>
-		<key>OtherJobEnabled</key>
-		<dict>
-			<key>com.uberzach.projector</key>
-			<true/>
-		</dict>
-	</dict>
+	<true/>
 	<key>ProgramArguments</key>
 	<array>
 		<string>/Users/tv/bin/video/dmx/state.pl</string>

+ 3 - 3
dmx/projectorPower.pl

@@ -136,11 +136,11 @@ while (1) {
 	}
 
 	# Calculate the color mode
-	# PLAY for playback
+	# PLAY for normal playback
 	# HIGH when playing and LIGHTS
 	# LOW for audio, and when we're half way to the timeout (to save the bulb)
 	# HIGH when the GUI is up
-	# LOW if we haven't figured out what else to do
+	# PLAY if we haven't figured out what else to do
 	if ($newState eq 'PLAY') {
 		if ($exists{'LIGHTS'}) {
 			$color = $COLOR_HIGH;
@@ -154,7 +154,7 @@ while (1) {
 	} elsif ($exists{'GUI'}) {
 		$color = $COLOR_HIGH;
 	} else {
-		$color = $COLOR_LOW;
+		$color = $COLOR_PLAY;
 	}
 
 	# Force updates on a periodic basis

+ 228 - 0
dmx/qt_player.pl

@@ -0,0 +1,228 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use File::Basename;
+use IPC::System::Simple;
+
+# Local modules
+use Cwd qw(abs_path);
+use File::Basename qw(dirname);
+use lib dirname(abs_path($0));
+use DMX;
+use Audio;
+
+# User config
+my $MEDIA_PATH = `~/bin/video/mediaPath`;
+my %FILES      = ();
+
+# Prototypes
+sub init();
+sub load($$);
+sub unload($);
+sub stop();
+sub printFiles();
+sub dieUnloaded($);
+
+# App config
+my $DATA_DIR    = DMX::dataDir();
+my $OUTPUT_FILE = $DATA_DIR . 'QT_PLAYER';
+my $STATE_SOCK  = $OUTPUT_FILE . '.socket';
+my $DELAY       = 5;
+my $MAX_CMD_LEN = 16384;
+
+# Debug
+my $DEBUG = 0;
+if ($ENV{'DEBUG'}) {
+	$DEBUG = 1;
+}
+
+# Clear our output file
+if (-e $OUTPUT_FILE) {
+	unlink($OUTPUT_FILE);
+}
+
+# Reset QT Player
+init();
+
+# Sockets
+my $SELECT = DMX::selectSock($STATE_SOCK);
+
+# Loop forever
+while (1) {
+
+	# Wait for commands
+	{
+		my @ready_clients = $SELECT->can_read($DELAY);
+		foreach my $fh (@ready_clients) {
+
+			# Ensure we won't block on recv()
+			$fh->blocking(0);
+
+			# Read
+			while (defined($fh->recv(my $text, $MAX_CMD_LEN))) {
+
+				# Parse
+				my @argv = split(/\|/, $text);
+
+				# Ignore empty commands
+				if (scalar(@argv) < 1) {
+					next;
+				}
+
+				# Dispatch commands
+				my $cmd = shift(@argv);
+				if ($cmd eq 'LOAD' && scalar(@argv) == 2) {
+					load($argv[0], $argv[1]);
+				} elsif ($cmd eq 'UNLOAD' && scalar(@argv) == 1) {
+					unload($argv[0]);
+				} elsif ($cmd eq 'STOP') {
+					stop();
+				} elsif ($cmd eq 'RESET') {
+					init();
+				} else {
+					warn('Ignored invalid command: ' . $text . "\n");
+				}
+			}
+		}
+	}
+
+	# Keep our list of files in-sync with reality
+	foreach my $name (keys(%FILES)) {
+		eval { Audio::runApplescript('tell application "QuickTime Player" to get document ' . $FILES{$name}); };
+		if ($@) {
+			warn('File went missing: ' . $name . "\n");
+			delete($FILES{$name});
+			printFiles();
+		}
+	}
+
+	# Ensure "silence" is always available -- reset if it goes away
+	if (!exists($FILES{'SILENCE'})) {
+		die("We need only the SILENCE\n");
+	}
+}
+
+# Get things into a reasonable state
+sub init() {
+	if ($DEBUG) {
+		print STDERR "init()\n";
+	}
+
+	# Reset our local state
+	%FILES = ();
+
+	# Close all QT documents and relaunch QT Player
+	Audio::runApplescript('tell application "QuickTime Player" to close every document');
+	Audio::runApplescript('tell application "QuickTime Player" to quit');
+	sleep(1);
+	IPC::System::Simple::system('open', '-a', 'QuickTime Player');
+
+	# Always load and play the SILENCE file (for sanity and general init)
+	load('SILENCE', 'DMX/Silence.wav');
+
+	# Bring Plex back to the front
+	activatePlex();
+}
+
+# Open the named file and record the handle
+sub load($$) {
+	my ($name, $path) = @_;
+	if ($DEBUG) {
+		print STDERR 'load(): ' . $name . ', ' . $path . "\n";
+	}
+
+	# Build a reasonable path
+	if (!($path =~ /^\//)) {
+		$path = $MEDIA_PATH . '/' . $path;
+	}
+
+	# Ensure the file exists
+	if (!-r $path) {
+		warn('Invalid audio file path: ' . $path . "\n");
+		return;
+	}
+
+	# Ignore duplicate loads
+	if (exists($FILES{$name})) {
+		warn('Ignoring load request for existing file: ' . $name . "\n");
+		return;
+	}
+
+	# Find out how many files are already open
+	my $count = Audio::runApplescript('tell application "QuickTime Player" to count items of every document');
+
+	# Open the file
+	# It would be nice to use AppleScript here, so we can get a more definitive name, but the open() call is borked
+	IPC::System::Simple::system('open', '-a', 'QuickTime Player', $path);
+
+	# Wait for QT to load the new file
+	# To avoid indefinate hangs this will give up after 5 seconds. When it does the results are undefined.
+	my @cmd = ('tell application "QuickTime Player"');
+	push(@cmd, 'set endDate to current date + (5/60 * minutes)');
+	push(@cmd, 'repeat while (count items of every document) <= ' . $count . ' and current date < endDate');
+	push(@cmd, 'delay 0.1');
+	push(@cmd, 'end repeat');
+	push(@cmd, 'get document 1');
+	push(@cmd, 'end tell');
+	my $doc = Audio::runApplescript(join("\n", @cmd));
+
+	# Clean up the document name
+	$doc =~ s/^\s*document //;
+	$doc =~ s/\s+$//;
+	$doc =~ s/\"/\\\"/g;
+	$doc = '"' . $doc . '"';
+
+	# Save the document handle
+	$FILES{$name} = $doc;
+	printFiles();
+
+	# Bring Plex back to the front
+	activatePlex();
+}
+
+# Close the named file
+sub unload($) {
+	my ($name) = @_;
+	if ($DEBUG) {
+		print STDERR 'unload(): ' . $name . "\n";
+	}
+
+	# Ignore files that don't exist
+	if (!exists($FILES{$name})) {
+		warn('No such file loaded: ' . $name . "\n");
+		return;
+	}
+
+	# Close to document
+	Audio::runApplescript('tell application "QuickTime Player" to close document ' . $FILES{$name});
+
+	# Delete our handle
+	delete($FILES{$name});
+	printFiles();
+}
+
+# Stop and rewind all QT documents
+sub stop() {
+	Audio::runApplescript('tell application "QuickTime Player" to stop every document');
+	Audio::runApplescript('tell application "QuickTime Player" to set current time of every document to 0');
+}
+
+# Update the output file list
+sub printFiles() {
+	my ($fh, $tmp) = File::Temp::tempfile($OUTPUT_FILE . '.XXXXXXXX', 'UNLINK' => 0);
+	foreach my $file (keys(%FILES)) {
+		print $fh $file . '|' . $FILES{$file} . "\n";
+	}
+	close($fh);
+	rename($tmp, $OUTPUT_FILE);
+}
+
+# Bring Plex to the front, if it's already running
+sub activatePlex() {
+	my @cmd = ('tell application "System Events"');
+	push(@cmd, 'if exists process "Plex" then');
+	push(@cmd, 'tell application "Plex" to activate');
+	push(@cmd, 'end if');
+	push(@cmd, 'end tell');	
+	Audio::runApplescript(join("\n", @cmd));
+}

+ 9 - 34
dmx/rave.pl

@@ -23,27 +23,17 @@ sub lsr_loop($$$);
 
 # User config
 my $MEDIA_PATH = `~/bin/video/mediaPath` . '/DMX';
-my @CHANNELS   = ();
+my @CHANNELS   = (1, 2, 4, 5, 6, 7, 8, 9, 13, 14, 15);
 my %EFFECTS    = ();
 my %FILES      = ();
 
-# Host-specific config
-my $HOST = Sys::Hostname::hostname();
-if ($HOST =~ /loki/i) {
+# Red Alert
+$FILES{'RED_ALERT'} = 'DMX/Red Alert.mp3';
+$EFFECTS{'RED_ALERT'} = { 'cmd' => \&red_alert };
 
-	# Rave-controlled channels
-	push(@CHANNELS, 1, 2, 4, 5, 6, 7, 8, 9, 13, 14, 15);
-
-	# Red Alert
-	$FILES{'RED_ALERT'} = 'DMX/Red Alert.mp3';
-	$EFFECTS{'RED_ALERT'} = { 'cmd' => \&red_alert };
-
-	# Light-switch rave
-	$EFFECTS{'LSR'} = { 'cmd' => \&lsr_init, 'next' => \&lsr_run, 'loop' => \&lsr_loop, 'done' => \&lsr_done };
-	$FILES{'LSR'} = 'DMX/Rave.mp3';
-} elsif ($HOST =~ /beddy/i) {
-} elsif ($HOST =~ /heady/i) {
-}
+# Light-switch rave
+$EFFECTS{'LSR'} = { 'cmd' => \&lsr_init, 'next' => \&lsr_run, 'loop' => \&lsr_loop, 'done' => \&lsr_done };
+$FILES{'LSR'} = 'DMX/Rave.mp3';
 
 # Utility prototypes
 sub ampWait($$$);
@@ -67,9 +57,8 @@ if ($ENV{'DEBUG'}) {
 }
 
 # Load all our audio files
-Audio::init();
 foreach my $file (keys(%FILES)) {
-	Audio::addLoad($file, $FILES{$file});
+	Audio::load($file, $FILES{$file});
 }
 
 # Sockets
@@ -130,7 +119,7 @@ while (1) {
 			if ($DEBUG) {
 				print STDERR "Ending background processing\n";
 			}
-			Audio::stop(undef());
+			Audio::stopAll();
 			kill(SIGTERM, $PID);
 		}
 
@@ -208,20 +197,6 @@ while (1) {
 # ======================================
 # Utility routines
 # ======================================
-sub runApplescript($) {
-	my ($script) = @_;
-	if ($DEBUG) {
-		print STDERR 'Running AppleScript: ' . $script . "\n";
-	}
-
-	my $retval = capture('osascript', '-e', $script);
-	if ($DEBUG) {
-		print STDERR "\tAppleScript result: " . $retval . "\n";
-	}
-
-	return $retval;
-}
-
 sub ampWait($$$) {
 	my ($name, $exists, $params) = @_;
 	if ($DEBUG) {

+ 3 - 3
dmx/rifftrax.pl

@@ -154,7 +154,7 @@ while (1) {
 			Audio::systemVolume($VOLUME_STD);
 
 			# Close the audio file
-			Audio::drop('RIFF');
+			Audio::unload('RIFF');
 			$riff = 0;
 
 			# Warm fuzzies
@@ -176,7 +176,7 @@ while (1) {
 			DMX::say('RiffTrax initiated');
 
 			# Load and start the audio file
-			Audio::addLoad('RIFF', $RIFFS{$riff}->{'file'});
+			Audio::load('RIFF', $RIFFS{$riff}->{'file'});
 			playRiff();
 
 			# Set volume when we load -- riffs should be louder than normal system sounds
@@ -195,7 +195,7 @@ while (1) {
 			$new = $RIFFS{$riff}->{'name'};
 		}
 		my $old = $new;
-		if ($riffLast > 0) {
+		if ($riffLast) {
 			$old = $RIFFS{$riffLast}->{'name'};
 		}
 

+ 93 - 63
dmx/state.pl

@@ -2,10 +2,8 @@
 use strict;
 use warnings;
 use IO::Select;
-use IO::Socket::UNIX;
 use File::Basename;
 use File::Temp qw( tempfile );
-use Sys::Hostname;
 
 # Local modules
 use Cwd qw(abs_path);
@@ -17,21 +15,21 @@ use DMX;
 sub mtime($);
 
 # User config
-my $STATE_TIMEOUT = 180;
 my $MEDIA_PATH    = `~/bin/video/mediaPath`;
-my %MON_FILES     = (
-	'GUI'     => 'STATUS_GUI',
-	'PLAYING' => 'STATUS_PLAYING',
-	'RIFF'    => 'STATUS_VALUE',
-);
-my $DISPLAY = '<NONE>';
+my %DISPLAY_DEVS  = ('TV' => 1, 'PROJECTOR' => 1);
+my $DISPLAY       = '<NONE>';
+my $STATE_TIMEOUT = 180;
+my %MON_FILES     = ();
 
-# Host-specific config
-my $HOST = Sys::Hostname::hostname();
-if ($HOST =~ /loki/i) {
+# Available state files
+{
 
-	# Display device
-	$DISPLAY = 'PROJECTOR';
+	# Plex
+	$MON_FILES{'GUI'}     = 'STATUS_GUI';
+	$MON_FILES{'PLAYING'} = 'STATUS_PLAYING';
+
+	# RiffTrax
+	$MON_FILES{'RIFF'} = 'STATUS_VALUE';
 
 	# Motion detection
 	$MON_FILES{'MOTION'}    = 'MTIME';
@@ -64,30 +62,11 @@ if ($HOST =~ /loki/i) {
 	$MON_FILES{'AUDIO'}       = 'STATUS_VALUE';
 	$MON_FILES{'AUDIO_STATE'} = 'STATUS_VALUE';
 
-} elsif ($HOST =~ /beddy/i) {
-
-	# Display device
-	$DISPLAY = 'TV';
-
 	# TV
-	#$MON_FILES{'TV'}       = 'STATUS';
-	#$MON_FILES{'TV_VOL'}   = 'STATUS_VALUE';
-	#$MON_FILES{'TV_INPUT'} = 'STATUS_VALUE';
-
-} elsif ($HOST =~ /heady/i) {
-
-	# Display device
-	$DISPLAY = '<NONE>';
+	$MON_FILES{'TV'}     = 'STATUS';
+	$MON_FILES{'TV_VOL'} = 'STATUS_VALUE';
 }
-
-# App config
-my $SOCK_TIMEOUT = 5;
-my $DATA_DIR     = DMX::dataDir();
-my $CMD_FILE     = $DATA_DIR . 'STATE.socket';
-my $MAX_CMD_LEN  = 4096;
-my $RESET_CMD    = $ENV{'HOME'} . '/bin/video/dmx/reset.sh';
-my $PUSH_TIMEOUT = 20;
-my %EXTRAS       = (
+my %EXTRAS = (
 	'PLAYING' => {
 		'URL'      => qr/^\<li\>Filename\:(.+)$/m,
 		'YEAR'     => qr/^\<li\>Year\:(\d+)/m,
@@ -102,6 +81,14 @@ my %EXTRAS       = (
 	}
 );
 
+# App config
+my $SOCK_TIMEOUT = 5;
+my $DATA_DIR     = DMX::dataDir();
+my $CMD_FILE     = $DATA_DIR . 'STATE.socket';
+my $MAX_CMD_LEN  = 4096;
+my $RESET_CMD    = $ENV{'HOME'} . '/bin/video/dmx/reset.sh';
+my $PUSH_TIMEOUT = 20;
+
 # Debug
 my $DEBUG = 0;
 if ($ENV{'DEBUG'}) {
@@ -117,20 +104,6 @@ if (!$DELAY) {
 # Socket init
 my $select = DMX::stateSocket($CMD_FILE);
 
-# Reset all dependents at starts
-system($RESET_CMD);
-
-# Subscribers
-my @subscribers = ();
-
-# State
-my $state      = 'INIT';
-my $stateLast  = $state;
-my $status     = '';
-my $statusLast = $status;
-my $updateLast = 0;
-my $pushLast   = 0;
-
 # Add the extras to the main file list
 foreach my $file (keys(%EXTRAS)) {
 	if (exists($MON_FILES{$file})) {
@@ -144,12 +117,13 @@ foreach my $file (keys(%EXTRAS)) {
 my %files = ();
 foreach my $file (keys(%MON_FILES)) {
 	my %tmp = (
-		'name'   => basename($file),
-		'type'   => $MON_FILES{$file},
-		'path'   => $DATA_DIR . $file,
-		'update' => 0,
-		'status' => 0,
-		'last'   => 0,
+		'name'      => basename($file),
+		'type'      => $MON_FILES{$file},
+		'path'      => $DATA_DIR . $file,
+		'update'    => 0,
+		'status'    => 0,
+		'last'      => 0,
+		'available' => 0,
 	);
 
 	# Allow absolute paths to override the $DATA_DIR path
@@ -164,6 +138,30 @@ foreach my $file (keys(%MON_FILES)) {
 	$files{$file} = \%tmp;
 }
 
+# Delete any existing input files, to ensure our state is reset
+foreach my $file (values(%files)) {
+	if (-e $file->{'path'}) {
+		if ($DEBUG) {
+			print STDERR 'Deleting ' . $file->{'name'} . ': ' . $file->{'path'} . "\n";
+		}
+		unlink($file->{'path'});
+	}
+}
+
+# Reset all dependents at starts
+system($RESET_CMD);
+
+# Subscribers
+my @subscribers = ();
+
+# State
+my $state      = 'INIT';
+my $stateLast  = $state;
+my $status     = '';
+my $statusLast = $status;
+my $updateLast = 0;
+my $pushLast   = 0;
+
 # Loop forever
 while (1) {
 
@@ -190,11 +188,7 @@ while (1) {
 		}
 
 		# Open the new socket
-		my $sub = IO::Socket::UNIX->new(
-			'Peer'    => $path,
-			'Type'    => SOCK_DGRAM,
-			'Timeout' => $SOCK_TIMEOUT
-		);
+		my $sub = eval { DMX::clientSock($path); };
 		if (!$sub) {
 			print STDERR 'Unable to open socket: ' . $path . ": ${@}\n";
 			next;
@@ -239,6 +233,42 @@ while (1) {
 		$file->{'last'}   = $file->{'status'};
 		$file->{'status'} = 0;
 
+		# Track available/unavailable files
+		{
+			my $wasAvailable = $file->{'available'};
+			$file->{'available'} = -r $file->{'path'} ? 1 : 0;
+			if ($wasAvailable != $file->{'available'}) {
+
+				# Reset and extras for this file
+				if (exists($EXTRAS{ $file->{'name'} })) {
+					foreach my $extra (keys(%{ $EXTRAS{ $file->{'name'} } })) {
+						my $name = $file->{'name'} . '_' . $extra;
+						$files{$name}{'last'}   = $files{$name}{'status'};
+						$files{$name}{'status'} = 0;
+					}
+				}
+
+				# Determine which display device we have (if any)
+				if (exists($DISPLAY_DEVS{ $file->{'name'} })) {
+					$DISPLAY = $file->{'name'};
+				}
+
+				if ($DEBUG) {
+					if ($file->{'available'}) {
+						print STDERR 'Added file ' . $file->{'name'} . ': ' . $file->{'path'} . "\n";
+					} else {
+						print STDERR 'Dropped file ' . $file->{'name'} . ': ' . $file->{'path'} . "\n";
+
+					}
+				}
+			}
+		}
+
+		# Skip unavailable files
+		if (!$file->{'available'}) {
+			next;
+		}
+
 		# Record the last update for STATUS and MTIME files
 		if ($file->{'type'} =~ /^STATUS/ || $file->{'type'} =~ /^MTIME/) {
 			$file->{'update'} = mtime($file->{'path'});
@@ -282,12 +312,12 @@ while (1) {
 			}
 
 			# Handle extras, if any
-			if ($EXTRAS{ $file->{'name'} }) {
+			if (exists($EXTRAS{ $file->{'name'} })) {
 				foreach my $extra (keys(%{ $EXTRAS{ $file->{'name'} } })) {
 
 					my $name = $file->{'name'} . '_' . $extra;
 					$files{$name}{'last'}   = $files{$name}{'status'};
-					$files{$name}{'status'} = '';
+					$files{$name}{'status'} = 0;
 					$files{$name}{'update'} = $file->{'update'};
 
 					if ($text =~ $EXTRAS{ $file->{'name'} }{$extra}) {

+ 10 - 2
plexMonitor/plexMonitor.pl

@@ -48,6 +48,9 @@ if (!-d $DATA_DIR) {
 	mkdir($DATA_DIR);
 }
 
+# Build our file name
+my $OUT_FILE = $DATA_DIR . '/' . $MODE;
+
 # Loop forever (unless no delay is set)
 my $data     = '';
 my $dataLast = '';
@@ -66,12 +69,17 @@ do {
 		}
 	}
 
+	# Ensure the output file exists
+	if (!-r $OUT_FILE) {
+		$changed = 1;
+	}
+
 	# If anything changed, save the data to disk
 	if ($changed) {
-		my ($fh, $tmp) = tempfile($DATA_DIR . '/' . $MODE . '.XXXXXXXX', 'UNLINK' => 0);
+		my ($fh, $tmp) = tempfile($OUT_FILE . '.XXXXXXXX', 'UNLINK' => 0);
 		print $fh $data;
 		close($fh);
-		rename($tmp, $DATA_DIR . '/' . $MODE);
+		rename($tmp, $OUT_FILE);
 	}
 
 	# Delay and loop

+ 10 - 18
serial/cmd.pl

@@ -2,35 +2,27 @@
 use strict;
 use warnings;
 use IO::Socket::UNIX;
-use POSIX qw(ceil floor);
 
-# App config
-my $TIMEOUT  = 5;
-my $TEMP_DIR = `getconf DARWIN_USER_TEMP_DIR`;
-chomp($TEMP_DIR);
-my $DATA_DIR = $TEMP_DIR . 'plexMonitor/';
+# Local modules
+use Cwd qw(abs_path);
+use File::Basename qw(dirname);
+use lib dirname(abs_path($0));
+use DMX;
 
-# Debug
-my $DEBUG = 0;
-if ($ENV{'DEBUG'}) {
-	$DEBUG = 1;
-}
+# App config
+my $DATA_DIR = DMX::dataDir();
 
 # Command-line parameters
 my ($DEV, $CMD) = @ARGV;
 my $CMD_FILE = $DATA_DIR . uc($DEV) . '.socket';
 
 # Sanity check
-if (!-d $DATA_DIR || !-S $CMD_FILE) {
-	die("Bad config\n");
+if (!-S $CMD_FILE) {
+	die('Invalid command file: ' . $CMD_FILE . "\n");
 }
 
 # Socket init
-my $sock = IO::Socket::UNIX->new(
-	'Peer'    => $CMD_FILE,
-	'Type'    => SOCK_DGRAM,
-	'Timeout' => $TIMEOUT
-) or die('Unable to open socket: ' . $CMD_FILE . ": ${@}\n");
+my $sock = DMX::clientSock($CMD_FILE);
 
 # Send the command
 $sock->send($CMD)

+ 2 - 1
serial/serial.pl

@@ -275,7 +275,8 @@ while (1) {
 			$STATUS{$cmd}->{'status'} =~ s/[^\w\.\-]/_/g;
 
 			# If something has changed, save the state to disk
-			if ($STATUS{$cmd}->{'status'} ne $STATUS{$cmd}->{'last'}) {
+			# (if the output file does not exist, something has changed)
+			if ($STATUS{$cmd}->{'status'} ne $STATUS{$cmd}->{'last'} || !-r $STATUS{$cmd}->{'path'}) {
 				if ($DEBUG) {
 					print STDERR 'New ' . uc($DEV) . ' status: ' . $cmd . ' => ' . $STATUS{$cmd}->{'status'} . "\n";
 				}