/usr/local/CPAN/Video-PlaybackMachine/Video/PlaybackMachine/Player.pm


package Video::PlaybackMachine::Player;

####
#### Video::PlaybackMachine::Player
####
#### A POE::Session which displays movies and still frames onscreen
#### based on events.
####

use strict;
use base 'Exporter';
our @EXPORT_OK = qw(PLAYER_STATUS_STOP PLAYER_STATUS_PLAY PLAYER_STATUS_STILL
                    PLAYBACK_OK PLAYBACK_ERROR PLAYBACK_STOPPED);

use POE;
use X11::FullScreen;
use Video::Xine;
use Video::PlaybackMachine::EventWheel::FullScreen;
use Video::PlaybackMachine::Config;
use Log::Log4perl;
use Carp;

############################# Class Constants ################################

## Status codes Xine will report
use constant PLAYER_STATUS_STOP => 0;
use constant PLAYER_STATUS_PLAY => 1;

## How-the-movie-played status codes

# OK == played through and stopped at the end
use constant PLAYBACK_OK => 1;

# ERROR == problem in trying to play
use constant PLAYBACK_ERROR => 2;

use constant X_DISPLAY => Video::PlaybackMachine::Config->config()->x_display();

## Types of playback
use constant PLAYBACK_TYPE_MUSIC => 0;
use constant PLAYBACK_TYPE_MOVIE => 1;

############################## Class Methods #################################

##
## new()
##
## Returns a new instance of Player. Note that the session is not created
## until you call spawn().
##
sub new {
  my $type = shift;

  my $self = {
	      logger => Log::Log4perl->get_logger('Video.PlaybackMachine.Player'),
	     };


  bless $self, $type;
}

############################## Session Methods ###############################

##
## On session start, initializes Xine and prepares it to start playing.
##
sub _start {
  my $kernel = $_[KERNEL];

  $kernel->alias_set('Player');
  my $display = X11::FullScreen::Display->new(X_DISPLAY);
  $_[HEAP]->{'display'} = $display;
  $_[HEAP]->{'window'} = $display->createWindow();
  $display->sync();
  my $xine = Video::Xine->new();
  my $config = Video::PlaybackMachine::Config->config();
  $xine->set_param(XINE_ENGINE_PARAM_VERBOSITY, $config->player_verbose());
  $_[HEAP]->{'xine'} = $xine;
  my $x11_visual = Video::Xine::Util::make_x11_visual($display,
						      $display->getDefaultScreen(),
						      $_[HEAP]->{'window'},
						      $display->getWidth(),
						      $display->getHeight(),
						      $display->getPixelAspect()
						     );
	# TODO Move "auto" to config file
  my $driver = Video::Xine::Driver::Video->new($xine,"auto",1,$x11_visual);
  my $s = $xine->stream_new(undef, $driver)
    or croak "Unable to open video stream";
  $_[OBJECT]->{'stream'} = $s;
  $_[HEAP]->{'stream_queue'} =
    Video::PlaybackMachine::Player::EventWheel->new($s);
  my $fq =
    Video::PlaybackMachine::EventWheel::FullScreen->new($display, $_[HEAP]->{'window'});
  $fq->set_expose_handler(
			  sub { $s->get_video_port()->send_gui_data(XINE_GUI_SEND_EXPOSE_EVENT, $_[1]); } );
  $fq->spawn();

  $_[HEAP]->{'fullscreen_queue'} = $fq

}

