qt_player.pl 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use File::Basename;
  5. use IPC::System::Simple;
  6. use Time::Out qw( timeout );
  7. # Local modules
  8. use Cwd qw(abs_path);
  9. use File::Basename qw(dirname);
  10. use lib dirname(abs_path($0));
  11. use DMX;
  12. use Audio;
  13. # User config
  14. my $MEDIA_PATH = `~/bin/video/mediaPath`;
  15. my %FILES = ();
  16. # Prototypes
  17. sub init();
  18. sub load($$);
  19. sub unload($);
  20. sub stop();
  21. sub printFiles();
  22. sub cmdApp($$);
  23. sub openQT($);
  24. # App config
  25. my $DATA_DIR = DMX::dataDir();
  26. my $STATE_SOCK = 'QT_PLAYER';
  27. my $OUTPUT_FILE = $DATA_DIR . $STATE_SOCK;
  28. my $DELAY = 5;
  29. my $MAX_CMD_LEN = 16384;
  30. my $TIMEOUT_OPEN = 10;
  31. # Debug
  32. my $DEBUG = 0;
  33. if ($ENV{'DEBUG'}) {
  34. $DEBUG = 1;
  35. }
  36. # Clear our output file
  37. if (-e $OUTPUT_FILE) {
  38. unlink($OUTPUT_FILE);
  39. }
  40. # Reset QT Player
  41. init();
  42. # Sockets
  43. my $SELECT = DMX::selectSock($STATE_SOCK);
  44. # Loop forever
  45. while (1) {
  46. # Wait for commands
  47. {
  48. my @ready_clients = $SELECT->can_read($DELAY);
  49. foreach my $fh (@ready_clients) {
  50. # Ensure we won't block on recv()
  51. $fh->blocking(0);
  52. # Read
  53. while (defined($fh->recv(my $text, $MAX_CMD_LEN))) {
  54. # Parse
  55. my @argv = split(/\|/, $text);
  56. # Ignore empty commands
  57. if (scalar(@argv) < 1) {
  58. next;
  59. }
  60. # Dispatch commands
  61. my $cmd = shift(@argv);
  62. if ($cmd eq 'LOAD' && scalar(@argv) == 2) {
  63. load($argv[0], $argv[1]);
  64. } elsif ($cmd eq 'UNLOAD' && scalar(@argv) == 1) {
  65. unload($argv[0]);
  66. } elsif ($cmd eq 'STOP') {
  67. stop();
  68. } elsif ($cmd eq 'RESET') {
  69. init();
  70. } else {
  71. warn('Ignored invalid command: ' . $text . "\n");
  72. }
  73. }
  74. }
  75. }
  76. # Keep our list of files in-sync with reality
  77. foreach my $name (keys(%FILES)) {
  78. eval { Audio::runApplescript('tell application "QuickTime Player" to get document ' . $FILES{$name}); };
  79. if ($@) {
  80. warn('File went missing: ' . $name . "\n");
  81. delete($FILES{$name});
  82. printFiles();
  83. }
  84. }
  85. # Ensure the output file always exists
  86. if (!-r $OUTPUT_FILE) {
  87. printFiles();
  88. }
  89. # Ensure "silence" is always available -- reset if it goes away
  90. if (!exists($FILES{'SILENCE'})) {
  91. die("We need only the SILENCE\n");
  92. }
  93. }
  94. # Get things into a reasonable state
  95. sub init() {
  96. if ($DEBUG) {
  97. print STDERR "init()\n";
  98. }
  99. # Reset our local state
  100. %FILES = ();
  101. # Close all QT documents and relaunch QT Player
  102. cmdApp('QuickTime Player', 'close every document');
  103. cmdApp('QuickTime Player', 'quit');
  104. sleep(1);
  105. openQT(undef());
  106. # Always load and play the SILENCE file (for sanity and general init)
  107. load('SILENCE', 'DMX/Silence.wav');
  108. # Bring Plex back to the front
  109. cmdApp('Plex', 'activate');
  110. }
  111. # Open the named file and record the handle
  112. sub load($$) {
  113. my ($name, $path) = @_;
  114. if ($DEBUG) {
  115. print STDERR 'load(): ' . $name . ', ' . $path . "\n";
  116. }
  117. # Build a reasonable path
  118. if (!($path =~ /^\//)) {
  119. $path = $MEDIA_PATH . '/' . $path;
  120. }
  121. # Ensure the file exists
  122. if (!-r $path) {
  123. warn('Invalid audio file path: ' . $path . "\n");
  124. return;
  125. }
  126. # Ignore duplicate loads
  127. if (exists($FILES{$name})) {
  128. warn('Ignoring load request for existing file: ' . $name . "\n");
  129. return;
  130. }
  131. # Find out how many files are already open
  132. my $count = Audio::runApplescript('tell application "QuickTime Player" to count items of every document');
  133. # Open the file
  134. openQT($path);
  135. # Wait for QT to load the new file
  136. # To avoid indefinate hangs this will give up after 5 seconds
  137. my @cmd = ('tell application "QuickTime Player"');
  138. push(@cmd, 'set endDate to current date + (5/60 * minutes)');
  139. push(@cmd, 'repeat while (count items of every document) <= ' . $count . ' and current date < endDate');
  140. push(@cmd, 'delay 0.1');
  141. push(@cmd, 'end repeat');
  142. push(@cmd, 'if (count items of every document) > ' . $count . ' then');
  143. push(@cmd, 'get document 1');
  144. push(@cmd, 'end if');
  145. push(@cmd, 'end tell');
  146. my $doc = Audio::runApplescript(join("\n", @cmd));
  147. # Reset if we cannot open -- something is out-of-sync
  148. if (!$doc) {
  149. init();
  150. return;
  151. }
  152. # Clean up the document name
  153. $doc =~ s/^\s*document //;
  154. $doc =~ s/\s+$//;
  155. $doc =~ s/\"/\\\"/g;
  156. $doc = '"' . $doc . '"';
  157. # Save the document handle
  158. $FILES{$name} = $doc;
  159. printFiles();
  160. # Bring Plex back to the front
  161. cmdApp('Plex', 'activate');
  162. }
  163. # Close the named file
  164. sub unload($) {
  165. my ($name) = @_;
  166. if ($DEBUG) {
  167. print STDERR 'unload(): ' . $name . "\n";
  168. }
  169. # Ignore files that don't exist
  170. if (!exists($FILES{$name})) {
  171. warn('No such file loaded: ' . $name . "\n");
  172. return;
  173. }
  174. # Close to document
  175. Audio::runApplescript('tell application "QuickTime Player" to close document ' . $FILES{$name});
  176. # Delete our handle
  177. delete($FILES{$name});
  178. printFiles();
  179. }
  180. # Stop and rewind all QT documents
  181. sub stop() {
  182. Audio::runApplescript('tell application "QuickTime Player" to stop every document');
  183. Audio::runApplescript('tell application "QuickTime Player" to set current time of every document to 0');
  184. }
  185. # Update the output file list
  186. sub printFiles() {
  187. my ($fh, $tmp) = File::Temp::tempfile($OUTPUT_FILE . '.XXXXXXXX', 'UNLINK' => 0);
  188. foreach my $file (keys(%FILES)) {
  189. print $fh $file . '|' . $FILES{$file} . "\n";
  190. }
  191. close($fh);
  192. rename($tmp, $OUTPUT_FILE);
  193. }
  194. # Request the specified action from the provided app, if it's already running
  195. sub cmdApp($$) {
  196. my ($app, $cmd) = @_;
  197. # Ask AppleScript to execute the provided command only if the specified app is already running
  198. my @cmd = ('tell application "System Events"');
  199. push(@cmd, 'if exists process "' . $app . '" then');
  200. push(@cmd, 'tell application "' . $app . '" to ' . $cmd);
  201. push(@cmd, 'end if');
  202. push(@cmd, 'end tell');
  203. # Execute
  204. Audio::runApplescript(join("\n", @cmd));
  205. }
  206. # Activate QT player, optionally loading a document
  207. # It would be nice to use AS here to get a more definitive document name, but the open() call is QT player is borked
  208. sub openQT($) {
  209. my ($doc) = @_;
  210. # Build the open command
  211. my @cmd = ('open', '-a', 'QuickTime Player');
  212. if ($doc) {
  213. push(@cmd, $doc);
  214. }
  215. # Execute, dying on timeout
  216. timeout $TIMEOUT_OPEN => sub {
  217. IPC::System::Simple::system(@cmd);
  218. };
  219. if ($@) {
  220. die("Open command timed out\n");
  221. }
  222. }