qt_player.pl 6.1 KB

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