##
## Responds to a 'play' request by playing a movie on Xine.
## Arguments:
##   ARG0: $postback -- what to call after the play is completed
##   ARG1: $offset -- number of seconds after the movie's start to begin
##   ARG2: @filenames -- ARG1 onward contains the files to play, in order.
##
## After Xine is started, we'll check on it every $XINE_CHECK_INTERVAL
## seconds to see if it has stopped.
##
sub play {
  my ($kernel, $self, $heap, $postback, $offset, @files) = @_[KERNEL, OBJECT, HEAP, ARG0, ARG1, ARG2 .. $#_ ];

  defined $offset or $offset = 0;

  my $log = $_[OBJECT]{'logger'};

	# TODO Remove fatal
  @files or die "No files specified! stopped";

  # Stop if we're playing
  if ( $self->{'stream'}->get_status() == XINE_STATUS_PLAY ) {
    $self->{'stream'}->stop();
    $self->{'stream'}->close();
  }
  
  # Clear out any previous events
  $heap->{'stream_queue'}->clear_events();

  $log->info("Playing $files[0]");

  my $s = $_[OBJECT]->{'stream'};
  $s->open($files[0])
    or do {
      $log->error("Unable to open '$files[0]': Error " . $s->get_error());
      $postback->(PLAYBACK_ERROR);
      return;
    };
  $s->play(0,$offset * 1000)
    or do {
      $log->error("Unable to play '$files[0]': Error " . $s->get_error());
      $postback->(PLAYBACK_ERROR);
      return;
    };

  # Tell the system to refresh the window
  # Drawable changed
  $s->get_video_port()->send_gui_data(XINE_GUI_SEND_DRAWABLE_CHANGED, $heap->{'window'});
  $s->get_video_port()->send_gui_data(XINE_GUI_SEND_VIDEOWIN_VISIBLE, 1);

  # Spawn a watcher to call the postback after the fact
  $heap->{'stream_queue'}->set_stop_handler($postback);
  $heap->{'stream_queue'}->spawn();

			     
  $heap->{'playback_type'} = PLAYBACK_TYPE_MOVIE;

}

##
## stop()
##
## Stops the currently-playing movie.
##
sub stop {
  # Stop if we're playing
  if ( $_[OBJECT]->{'stream'}->get_status() == XINE_STATUS_PLAY ) {
    $_[OBJECT]->{'stream'}->stop();
  }
  
}

##
## play_still()
##
## Arguments:
##   STILL_FILE: Filename of our stillstore.
## 
## Responds to a 'play_still' request by playing a still frame
## on Xine. The stillframe will remain there until something
## replaces it.
##
sub play_still {
  my ($self, $kernel, $heap, $still, $callback, $time) = @_[OBJECT, KERNEL, HEAP, ARG0, ARG1];
  my $log = $self->{'logger'};
  if ($self->{'stream'}->get_status() == XINE_STATUS_PLAY
  	&& $heap->{'playback_type'} == PLAYBACK_TYPE_MOVIE 
  ) {
  		$log->error("Attempted to show still '$still' while playing a movie");
  		return;
  }
  $log->debug("Showing still '$_[ARG0]'");
  eval {
    $heap->{'display'}->displayStill($heap->{'window'}, $still);
  };
  if ($@) {
    $log->error("Error displaying still '$still': $@");
    $callback->(PLAYBACK_ERROR);
    return;
  }

  if (defined $time) {
    POE::Session->create(
			 inline_states => {
					   _start => sub {
					     $_[KERNEL]->delay('end_delay', $time);
					   },
					   end_delay => sub {
					     $log->debug("Still playback finished for '$still'");
					     $callback->($still, PLAYBACK_OK);
					   }
					  }
			);
  }

}

##
## play_music()
##
## Arguments:
##  ARG0 -- callback. What to call when the music's over.
##  ARG1 -- song file. Filename of the song to play.
##
## Responds to a 'play_music' request by playing a particular song.
## Logs a warning and does nothing if we tried to play music during a
## movie. If a song was already playing, lets it play, but substitutes
## the current callback.
##
sub play_music {
  my ($self, $heap, $kernel, $callback, $song_file) = @_[OBJECT,HEAP,KERNEL,ARG0,ARG1];

  defined $callback or die "Must define callback!\n";

  defined $song_file or die "Must define song file!\n";

  # If there's a movie running, let it play
  if ($self->get_status() == PLAYER_STATUS_PLAY) {
    if ($heap->{'playback_type'} == PLAYBACK_TYPE_MOVIE) {
      $self->{'logger'}->warn("Attempted to play '$song_file' while a movie is playing");
      $callback->($self->{'stream'}, PLAYBACK_ERROR);
      return;
    }
    else {
      $heap->{'stream_queue'}->set_stop_handler($callback);
    }
  }
  else {
    $self->{'logger'}->debug("Playing music file '$song_file'");
    $heap->{'stream_queue'}->clear_events();
    $self->{'stream'}->open($song_file)
      or do {
	$self->{'logger'}->warn("Unable to play '$song_file'");
	$callback->($self->{'stream'}, PLAYBACK_ERROR);
	return;
      };
    $self->{'stream'}->play(0,0);
    $heap->{'stream_queue'}->set_stop_handler($callback);
    $heap->{'stream_queue'}->spawn();
    $heap->{'playback_type'} = PLAYBACK_TYPE_MUSIC;
  }
}



############################## Object Methods ################################

##
## spawn()
##
## Creates the appropriate Player session.
##
sub spawn {
  my $self = shift;

  POE::Session->create(
		       object_states => 
		       [
			$self => [
				  qw(_start
                                     play
                                     play_still
				     play_music
                                     stop
                                  )
				 ] ,
		       ],
		     );

}

##
## get_status()
##
## Returns one of:
##   PLAYER_STATUS_PLAY if a movie (or music) is playing
##   PLAYER_STATUS_STOP if nothing is playing.
##
sub get_status {
  my $self = shift;


  my $session = $poe_kernel->get_active_session();
  my $heap = $session->get_heap();

  if (! defined $self->{'stream'} ) {
    $self->{'logger'}->fatal("Undefined stream! Called on session $session, caller " . join(' ', caller()) );
    confess("Undefined stream!");
  }

  $self->{'stream'}->get_status() == XINE_STATUS_PLAY
    and return PLAYER_STATUS_PLAY;

  return PLAYER_STATUS_STOP;
}


package Video::PlaybackMachine::Player::EventWheel;

# TODO: Make a subclass of EventWheel

###
### When spawned, these will pass along events from the given
### streams to the appropriate callbacks.
###

use strict;
use POE;
use Video::Xine;

## How often to check to see if Xine has stopped, in seconds
use constant XINE_CHECK_INTERVAL_SECS => 2;

sub new {
  my $type = shift;
  my ($stream, %handlers) = @_;

  my $self = {
	      type => $type,
	      stream => $stream,
	      handlers => { %handlers },
	      queue => undef,
	      logger => Log::Log4perl->get_logger('Video.PlaybackMachine.Player.EventWheel'),	     
	     };

  $self->{queue} = Video::Xine::Event::Queue->new($self->{'stream'})
    or die "Couldn't create Xine::Event::Queue";

  bless $self, $type;
}

sub spawn {
  my $self = shift;
  my ($callback) = @_;

  POE::Session->create(
		       object_states => [$self=>[qw(_start get_events)]]
		      );
}

sub _start {
  my ($self, $heap, $kernel) = @_[OBJECT, HEAP, KERNEL];

  $kernel->yield('get_events');
}


sub clear_events {
	my $self = shift;
		
	1 while $self->{queue}->get_event();	
}

sub get_events {
  my ($self, $heap, $kernel) = @_[OBJECT, HEAP, KERNEL];

  # Translate all events into callbacks
  while ( my $event = $self->{queue}->get_event() ) {
    $self->{'logger'}->debug("Received event: ", $event->get_type(), "\n");
    if ( $event->get_type() == XINE_EVENT_UI_PLAYBACK_FINISHED ) {
      $self->{'stream'}->close();
    }
    if ( exists $self->{'handlers'}{$event->get_type()} ) {
      $self->{'logger'}->debug("Invoking handler for ", $event->get_type(), "\n");
      $self->{'handlers'}{$event->get_type()}->($self->{'stream'}, $event);
    }
  }

  # Keep checking so long as we're playing
  if ( $self->{'stream'}->get_status() == XINE_STATUS_PLAY ) {
    $kernel->delay('get_events', XINE_CHECK_INTERVAL_SECS);
  }
}

sub set_handler {
  my $self = shift;
  my ($event, $callback) = @_;
  $self->{'handlers'}{$event} = sub { $callback->($_[0], Video::PlaybackMachine::Player::PLAYBACK_OK) };
}


# Convenience method
sub set_stop_handler {
  $_[0]->set_handler(XINE_EVENT_UI_PLAYBACK_FINISHED, $_[1]);
}