/usr/local/CPAN/Video-PlaybackMachine/Video/PlaybackMachine/Scheduler.pm
package Video::PlaybackMachine::Scheduler;
####
#### Video::PlaybackMachine::Scheduler
####
#### $Revision: 677 $
####
#### Plays movies in the ScheduleTable at the appropriate times.
####
use strict;
use warnings;
use POE;
use POE::Session;
use Log::Log4perl;
use Date::Manip;
use POSIX 'INT_MAX';
use Video::PlaybackMachine::Player qw(PLAYER_STATUS_PLAY);
use Video::PlaybackMachine::ScheduleView;
use Video::PlaybackMachine::Config;
use Time::Duration;
use Carp;
############################# Class Constants #############################
use constant DEFAULT_SKIP_TOLERANCE => 30;
use constant DEFAULT_IDLE_TOLERANCE => 15;
our $Minimum_Fill = 7;
## Modes of operation
# Starting up, haven't played anything yet
use constant START_MODE => 0;
# Idle mode -- dead air
use constant IDLE_MODE => 1;
# Between scheduled content
use constant FILL_MODE => 2;
# Playing scheduled content
use constant PLAY_MODE => 3;
############################## Class Methods ##############################
##
## new()
##
## Arguments: (hash)
## schedule_table => Video::Playback::ScheduleTable
## offset => integer: seconds
## player => Video::PlaybackMachine::Player (optional)
## filler => Video::PlaybackMachine::Filler (optional)
## skip_tolerance => integer: seconds (optional)
## terminate_on_finish => boolean (default: true)
## run_forever => boolean (default: false)
##
sub new {
my $type = shift;
my %in = @_;
defined $in{schedule_table} or croak "Argument 'schedule_table' required; stopped";
defined $in{skip_tolerance} or $in{skip_tolerance} = DEFAULT_SKIP_TOLERANCE;
defined $in{'terminate_on_finish'} or $in{'terminate_on_finish'} = 1;
defined $in{'run_forever'} or $in{'run_forever'} = 0;
my $self = {
terminate_on_finish => $in{'terminate_on_finish'},
run_forever => $in{'run_forever'},
skip_tolerance => $in{skip_tolerance},
schedule_table => $in{schedule_table},
player => $in{player} || Video::PlaybackMachine::Player->new(),
filler => $in{filler} || Video::PlaybackMachine::Filler->new(),
waitlist => [],
mode => START_MODE,
offset => $in{offset},
minimum_fill => $Minimum_Fill,
schedule_view => Video::PlaybackMachine::ScheduleView->new($in{schedule_table}, $in{offset}),
watcher_session => $in{watcher},
logger => Log::Log4perl->get_logger('Video::Playback::Scheduler'),
};
$self->{'logger'}->info("$0 started");
bless $self, $type;
}
############################# Object Methods ##############################
sub spawn {
my $self = shift;
POE::Session->create(
object_states => [
$self => [qw(_start time_tick finished update play_scheduled warning_scheduled schedule_next shutdown wait_for_scheduled query_next_scheduled)]
],
);
}
##
## get_mode()
##
## Returns:
##
## integer -- START_MODE, FILL_MODE, or PLAY_MODE.
##
sub get_mode {
return $_[0]->{'mode'};
}
##
## should_be_playing()
##
## Returns:
##
## Video::PlaybackMachine::ScheduleEntry
##
## Returns the movie, if any, which should be playing right
## now.
##
## Enforces our playback policies.
##
## If we're just starting up, and something is scheduled to be played
## right now, we'll play it no matter how far along we're supposed to
## be. That way we can restart in the middle of a movie and not miss the
## whole thing.
##
## Otherwise, it returns a movie if there's one scheduled for right
## now and playing it would not make us miss an unacceptably long part
## of the movie.
##
sub should_be_playing {
my $self = shift;
my $schedule_now = $self->real_to_schedule(@_);
my $current = $self->{schedule_view}->get_schedule_table()->get_entry_during($schedule_now);
# If there's no entry to play right now, return nothing
defined($current) or return;
if ($self->get_mode() == START_MODE) {
# Return the movie listing
return $current;
} # End if we're in startup mode
# Else we're not in startup mode
else {
# Return the movie if it's not too far along
if ($self->get_seek($current) < $self->{skip_tolerance} ) {
return $current;
}
# TODO make sure that there's no edge condition
else {
return;
}
} # End else not in startup mode
}
sub get_seek {
my $self = shift;
return $self->{schedule_view}->get_seek(@_);
}
sub get_next_entry {
my $self = shift;
return $self->{schedule_view}->get_next_entry(@_);
}
sub get_time_to_next {
my $self = shift;
my $schedule_to_next = $self->{schedule_view}->get_time_to_next(@_);
if ( (! defined($schedule_to_next) ) && $self->{'run_forever'} ) {
return INT_MAX;
}
else {
return $schedule_to_next;
}
}
sub schedule_to_real {
my $self = shift;
return $self->{'schedule_view'}->schedule_to_real(@_);
}
sub real_to_schedule {
my $self = shift;
return $self->{'schedule_view'}->real_to_schedule(@_);
}
##
## Returns the amount of time required to skip to play
## the given movie before the next scheduled entry.
##
sub time_skip {
my $self = shift;
my $movie = shift;
my $time = $self->real_to_schedule(@_);
my $diff = $self->get_time_to_next(@_);
if ($movie->get_length() > $diff) {
return $movie->get_length() - $diff;
}
else {
return 0;
}
}
############################# Session Methods #############################
##
## _start()
##
## POE startup state.
##
## Called when the session begins. Spawns off a player and filler session
## so that they can do whatever prep work they need to do, identifies
## this session as a scheduler, and checks the database for things
## that should be played.
##
sub _start {
my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
# Hang out a shingle
$kernel->alias_set('Scheduler');
# Set up our Player and our Filler
$heap->{player_session} = $self->{player}->spawn();
$heap->{filler_session} = $self->{filler}->spawn();
# Start the time ticker
$kernel->delay('time_tick', Video::PlaybackMachine::Config->config()->time_tick() );
# Check the database for things that need playing
$kernel->yield('update');
}
##
## time_tick()
##
## Updates the process table entry with the current time (according to the schedule)
##
sub time_tick
{
my $time = $_[OBJECT]->real_to_schedule(time());
$0 = "playback_machine: " . scalar localtime($time) . "($time)";
$_[KERNEL]->delay('time_tick', Video::PlaybackMachine::Config->config()->time_tick());
}
##
## query_next_scheduled()
##
## Designed to be called and return the next item on the schedule.
## Although this is a POE event handler, it's useful only when called
## with the call() command.
##
sub query_next_scheduled {
return $_[OBJECT]->get_next_entry(undef,$_[ARG0]);
}
##
## update()
##
## POE state.
##
## Called whenever there's a change to the schedule
## and we need to make sure that the Scheduler's state
## matches what's in the database. Does NOT interrupt
## a running movie.
##
sub update {
my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
# Clear all schedule alarms
$kernel->alarm_set('play_scheduled');
$kernel->alarm_set('warning_scheduled');
# If we're not playing
if ($self->get_mode() != PLAY_MODE) {
# If there's something supposed to be playing
if ( my $entry = $self->should_be_playing() ) {
$self->{'logger'}->debug("Time to play ", $entry->getTitle());
# Play it
$kernel->yield('play_scheduled', $entry->get_listing(), $self->get_seek($entry));
return;
} # End if supposed to be playing
# Otherwise, fill gap until next scheduled item
else {
$kernel->yield('wait_for_scheduled');
}
} # End if we're not playing
# Set alarm to play next scheduled item
$kernel->delay('schedule_next', 5);
}
##
## finished()
##
## POE state.
##
## Called whenever playback is finished. It checks to see if there is anything
## waiting for immediate play (i.e. was double-scheduled earlier) and plays it.
## Otherwise, sends us to fill mode.
##
## Until we enter fill or play mode, this method puts us into idle mode.
##
sub finished {
my ($self, $kernel, $request, $response) = @_[OBJECT, KERNEL, ARG0, ARG1];
my $now = time();
# If we've been running longer than the restart interval, restart the system
my $config = Video::PlaybackMachine::Config->config();
if ($config->restart_interval() > 0) {
if ( ($now - $^T) > $config->restart_interval() ) {
$self->{'logger'}->info("Shutting down for restart");
exit(0);
}
}
# We're in idle mode now
$self->{mode} = IDLE_MODE;
# Log the item that finished playing
$kernel->post('Logger', 'log_played_movie', $request->[0], $request->[1], time(), $response->[0]);
# If there's something waiting to be played
my $waiting_movie;
if ( $waiting_movie = shift @{ $self->{waitlist} } ) {
# If there's time enough to play it
if ( $self->time_skip( $waiting_movie, $now ) <= $self->{skip_tolerance} ) {
# Play it, skipping as necessary
$kernel->yield('play_scheduled', $waiting_movie, $self->time_skip( $waiting_movie ) );
} # End if time enough
# Otherwise we didn't have time to play it
else {
# Log that we had to skip something
$kernel->post('Logger', 'log_skipped_movie', $waiting_movie);
# Schedule the next movie
$kernel->yield('schedule_next');
# Wait for the next movie
$kernel->yield('wait_for_scheduled');
} # End no time
}# End if something waiting
# Otherwise, nothing scheduled to play right now
else {
# If there's something else scheduled
if ( defined $self->get_next_entry($now) ) {
# If there's enough time to start filling
if ( $self->get_time_to_next($now) > $self->{minimum_fill} ) {
# Fill until next scheduled entry
$kernel->yield('wait_for_scheduled');
} # End if enough time
# Otherwise, go into idle mode till next
else {
$self->{'logger'}->debug("Not filling: " . $self->get_time_to_next($now) . " too short for fill (minimum $self->{'minimum_fill'})\n");
$self->{mode} = IDLE_MODE;
}
} # End if something else scheduled
# Otherwise, nothing scheduled; shut down.
else {
$kernel->yield('shutdown');
}
} # End nothing right now
}
sub warning_scheduled {
my ($self, $kernel) = @_[OBJECT, KERNEL];
# If we're in fill mode
if ( $self->get_mode() == FILL_MODE ) {
# Send a warning message to the Filler
$kernel->post('Filler', 'warning', $self->get_time_to_next());
} # End if we're in fill mode
# Otherwise, do nothing; we do not interrupt scheduled content.
}
sub play_scheduled {
my ($self, $kernel, $movie, $seek) = @_[OBJECT, KERNEL, ARG0, ARG1];
# If we're playing something scheduled
if ( ( $self->get_mode() == PLAY_MODE ) && ($self->{player}->get_status() == PLAYER_STATUS_PLAY) ) {
# Add the currently-scheduled item to the waiting list
# This discards any existing $seek
push(@{ $self->{waitlist} }, $movie);
return;
} # End if we're playing something scheduled
# Otherwise, we're ready to play
else {
# Tell the Filler to stop filling
$kernel->post('Filler', 'stop');
# Mark that we're in play mode now
$self->{'mode'} = PLAY_MODE;
# Start playing the movie
$movie->play($seek);
# Schedule the next item from the schedule table
$kernel->delay('schedule_next', 3);
} # End otherwise
}
sub wait_for_scheduled {
my ($self, $kernel) = @_[OBJECT, KERNEL];
defined $self->get_time_to_next()
or $self->{'logger'}->logdie("Called wait_for_scheduled with nothing to wait for; schedule time is " . scalar localtime($self->real_to_schedule()) );
# If there's enough time before the next item to bother with fill
if ( $self->get_time_to_next() > $self->{minimum_fill} ) {
# Mark that we're in Fill mode
$self->{mode} = FILL_MODE;
# Tell our Filler to get to work
$kernel->post('Filler', 'start_fill', $self->{schedule_view});
} # End if enough time
# Else not enough time
else {
# Go to Idle mode
$self->{mode} = IDLE_MODE;
}
}
sub schedule_next {
my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
# If there's something left in the schedule
if ( my $entry = $self->get_next_entry() ) {
# Set an alarm to play it
my $alarm_offset = $self->{'schedule_view'}->schedule_to_real($entry->get_start_time());
my $in_time = $alarm_offset - time();
($in_time >= 0)
or $self->{'logger'}->logdie("Attempt to schedule '", $entry->getTitle(), "' in the past ($in_time) at ", scalar localtime $alarm_offset);
$self->{'logger'}->info("scheduling: ", $entry->getTitle(), " at ", scalar localtime($alarm_offset), " in ", duration($in_time));
$kernel->alarm( 'play_scheduled', $alarm_offset, $entry->get_listing(), 0 );
} # End if there's something left
}
sub shutdown {
my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
# If we're supposed to quit
if ($self->{terminate_on_finish}) {
# Pull in the shingle
$kernel->alias_remove('Scheduler');
# Terminate Watcher if defined
$kernel->post($self->{'watcher_session'}, 'shutdown');
# Terminate Player and Filler
$kernel->post($heap->{player_session}, 'shutdown');
$kernel->post($heap->{filler_session}, 'shutdown');
# Stop watching for 'finished' events
$kernel->state('finished');
delete $heap->{$_} foreach keys %$heap;
$kernel->alarm_remove_all();
return;
} # End if we're supposed to quit
# Otherwise we're supposed to put up a standby screen
else {
# Put up the standby screen
warn "Putting up standby screen unimplemented...";
} # End otherwise
}
1;