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;