###################################################################### # # VideoPlugin.pm -- plugin for processing video files # A component of the Greenstone digital library software # from the New Zealand Digital Library Project at the # University of Waikato, New Zealand. # # Copyright (C) 1999 New Zealand Digital Library Project # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # ########################################################################### # -- Largely modeled on how ImagePlugin works # -- Can convert to audio as well as video package VideoPlugin; use strict; no strict 'refs'; # allow filehandles to be variables and viceversa no strict 'subs'; use XMLParser; use gsprintf; use MultimediaPlugin; use VideoConverter; use ReadXMLFile; sub BEGIN { @VideoPlugin::ISA = ('MultimediaPlugin', 'VideoConverter', 'ReadXMLFile'); } my $arguments = [ { 'name' => "process_exp", 'desc' => "{BasePlugin.process_exp}", 'type' => "regexp", 'deft' => &get_default_process_exp(), 'reqd' => "no" } ]; my $options = { 'name' => "VideoPlugin", 'desc' => "{VideoPlugin.desc}", 'abstract' => "no", 'inherits' => "yes", 'args' => $arguments }; sub new { my ($class) = shift (@_); my ($pluginlist,$inputargs,$hashArgOptLists) = @_; push(@$pluginlist, $class); push(@{$hashArgOptLists->{"ArgList"}},@{$arguments}); push(@{$hashArgOptLists->{"OptList"}},$options); new VideoConverter($pluginlist, $inputargs, $hashArgOptLists); my $self = new MultimediaPlugin($pluginlist, $inputargs, $hashArgOptLists); if ($self->{'info_only'}) { # don't worry about creating the XML parser as all we want is the # list of plugin options return bless $self, $class; } # create XML::Parser object for parsing keyframe files (produced by hive) my $parser = new XML::Parser('Style' => 'Stream', 'Pkg' => 'VideoPlugin', 'PluginObj' => $self, 'Namespaces' => 1, # strip out namespaces 'Handlers' => {'Char' => \&Char, 'XMLDecl' => \&XMLDecl, 'Entity' => \&Entity, 'Doctype' => \&Doctype, 'Default' => \&Default }); $self->{'parser'} = $parser; return bless $self, $class; } sub begin { my $self = shift (@_); my ($pluginfo, $base_dir, $processor, $maxdocs) = @_; $self->SUPER::begin(@_); $self->VideoConverter::begin(@_); } sub init { my $self = shift (@_); my ($verbosity, $outhandle, $failhandle) = @_; $self->SUPER::init(@_); $self->VideoConverter::init(@_); } sub get_default_process_exp { my $self = shift (@_); return q^(?i)\.(mpe?g|flv|mov|qt|wmv|vob|avi|mp4|m4v)$^; } sub extract_keyframes { my $self = shift (@_); my ($doc_obj,$originalfilename,$filename) = @_; my $section = $doc_obj->get_top_section(); my $output_dir = $self->{'cached_dir'}; my $ivideo_root = $self->{'cached_file_root'}; # Generate the keyframes with ffmpeg and hive my ($keyframe_cmd,$okeyframe_filename) = $self->keyframe_cmd($originalfilename || $filename); my $keyframe_options = { @{$self->{'ffmpeg_monitor'}}, 'message_prefix' => "Keyframe", 'message' => "Extracting keyframes" }; $self->run_cached_general_cmd($keyframe_cmd,$okeyframe_filename,$keyframe_options); $self->parse_shot_xml(); $self->associate_keyframes($doc_obj,$section); } sub extract_thumbnail { my $self = shift (@_); my ($doc_obj,$filename,$convertto_regenerated,$thumbnailtype, $thumbnail_width, $thumbnail_height) = @_; my $section = $doc_obj->get_top_section(); my $output_dir = $self->{'cached_dir'}; my $ivideo_root = $self->{'cached_file_root'}; my $verbosity = $self->{'verbosity'}; my $outhandle = $self->{'outhandle'}; # Generate the thumbnail with convert, a la ImagePlug my $thumbnailfile = &util::filename_cat($output_dir,"$ivideo_root.$thumbnailtype"); my $optionally_run_general_cmd = "run_uncached_general_cmd"; if ($self->{'enable_cache'}) { $optionally_run_general_cmd = ($convertto_regenerated) ? "regenerate_general_cmd" : "run_cached_general_cmd"; } my ($thumb_cmd ,$othumb_filename) = $self->keyframe_thumbnail_cmd($filename,$thumbnailfile,$thumbnail_width,$thumbnail_height); my $thumb_options = { 'verbosity' => $verbosity, 'outhandle' => $outhandle, 'message_prefix' => "Thumbnail", 'message' => "Generating thumbnail" }; my ($thumb_regenerated,$thumb_result,$thumb_had_error) = $self->$optionally_run_general_cmd($thumb_cmd,$thumbnailfile,$thumb_options); # Add the thumbnail as an associated file ... if (-e "$thumbnailfile") { $doc_obj->associate_file("$thumbnailfile", "thumbnail.$thumbnailtype", "image/$thumbnailtype",$section); $doc_obj->add_metadata ($section, "ThumbType", $thumbnailtype); $doc_obj->add_metadata ($section, "Thumb", "thumbnail.$thumbnailtype"); $doc_obj->add_utf8_metadata ($section, "thumbicon", ""); ### $doc_obj->add_utf8_metadata ($section, "thumbicon", ""); } # Extract Thumnail metadata from convert output if ($thumb_result =~ m/[0-9]+x[0-9]+=>([0-9]+)x([0-9]+)/) { $doc_obj->add_metadata ($section, "ThumbWidth", $1); $doc_obj->add_metadata ($section, "ThumbHeight", $2); } else { # Two reasons for getting to here: # 1.thumbnail was generated by ffmpeg, not imagemagick convert # 2.thumbnail was cached, so imagemagick convert was not run # Either way, the solution is the same: # => run "identify $thumbnailfile" and parse result $thumb_result = `identify \"$thumbnailfile\"`; if ($thumb_result =~ m/([0-9]+)x([0-9]+)/) { $doc_obj->add_metadata ($section, "ThumbWidth", $1); $doc_obj->add_metadata ($section, "ThumbHeight", $2); } } } sub extract_keyframes_montage { my $self = shift (@_); my ($doc_obj,$filename,$thumbnailtype) = @_; my $section = $doc_obj->get_top_section(); my $output_dir = $self->{'cached_dir'}; my $ivideo_root = $self->{'cached_file_root'}; # Generate the mosaic with 'montage' my $montagefile = &util::filename_cat($output_dir,"$ivideo_root\_montage.$thumbnailtype"); my ($montage_cmd,$omontage_filename) = $self->keyframe_montage_cmd($filename,$montagefile); my $montage_options = { 'message_prefix' => "Montage", 'message' => "Generating montage" }; my ($montage_result,$montage_had_error) = $self->run_general_cmd($montage_cmd,$montage_options); # Add the montage as an associated file ... if (-e "$montagefile") { $doc_obj->associate_file("$montagefile", "montage.$thumbnailtype", "image/$thumbnailtype",$section); $doc_obj->add_metadata ($section, "MontageType", $thumbnailtype); $doc_obj->add_metadata ($section, "Montage", "montage.$thumbnailtype"); $doc_obj->add_utf8_metadata ($section, "montageicon", ""); } } sub extract_screenview { my $self = shift (@_); my ($doc_obj,$filename,$screenviewtype, $screenview_width,$screenview_height) = @_; my $section = $doc_obj->get_top_section(); my $output_dir = $self->{'cached_dir'}; my $ivideo_root = $self->{'cached_file_root'}; # make the screenview image my $screenviewfilename = &util::filename_cat($output_dir,"$ivideo_root.$screenviewtype"); my ($screenview_cmd,$oscreenview_filename) = $self->keyframe_thumbnail_cmd($filename,$screenviewfilename,$screenview_width,$screenview_height); my $screenview_options = { 'message_prefix' => "Screenview", 'message' => "Generating screenview image" }; my ($result,$had_error) = $self->run_general_cmd($screenview_cmd,$screenview_options); # get screenview dimensions, size and type if ($result =~ m/[0-9]+x[0-9]+=>([0-9]+)x([0-9]+)/) { $doc_obj->add_metadata ($section, "ScreenWidth", $1); $doc_obj->add_metadata ($section, "ScreenHeight", $2); } else { $doc_obj->add_metadata ($section, "ScreenWidth", $screenview_width); $doc_obj->add_metadata ($section, "ScreenHeight", $screenview_height); } #add the screenview as an associated file ... if (-e "$screenviewfilename") { $doc_obj->associate_file("$screenviewfilename", "screenview.$screenviewtype", "image/$screenviewtype",$section); $doc_obj->add_metadata ($section, "ScreenType", $screenviewtype); $doc_obj->add_metadata ($section, "Screen", "screenview.$screenviewtype"); $doc_obj->add_utf8_metadata ($section, "screenicon", ""); } else { my $outhandle = $self->{'outhandle'}; print $outhandle "VideoPlugin: couldn't find \"$screenviewfilename\"\n"; } } # Create the keyframes, thumbnail and screenview images, and discover # the Video's size, width, and height using the ffmpeg utility. sub run_convert { my $self = shift (@_); my $base_dir = shift (@_); my $filename = shift (@_); # filename with full path my $file = shift (@_); # filename without path my $doc_obj = shift (@_); my $section = $doc_obj->get_top_section(); my $verbosity = $self->{'verbosity'}; my $outhandle = $self->{'outhandle'}; # check the filename is okay return 0 if ($file eq "" || $filename eq ""); my $minimumsize = $self->{'minimumsize'}; if (defined $minimumsize && (-s $filename < $minimumsize)) { print $outhandle "VideoPlugin: \"$filename\" too small, skipping\n" if ($verbosity > 1); } my ($video_type, $video_width, $video_height, $video_duration, $video_size, $vcodec,$vfps,$atype,$afreq,$achan,$arate) = &VideoConverter::identify($filename, $outhandle, $verbosity); if ($vfps eq "unknown") { print $outhandle "Unknown framerate, defaulting to 25 frames per second.\n"; $vfps = 25; } my ($dur_hour,$dur_min,$dur_sec) = ($video_duration =~ m/(\d+):(\d+):(\d+\.\d+)/); my $total_dur_secs = $dur_hour*3600 + $dur_min*60 + $dur_sec; $self->{'video-fps'} = $vfps; $self->{'num-total-frames'} = $total_dur_secs * $vfps; # Convert the video to a new type (if required). my $converttotype = $self->{'converttotype'}; my $converttosize = $self->{'converttosize'}; # shorten duration prcessed for experimentation purposes my $exp_duration = undef; my $excerpt_duration = $self->{'excerpt_duration'}; if ((defined $excerpt_duration) && ($excerpt_duration ne "")) { $exp_duration = $excerpt_duration; my ($hh,$mm,$ss,$ms); if ($exp_duration =~ m/^(\d\d):(\d\d):(\d\d)\.?(\d\d)?/) { $hh = $1; $mm = $2; $ss = $3; $ms = $4; } else { # assume value is in seconds $hh = 0; $mm = 0; $ss = $exp_duration; $ms = 0; } my $excerpt_dur_in_secs = $hh * 3600 + $mm * 60 + $ss; if ($excerpt_dur_in_secs < $total_dur_secs) { $self->{'num-total-frames'} = $excerpt_dur_in_secs * $vfps; # override calculation for full length duration } else { # clip is already shorter than requested video excerpt duration # set exp_duration back to undefined $exp_duration = undef; } } if (defined $exp_duration) { print $outhandle "Only encoding first $exp_duration of video.\n"; $self->{'exp_duration'} = $exp_duration; } my $ascii_only_filenames = $self->{'use_ascii_only_filenames'}; $self->init_cache_for_file($filename); my $originalfilename = undef; my $type = "unknown"; my $output_dir = $self->{'cached_dir'}; my $ivideo_root = $self->{'cached_file_root'}; my $convertto_regenerated = 0; if (($converttotype ne "" && $filename =~ m/$converttotype$/i) || (($converttosize ne "" && $converttosize ne $video_width) || ($converttosize ne "" && $converttosize ne $video_height))) { if ($converttotype eq "") { # in this block because the video width x height different to original # => set (for this call to run_convert only) converttotype # to input file extension ($converttotype) = ($filename =~ m/\.(.*?)$/); } $originalfilename = $filename; $file = "$ivideo_root.$converttotype"; $filename = &util::filename_cat($output_dir,$file); my $s_opt = $self->optional_frame_scale($converttosize,$video_width,$video_height); my $exp_duration = $self->{'exp_duration'}; my $t_opt = (defined $exp_duration) ? "-t $exp_duration" : ""; my $main_opts = "-y $t_opt $s_opt"; my $originalfilename_gsdlenv = $self->gsdlhome_independent($originalfilename); my $filename_gsdlenv = $self->gsdlhome_independent($filename); my $convertto_command = "ffmpeg $main_opts -i \"$originalfilename_gsdlenv\""; $convertto_command .= " -ar 22050" if ($converttotype eq "flv"); $convertto_command .= " -y \"$filename_gsdlenv\""; my $convertto_result; my $convertto_error; my $convertto_options = { @{$self->{'ffmpeg_monitor'}}, 'message_prefix' => "Convert to", 'message' => "Converting video to $converttotype" }; ($convertto_regenerated,$convertto_result,$convertto_error) = $self->run_cached_general_cmd($convertto_command,$filename,$convertto_options); $type = $converttotype; } # Add the video metadata my $file_unicode = pack("U0C*", map { ord($_) } split(//,$file)); # force explicitly to unicode $file_unicode =~ s/\x{2018}|\x{2019}|\x{201C}|\x{201D}//g; # remove smart quotes as cause problem in URL for video server $file_unicode =~ s/\x{2013}/\-/g; # change en-dash to '-' as again causes problems for video server ## print STDERR "**** file metadata = ", &gsprintf::debug_unicode_string($file_unicode), "\n"; # split filename on char if specified if ($file_unicode =~ m/\-/) { my @file_split = split(/\s*\-\s*/,$file_unicode); my $creator = shift @file_split; my $title = shift @file_split; $title =~ s/\..*?$//; $title =~ s/^(.)/\u$1/; $doc_obj->add_utf8_metadata($section,"Title",$title); my @creator_split = split(/\s+and\s+/,$creator); foreach my $c (@creator_split) { $doc_obj->add_utf8_metadata($section,"Creator",$c); } } $file = $file_unicode; my $filemeta = $self->filename_to_utf8_metadata($file); my $filemeta_url_safe = $self->url_safe($filemeta); $doc_obj->add_utf8_metadata ($section, "Video", $filemeta_url_safe); # Also want to set filename as 'Source' metadata to be # consistent with other plugins $doc_obj->add_utf8_metadata ($section, "Source", $filemeta_url_safe); if ($video_type ne " ") { $type = $video_type; } $doc_obj->add_metadata ($section, "FileFormat", $type); $doc_obj->add_metadata ($section, "FileSize", $video_size); $doc_obj->add_metadata ($section, "VideoType", $video_type); $doc_obj->add_metadata ($section, "VideoWidth", $video_width); $doc_obj->add_metadata ($section, "VideoHeight", $video_height); $doc_obj->add_metadata ($section, "VideoDuration", $video_duration); $doc_obj->add_metadata ($section, "VideoSize", $video_size); $doc_obj->add_metadata ($section, "VideoCodec", $vcodec); $doc_obj->add_metadata ($section, "VideoFPS", $vfps); $doc_obj->add_metadata ($section, "AudioType", $atype); $doc_obj->add_metadata ($section, "AudioFreq", $afreq); $doc_obj->add_metadata ($section, "AudioChannels", $achan); $doc_obj->add_metadata ($section, "AudioRate", $arate); $doc_obj->add_utf8_metadata ($section, "srclink", ""); $doc_obj->add_utf8_metadata ($section, "/srclink", ""); $doc_obj->add_metadata ($section, "srcicon", "[VideoType]"); # Add the image as an associated file $doc_obj->associate_file($filename,$file,"video/$type",$section); if ($self->{'extractkeyframes'}) { $self->extract_keyframes($doc_obj,$originalfilename,$filename); } my $streamable_regenerated = 0; if ($self->{'enable_streaming'}) { $streamable_regenerated = $self->enable_full_streaming($doc_obj, $originalfilename,$filename, $convertto_regenerated, $video_width,$video_height); } my $thumbnailsize = $self->{'thumbnailsize'} || 100; my $thumbnailtype = $self->{'thumbnailtype'} || 'jpg'; if ($self->{'create_thumbnail'} eq "true") { my $thumbnail_width; my $thumbnail_height; if ($video_width>$video_height) { my $scale_ratio = $video_height / $video_width; $thumbnail_width = $thumbnailsize; $thumbnail_height = int($thumbnailsize * $scale_ratio); } else { my $scale_ratio = $video_width / $video_height; $thumbnail_width = int($thumbnailsize * $scale_ratio); $thumbnail_height = $thumbnailsize; } # for some video formats, extracted size needs to be multiple of 2 $thumbnail_width = int($thumbnail_width/2) * 2; $thumbnail_height = int($thumbnail_height/2) * 2; $self->extract_thumbnail($doc_obj,$filename,$convertto_regenerated, $thumbnailtype, $thumbnail_width,$thumbnail_height); } if ($self->{'extractkeyframes'}) { $self->extract_keyframes_montage($doc_obj,$filename,$thumbnailtype); } my $screenviewsize = $self->{'screenviewsize'}; my $screenviewtype = $self->{'screenviewtype'} || 'jpeg'; # Make a screen-sized version of the picture if requested if ($self->{'create_screenview'} eq "true") { # To do: if the actual image smaller than the screenview size, # we should use the original ! my $screenview_width; my $screenview_height; if ($video_width>$video_height) { my $scale_ratio = $video_height / $video_width; $screenview_width = $screenviewsize; $screenview_height = int($screenviewsize * $scale_ratio); } else { my $scale_ratio = $video_width / $video_height; $screenview_width = int($screenviewsize * $scale_ratio); $screenview_height = $screenviewsize; } # for some video formats, extracted size needs to be multiple of 2 $screenview_width = int($screenview_width/2) * 2; $screenview_height = int($screenview_height/2) * 2; $self->extract_screenview($doc_obj,$filename, $screenviewtype, $screenview_width,$screenview_height); } return $type; } sub read_into_doc_obj { my $self = shift (@_); my ($pluginfo, $base_dir, $file, $block_hash, $metadata, $processor, $maxdocs, $total_count, $gli) = @_; my ($rv,$doc_obj) = $self->SUPER::read_into_doc_obj(@_); if ($rv != 1) { return ($rv,$doc_obj); } if (($self->{'enablestreaming'}) && ($self->{'extractkeyframes'})) { my $section = $doc_obj->get_top_section(); my $oflash_filename = $self->{'oflash_filename'}; my ($streamkeyframes_cmd,$ostreamkeyframes_filename) = $self->streamkeyframes_cmd($oflash_filename,$doc_obj,$section); my $verbosity = $self->{'verbosity'}; my $streamkeyframes_options = { @{$self->{'flvtool2_monitor'}}, 'message_prefix' => "Stream Keyframes", 'message' => "Reprocessing video stream to add keyframes on timeline" }; $self->run_general_cmd($streamkeyframes_cmd,$streamkeyframes_options); } return ($rv,$doc_obj); } sub output_distributed_keyframes { my ($self) = shift @_; my ($timeline,$num_dist_frames) = @_; my $num_total_frames = $self->{'num-total-frames'}; my $frame_delta = $num_total_frames/$num_dist_frames; my $closest_to = $frame_delta; my $prev_t = 0; # print STDERR "*** num total frames = $num_total_frames\n"; # print STDERR "*** frame delta = $frame_delta\n"; foreach my $curr_t (sort { $a <=> $b } keys %$timeline) { # print STDERR "*** curr_t = $curr_t\n"; if ($curr_t>$closest_to) { # decide if previous t (prev_t_ or current t (curr_t) is closest my $timeline_rec; my $prev_t_dist = $closest_to - $prev_t; my $curr_t_dist = $curr_t - $closest_to; if ($curr_t_dist <= $prev_t_dist) { $timeline_rec = $timeline->{$curr_t} } else { $timeline_rec = $timeline->{$prev_t} } my $name = $timeline_rec->{'name'}; my $timestamp = $timeline_rec->{'timestamp'}; my $thumb = $timeline_rec->{'thumb'}; print CUEOUT " \n"; print CUEOUT " $name\n"; print CUEOUT " $timestamp\n"; print CUEOUT " \n"; print CUEOUT " $thumb\n"; print CUEOUT " \n"; print CUEOUT " navigation\n"; print CUEOUT " \n"; # my $testtime = $timestamp+1000; # print CUEOUT " \n"; # print CUEOUT " Test $name\n"; # print CUEOUT " $testtime\n"; # print CUEOUT " \n"; # print CUEOUT " $thumb\n"; # print CUEOUT " 1\n"; # print CUEOUT " 0\n"; # print CUEOUT " \n"; # print CUEOUT " event\n"; # print CUEOUT " \n"; $closest_to += $frame_delta; } $prev_t = $curr_t; } } sub StartDocument {$_[0]->{'PluginObj'}->xml_start_document(@_);} sub XMLDecl {$_[0]->{'PluginObj'}->xml_xmldecl(@_);} sub Entity {$_[0]->{'PluginObj'}->xml_entity(@_);} sub Doctype {$_[0]->{'PluginObj'}->xml_doctype(@_);} sub StartTag {$_[0]->{'PluginObj'}->xml_start_tag(@_);} sub EndTag {$_[0]->{'PluginObj'}->xml_end_tag(@_);} sub Text {$_[0]->{'PluginObj'}->xml_text(@_);} sub PI {$_[0]->{'PluginObj'}->xml_pi(@_);} sub EndDocument {$_[0]->{'PluginObj'}->xml_end_document(@_);} sub Default {$_[0]->{'PluginObj'}->xml_default(@_);} # This Char function overrides the one in XML::Parser::Stream to overcome a # problem where $expat->{Text} is treated as the return value, slowing # things down significantly in some cases. sub Char { use bytes; # Necessary to prevent encoding issues with XML::Parser 2.31+ $_[0]->{'Text'} .= $_[1]; return undef; } sub xml_start_document { my $self = shift(@_); my ($expat, $name, $sysid, $pubid, $internal) = @_; } # Called for XML declarations sub xml_xmldecl { my $self = shift(@_); my ($expat, $version, $encoding, $standalone) = @_; } # Called for XML entities sub xml_entity { my $self = shift(@_); my ($expat, $name, $val, $sysid, $pubid, $ndata) = @_; } # Called for DOCTYPE declarations - use die to bail out if this doctype # is not meant for this plugin sub xml_doctype { my $self = shift(@_); my ($expat, $name, $sysid, $pubid, $internal) = @_; # This test used to be done in xml_start_document # Moved to here as seems more logical if ($name !~ "seg") { die "VideoPlugin: Root tag $name does not match expected "; } } sub xml_start_tag { my $self = shift(@_); my ($expat, $element) = @_; my %attr = %_; if ($element eq "seg") { $self->{'keyframe_index'} = 0; $self->{'keyframe_fnames'} = []; $self->{'keyframe_timeline'} = {}; #$self->{'flowplayer_thumblist'} = "thumbs: \\["; my $output_dir = $self->{'cached_dir'}; my $cue_filename = &util::filename_cat($output_dir,"on_cue.xml"); open(CUEOUT,">$cue_filename") || die "Unable to open $cue_filename: $!\n"; print CUEOUT "\n"; } elsif ($element eq "trans") { my $trans_type = $attr{'type'}; my $pre_frame_num = $attr{'preFNum'}; my $post_frame_num = $attr{'postFNum'}; my $avg_frame_num = int(($pre_frame_num+$post_frame_num)/2.0)+1; my $output_dir = $self->{'cached_dir'}; my $ivideo_root = $self->{'cached_file_root'}; my $keyframe_index = $self->{'keyframe_index'}; my $fps = $self->{'video-fps'}; if ($keyframe_index==0) { # hive actually generates one extra keyframe at the start, # which is half way between frame 0 and the frist pre_frame my $thumb_file = sprintf("%s_%04d.jpg",$ivideo_root,$keyframe_index); my $thumb_filename = &util::filename_cat($output_dir,$thumb_file); push(@{$self->{'keyframe_fnames'}},$thumb_file); my $half_frame_num = $pre_frame_num/2.0; my $time_msec = ($half_frame_num / $fps) * 1000; # print CUEOUT " \n"; # print CUEOUT " Keyframe $keyframe_index\n"; # print CUEOUT " $time_msec\n"; # print CUEOUT " \n"; # print CUEOUT " $thumb_file\n"; # print CUEOUT " \n"; # print CUEOUT " navigation\n"; # print CUEOUT " \n"; my $timeline_rec = { 'name'=> "Keyframe $keyframe_index", 'keyframeindex' => $keyframe_index, 'timestamp' => $time_msec, 'thumb' => $thumb_file }; $self->{'keyframe_timeline'}->{$half_frame_num}=$timeline_rec; } $keyframe_index++; my $thumb_file = sprintf("%s_%04d.jpg",$ivideo_root,$keyframe_index); my $thumb_filename = &util::filename_cat($output_dir,$thumb_file); push(@{$self->{'keyframe_fnames'}},$thumb_file); my $time_msec = (($avg_frame_num) / $fps) * 1000; my $time_sec = (($avg_frame_num)/ $fps); # print CUEOUT " \n"; # print CUEOUT " Keyframe $keyframe_index\n"; # print CUEOUT " $time_msec\n"; # print CUEOUT " \n"; # print CUEOUT " $thumb_file\n"; # print CUEOUT " \n"; # print CUEOUT " navigation\n"; # print CUEOUT " \n"; my $timeline_rec = { 'name'=> "Keyframe $keyframe_index", 'keyframeindex' => $keyframe_index, 'timestamp' => $time_msec, 'thumb' => $thumb_file }; $self->{'keyframe_timeline'}->{$avg_frame_num}=$timeline_rec; # $self->{'flowplayer_thumblist'} .= "\\{ thumbNail: '$thumb_file', time: $time_sec \\},"; $self->{'keyframe_index'} = $keyframe_index; } } sub xml_end_tag { my $self = shift(@_); my ($expat, $element) = @_; if ($element eq "seg") { $self->output_distributed_keyframes($self->{'keyframe_timeline'},6); print CUEOUT "\n"; close(CUEOUT); #$self->{'flowplayer_thumblist'} .= "\\]"; } } # Called just before start or end tags with accumulated non-markup text in # the $_ variable. sub xml_text { my $self = shift(@_); my ($expat) = @_; } # Called for processing instructions. The $_ variable will contain a copy # of the pi. sub xml_pi { my $self = shift(@_); my ($expat, $target, $data) = @_; } # Called at the end of the XML document. sub xml_end_document { my $self = shift(@_); my ($expat) = @_; # **** # $self->close_document(); } # Called for any characters not handled by the above functions. sub xml_default { my $self = shift(@_); my ($expat, $text) = @_; } 1;