Changeset 18556
- Timestamp:
- 2009-02-20T11:40:23+13:00 (15 years ago)
- Location:
- extensions/gsdl-video/trunk/perllib/plugins
- Files:
-
- 4 added
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
extensions/gsdl-video/trunk/perllib/plugins/MultimediaConverter.pm
r18543 r18556 1 ########################################################################## 2 # 3 # videoconvert.pm -- utility to help convert video file 4 # 5 # Copyright (C) 1999 DigiLib Systems Limited, NZ 1 ########################################################################### 2 # 3 # MultimediaConverter - helper plugin that does audio and video conversion using ffmpeg 4 # 5 # A component of the Greenstone digital library software 6 # from the New Zealand Digital Library Project at the 7 # University of Waikato, New Zealand. 8 # 9 # Copyright (C) 2008 New Zealand Digital Library Project 6 10 # 7 11 # This program is free software; you can redistribute it and/or modify … … 20 24 # 21 25 ########################################################################### 22 23 24 package videoconvert; 26 package MultimediaConverter; 27 28 use BaseMediaConverter; 29 25 30 26 31 use strict; 27 32 no strict 'refs'; # allow filehandles to be variables and viceversa 28 33 29 use baseconvert; 30 31 sub BEGIN { 32 @videoconvert::ISA = ('baseconvert'); 33 } 34 35 36 # Discover the characteristics of a video file. 37 # Equivalent step to that in ImagePlugin that uses ImageMagicks's 'indentify' utility 38 # Here we use 'ffmpeg' for video but for consistency keep the Perl method name the same 39 # as before 34 use gsprintf 'gsprintf'; 35 36 BEGIN { 37 @MultimediaConverter::ISA = ('BaseMediaConverter'); 38 } 39 40 my $arguments = [ 41 { 'name' => "converttotype", 42 'desc' => "{ImageConverter.converttotype}", 43 'type' => "string", 44 'deft' => "", 45 'reqd' => "no" }, 46 { 'name' => "minimumsize", 47 'desc' => "{ImageConverter.minimumsize}", 48 'type' => "int", 49 'deft' => "100", 50 'range' => "1,", 51 'reqd' => "no" }, 52 ]; 53 54 my $options = { 'name' => "MultimediaConverter", 55 'desc' => "{MultimediaConverter.desc}", 56 'abstract' => "yes", 57 'inherits' => "yes", 58 'args' => $arguments }; 59 60 sub new { 61 my ($class) = shift (@_); 62 my ($pluginlist,$inputargs,$hashArgOptLists) = @_; 63 push(@$pluginlist, $class); 64 65 push(@{$hashArgOptLists->{"ArgList"}},@{$arguments}); 66 push(@{$hashArgOptLists->{"OptList"}},$options); 67 68 my $self = new BaseMediaConverter($pluginlist, $inputargs, $hashArgOptLists, 1); 69 70 71 return bless $self, $class; 72 73 } 74 75 76 77 # needs to be called after BasePlugin init, so that outhandle is set up. 78 sub init { 79 my $self = shift(@_); 80 81 my $outhandle = $self->{'outhandle'}; 82 83 $self->{'tmp_file_paths'} = (); 84 85 # Check that ffmpeg is installed and available on the path 86 my $multimedia_conversion_available = 1; 87 my $no_multimedia_conversion_reason = ""; 88 89 # None of this works very well on Windows 95/98... 90 if ($ENV{'GSDLOS'} eq "windows" && !Win32::IsWinNT()) { 91 $multimedia_conversion_available = 0; 92 $no_multimedia_conversion_reason = "win95notsupported"; 93 } else { 94 95 my $result = `ffmpeg -h 2>&1`; 96 97 98 if (!defined $result || $result !~ m/^FFmpeg version/) { 99 $self->{'ffmpeg_installed'} = 0; 100 print $outhandle $result; 101 $multimedia_conversion_available = 0; 102 $no_multimedia_conversion_reason = "ffmpegnotinstalled"; 103 } 104 else { 105 $self->{'ffmpeg_installed'} = 1; 106 } 107 } 108 109 $self->{'multimedia_conversion_available'} = $multimedia_conversion_available; 110 $self->{'no_multimedia_conversion_reason'} = $no_multimedia_conversion_reason; 111 112 if ($self->{'multimedia_conversion_available'} == 0) { 113 &gsprintf($outhandle, "MultimediaConverter: {MultimediaConverter.noconversionavailable} ({MultimediaConverter.".$self->{'no_multimedia_conversion_reason'}."})\n"); 114 } 115 116 } 117 118 40 119 41 120 42 121 sub identify { 43 my ($video, $outhandle, $verbosity) = @_; 44 45 # Use the ffmpeg command to get the file specs 46 my $command = "ffmpeg -i \"$video\""; 47 48 print $outhandle " $command\n" if ($verbosity > 2); 49 my $result = ''; 50 $result = `$command 2>&1`; 51 print $outhandle " $result\n" if ($verbosity > 4); 52 53 # Read the type, width, and height etc. 54 my $vtype = 'unknown'; 55 my $vcodec = 'unknown'; 56 my $width = 'unknown'; 57 my $height = 'unknown'; 58 my $fps = 'unknown'; 59 60 my $atype = 'unknown'; 61 my $afreq = 'unknown'; 62 my $achan = 'unknown'; 63 my $arate = 'unknown'; 64 65 my $video_safe = quotemeta $video; 66 67 # strip off everything up to filename 68 $result =~ s/^.*\'$video_safe\'://s; 69 70 if ($result =~ m/Video: (.*?) fps/m) { 71 my $video_info = $1; 72 if ($video_info =~ m/([^,]+),(?: ([^,]+),)? (\d+)x(\d+),.*?(\d+\.\d+)/) 73 { 74 $vtype = $1; 75 $vcodec = $2 if defined $2; 76 $width = $3; 77 $height = $4; 78 $fps = $5; 79 } 80 } 81 82 ## if ($result =~ m/Video: (\w+), (\w+), (\d+)x(\d+),.*?(\d+\.\d+) fps/m) { 83 # if ($result =~ m/Video: ([^,]+),(?: ([^,]+),)? (\d+)x(\d+),.*?(\d+\.\d+) fps/m) { 84 # $vtype = $1; 85 # $vcodec = $2; 86 # $width = $3; 87 # $height = $4; 88 # $fps = $5; 89 # } 90 91 if ($result =~ m/Audio: (\w+), (\d+) Hz, (\w+)(?:, (\d+.*))?/m) { 92 $atype = $1; 93 $afreq = $2; 94 $achan = $3; 95 $arate = $4 if (defined $4); 96 } 97 98 # Read the duration 99 my $duration = "unknown"; 100 if ($result =~ m/Duration: (\d+:\d+:\d+\.\d+)/m) { 101 $duration = $1; 102 } 103 print $outhandle " file: $video:\t $vtype, $width, $height, $duration\n" 104 if ($verbosity > 2); 105 106 if ($verbosity >3) { 107 print $outhandle "\t video codec=$vcodec, fps = $fps\n"; 108 print $outhandle "\t audio codec=$atype, freq = $afreq Hz, $achan, $arate\n"; 109 } 110 111 # Return the specs 112 return ($vtype, $width, $height, $duration, -s $video, 113 $vcodec,$fps,$atype,$afreq,$achan,$arate); 114 } 115 116 117 sub vob_durations 118 { 119 my ($media_base_dir,$title_num,$outhandle) = @_; 120 121 my $filter_re = sprintf("^VTS_%02d_[1-9]\\.VOB\$",$title_num); 122 123 my $duration_info = {}; 124 125 if (opendir(VTSIN,$media_base_dir)) { 126 my @vts_title_vobs = grep { $_ =~ m/$filter_re/ } readdir(VTSIN); 127 closedir(VTSIN); 128 129 foreach my $v (@vts_title_vobs) { 130 my $full_v = &util::filename_cat($media_base_dir,$v); 131 132 my ($vtype, $width, $height, $duration, $vsize, 133 $vcodec,$fps,$atype,$afreq,$achan,$arate) = identify($full_v,$outhandle,0); 134 135 my ($vob_num) = ($v =~ m/^VTS_\d\d_(\d)\.VOB$/); 136 137 $duration_info->{$vob_num} = $duration; 138 print STDERR "**** $title_num: $title_num, storing {$vob_num} => $duration\n"; 139 140 } 141 142 } 143 else { 144 print $outhandle "Warning: unable to read files in directory $media_base_dir\n"; 145 } 146 147 return $duration_info; 148 149 } 150 151 152 153 sub new { 154 my ($class) = shift @_; 155 156 my ($base_dir,$video_filename, $verbosity,$outhandle, 157 $exp_duration,$ascii_only_filenames) = @_; 158 159 my $self = new baseconvert($base_dir,$video_filename,$verbosity,$outhandle); 160 161 $self->{'exp_duration'} = $exp_duration; 122 my ($filename, $outhandle, $verbosity) = @_; 123 124 die "MultimediaConvert::identify() needs to be defined by inheriting plugin"; 125 } 126 127 128 129 sub init_cache_for_file { 130 my $self = shift(@_); 131 132 my ($media_filename) = @_; 133 134 $self->SUPER::init_cache_for_file($media_filename); 135 136 137 # This should probably be replaced with Anu's work that replaced 138 # non-ASCII chars with URL encodings 139 140 my $ascii_only_filenames = $self->{'use_ascii_only_filenams'}; 162 141 163 142 if (defined $ascii_only_filenames && ($ascii_only_filenames)) { 164 my $file_root = $self->{' file_root'};165 $self->{' file_root'} = ascii_only_filename($file_root);143 my $file_root = $self->{'cached_file_root'}; 144 $self->{'cached_file_root'} = ascii_only_filename($file_root); 166 145 } 167 146 168 my @ffmpeg_monitor = ( 'monitor_init' , " videoconvert::ffmpeg_monitor_init",169 'monitor_line' , " videoconvert::ffmpeg_monitor_line",170 'monitor_deinit' , " videoconvert::ffmpeg_monitor_deinit" );147 my @ffmpeg_monitor = ( 'monitor_init' , "MultimediaConverter::ffmpeg_monitor_init", 148 'monitor_line' , "MultimediaConverter::ffmpeg_monitor_line", 149 'monitor_deinit' , "MultimediaConverter::ffmpeg_monitor_deinit" ); 171 150 172 my @flvtool2_monitor = ( 'monitor_init' ,"monitor_init_unbuffered",173 'monitor_line' , "videoconvert::flvtool2_monitor_line",174 'monitor_deinit' , "monitor_deinit_unbuffered" );175 151 176 152 $self->{'ffmpeg_monitor'} = \@ffmpeg_monitor; 177 $self->{'flvtool2_monitor'} = \@flvtool2_monitor; 178 179 180 return bless $self, $class; 181 } 153 154 155 } 156 157 182 158 183 159 … … 216 192 217 193 my $url = $file_unicode; 218 $url =~ s/\x{2018}|\x{2019}|\x{201C}|\x{201D}//g; # remove smart quotes as cause problem in URL for videoserver219 $url =~ s/\x{2013}/\-/g; # change en-dash to '-' as again causes problems for videoserver194 $url =~ s/\x{2018}|\x{2019}|\x{201C}|\x{201D}//g; # remove smart quotes as cause problem in URL for streaming web server 195 $url =~ s/\x{2013}/\-/g; # change en-dash to '-' as again causes problems for streaming web server 220 196 221 197 return $url; … … 255 231 256 232 257 258 sub optional_frame_scale259 {260 my $self = shift (@_);261 my ($orig_size,$video_width,$video_height) = @_;262 263 my $s_opt = "";264 if ($video_width > $video_height) {265 if ($video_width > $orig_size) {266 my $scale_factor = $orig_size/$video_width;267 my $scaled_width = int($video_width * $scale_factor);268 my $scaled_height = int($video_height * $scale_factor);269 270 # round to be ensure multiple of 2 (needed by some codecs)271 $scaled_width = int($scaled_width/2)*2;272 $scaled_height = int($scaled_height/2)*2;273 274 $s_opt = "-s ${scaled_width}x${scaled_height}";275 }276 # else, video is smaller than requested size, don't scale up277 }278 else {279 if ($video_height > $orig_size) {280 my $scale_factor = $orig_size/$video_height;281 my $scaled_width = int($video_width * $scale_factor);282 my $scaled_height = int($video_height * $scale_factor);283 284 # round to be ensure multiple of 2 (needed by some codecs)285 $scaled_width = int($scaled_width/2)*2;286 $scaled_height = int($scaled_height/2)*2;287 288 $s_opt = "-s ${scaled_width}x${scaled_height}";289 }290 # else, video is smaller than requested size, don't scale up291 292 }293 294 return $s_opt;295 }296 297 298 sub keyframe_cmd299 {300 my $self = shift (@_);301 my ($ivideo_filename) = @_;302 303 my $video_ext_dir = &util::filename_cat($ENV{'GSDLHOME'},"ext","video");304 305 my $output_dir = $self->{'cached_dir'};306 my $ivideo_root = $self->{'file_root'};307 308 my $oshot_filename = &util::filename_cat($output_dir,"shots.xml");309 310 my $exp_duration = $self->{'exp_duration'};311 my $t_opt = (defined $exp_duration) ? "-t $exp_duration" : "";312 313 my $main_opts = "-y $t_opt";314 315 my $hive = &util::filename_cat($video_ext_dir,"lib","vhook","hive.so");316 317 my $oflash_filename = &util::filename_cat($output_dir,"$ivideo_root\_keyframe.flv");318 319 my $vhook_opts = "$hive -o $oshot_filename -k $output_dir $ivideo_filename";320 321 my $ivideo_filename_gsdlenv = $self->gsdlhome_independent($ivideo_filename);322 my $oflash_filename_gsdlenv = $self->gsdlhome_independent($oflash_filename);323 324 my $ffmpeg_cmd = "ffkeyframe $main_opts -vhook \"$vhook_opts\" -i \"$ivideo_filename_gsdlenv\" -an -y \"$oflash_filename_gsdlenv\"";325 326 327 return ($ffmpeg_cmd,$oflash_filename);328 }329 330 331 sub stream_cmd332 {333 my $self = shift (@_);334 my ($ivideo_filename,$video_width,$video_height,335 $streaming_quality,336 $streaming_bitrate,$streaming_size,337 $opt_streaming_achan, $opt_streaming_arate) = @_;338 339 my $streaming_achan340 = (defined $opt_streaming_achan) ? $opt_streaming_achan : 2;341 342 my $streaming_arate343 = (defined $opt_streaming_arate) ? $opt_streaming_arate : 22050;344 345 my $output_dir = $self->{'cached_dir'};346 my $ivideo_root = $self->{'file_root'};347 348 my $oflash_file = "${ivideo_root}_stream.flv";349 my $oflash_filename = &util::filename_cat($output_dir,$oflash_file);350 351 my $s_opt = $self->optional_frame_scale($streaming_size,$video_width,$video_height);352 353 my $exp_duration = $self->{'exp_duration'};354 my $t_opt = (defined $exp_duration) ? "-t $exp_duration" : "";355 356 my $main_opts = "-y $t_opt";357 358 my $bitrate_opt = "-b $streaming_bitrate";359 ### my $stream_opts = "-r 25 $s_opt";360 my $stream_opts .= " $s_opt -ac $streaming_achan -ar $streaming_arate";361 362 # -flags +ilme+ildct' and maybe '-flags +alt' for interlaced material, and try '-top 0/1'363 364 my $all_opts = "$main_opts $stream_opts";365 366 my $ffmpeg_cmd;367 368 my $ivideo_filename_gsdlenv = $self->gsdlhome_independent($ivideo_filename);369 my $oflash_filename_gsdlenv = $self->gsdlhome_independent($oflash_filename);370 371 if ($streaming_quality eq "high") {372 373 my $pass_log_file = &util::filename_cat($output_dir,"$ivideo_root-logpass.txt");374 if (-e $pass_log_file) {375 &util::rm($pass_log_file);376 }377 378 my $pass_log_file_gsdlenv = $self->gsdlhome_independent($pass_log_file);379 380 $all_opts .= " -passlogfile \"$pass_log_file_gsdlenv\"";381 382 my $ffmpeg_cmd_pass1 = "ffmpeg -pass 1 -i \"$ivideo_filename_gsdlenv\" $all_opts -y \"$oflash_filename_gsdlenv\"";383 384 my $ffmpeg_cmd_pass2 = "ffmpeg -pass 2 -i \"$ivideo_filename_gsdlenv\" $all_opts $bitrate_opt -y \"$oflash_filename_gsdlenv\"";385 $ffmpeg_cmd = "( $ffmpeg_cmd_pass1 ; $ffmpeg_cmd_pass2 )";386 }387 else {388 # single pass389 390 $ffmpeg_cmd = "ffmpeg -i \"$ivideo_filename_gsdlenv\" $all_opts -y \"$oflash_filename_gsdlenv\"";391 }392 393 return ($ffmpeg_cmd,$oflash_filename,$oflash_file);394 }395 396 397 398 sub audio_excerpt_cmd399 {400 my $self = shift (@_);401 my ($ivoa_filename,$hh,$mm,$ss,$opt_excerpt_len) = @_;402 403 # ivoa = input video or audio404 405 my $time_encoded = "$hh:$mm:$ss";406 my $time_encoded_file = "$hh$mm$ss";407 408 409 my $output_dir = $self->{'cached_dir'};410 my $ivoa_root = $self->{'file_root'};411 412 my $omp3_file = "${ivoa_root}_$time_encoded_file.mp3";413 my $omp3_filename = &util::filename_cat($output_dir,$omp3_file);414 415 my $all_opts = "-y -acodec mp3 -ss $time_encoded ";416 417 if (defined $opt_excerpt_len) {418 $all_opts .= "-t $opt_excerpt_len ";419 }420 421 422 my $ivoa_filename_gsdlenv = $self->gsdlhome_independent($ivoa_filename);423 my $omp3_filename_gsdlenv = $self->gsdlhome_independent($omp3_filename);424 425 426 my $ffmpeg_cmd = "ffmpeg -i \"$ivoa_filename_gsdlenv\" $all_opts \"$omp3_filename_gsdlenv\"";427 428 return ($ffmpeg_cmd,$omp3_filename,$omp3_file);429 }430 431 432 433 sub streamseekable_cmd434 {435 my $self = shift (@_);436 my ($oflash_filename) = @_;437 438 my $output_dir = $self->{'cached_dir'};439 my $ivideo_root = $self->{'file_root'};440 441 my $cue_filename = &util::filename_cat($output_dir,"on_cue.xml");442 443 my $flvtool_cmd = "flvtool2 -vUP \"$oflash_filename\"";444 445 return ($flvtool_cmd,$oflash_filename);446 }447 448 449 sub streamkeyframes_cmd450 {451 my $self = shift (@_);452 my ($oflash_filename,$doc_obj,$section) = @_;453 454 my $assocfilepath455 = $doc_obj->get_metadata_element($section,"assocfilepath");456 457 my $output_dir = $self->{'cached_dir'};458 459 my $cue_filename = &util::filename_cat($output_dir,"on_cue.xml");460 461 my $video_server = $ENV{'GEXT_VIDEO_SERVER'};462 my $video_prefix = $ENV{'GEXT_VIDEO_PREFIX'};463 464 my $collect = $ENV{'GSDLCOLLECTION'};465 466 my $flvtool_cmd = "flvtool2 -vAUtP \"$cue_filename\" -thumbLocation:$video_server$video_prefix/collect/$collect/index/assoc/$assocfilepath \"$oflash_filename\"";467 468 469 return ($flvtool_cmd,$oflash_filename);470 }471 472 473 sub streamcuepts_cmd474 {475 my $self = shift (@_);476 my ($oflash_filename) = @_;477 478 my $output_dir = $self->{'cached_dir'};479 480 my $cue_filename = &util::filename_cat($output_dir,"on_cue.xml");481 482 my $video_server = $ENV{'GEXT_VIDEO_SERVER'};483 my $video_prefix = $ENV{'GEXT_VIDEO_PREFIX'};484 485 my $collect = $ENV{'GSDLCOLLECTION'};486 my $thumbloc = "$video_server$video_prefix/collect/$collect";487 488 489 # my $flvtool_cmd = "flvtool2 -vUAtP \"$cue_filename\" -thumbLocation:$thumbloc \"$oflash_filename\"";490 491 # my $flvtool_cmd = "flvtool2 -vUAt \"$cue_filename\" \"$oflash_filename\"";492 493 494 495 # my $flvtool_cmd = "flvtool2 -vUAt \"$cue_filename\" \"$oflash_filename\"";496 497 498 ## my $flvtool_cmd = "flvtool2 -vAt \"$cue_filename\" -UP \"$oflash_filename\" \"$output_dir/updated.flv\"";499 500 ## my $flvtool_cmd = "flvtool2 -vAtU \"$cue_filename\" \"$oflash_filename\" \"$output_dir/updated.flv\"";501 502 my $flvtool_cmd = "flvtool2 -vAtUP \"$cue_filename\" \"$oflash_filename\"";503 504 return ($flvtool_cmd,$oflash_filename);505 }506 507 508 sub keyframe_thumbnail_cmd509 {510 my $self = shift (@_);511 my ($ivideo_filename,$thumbnailfile,$thumbnailwidth,$thumbnailheight) = @_;512 513 my $output_dir = $self->{'cached_dir'};514 my $ivideo_root = $self->{'file_root'};515 516 my $key_filename_prefix = &util::filename_cat($output_dir,$ivideo_root);517 518 519 # Try for 4th keyframe, but fall back to 1st if doesn't exist520 my $key_filename = "${key_filename_prefix}_0003.jpg";521 $key_filename = "${key_filename_prefix}_0000.jpg" if (!-e $key_filename);522 523 my $key_filename_gsdlenv = $self->gsdlhome_independent($key_filename);524 my $thumbnailfile_gsdlenv = $self->gsdlhome_independent($thumbnailfile);525 526 my $command;527 528 if (-e $key_filename) {529 $command = "convert -interlace plane -verbose -geometry $thumbnailwidth"530 . "x$thumbnailheight \"$key_filename_gsdlenv\" \"$thumbnailfile_gsdlenv\"";531 }532 else {533 # extractkeyframe has either not been switched on, or else had534 # a problem when running535 # => extract a from536 # my $frame_rate = 1.0 / 60.0;537 538 my $ivideo_filename_gsdlenv = $self->gsdlhome_independent($ivideo_filename);539 540 541 542 $command = "ffmpeg -i \"$ivideo_filename_gsdlenv\" -ss 12.5 -vframes 1 -f image2 -s ${thumbnailwidth}x${thumbnailheight} -y \"$thumbnailfile_gsdlenv\"";543 544 # fmpeg -i input.dv -r 1 -f image2 -s 120x96 images%05d.png545 }546 547 return ($command,$thumbnailfile);548 }549 550 551 sub keyframe_montage_cmd552 {553 my $self = shift (@_);554 my ($ivideo_filename,$montagefile) = @_;555 556 my $output_dir = $self->{'cached_dir'};557 my $ivideo_root = $self->{'file_root'};558 559 my $key_filename_prefix = &util::filename_cat($output_dir,$ivideo_root);560 561 my $options = "-tile 10 -geometry 75x62+2+2";562 563 my $command = "montage $options ${key_filename_prefix}_*.jpg \"$montagefile\"";564 565 return ($command,$montagefile);566 }567 568 569 570 sub parse_shot_xml571 {572 my ($self) = shift(@_);573 574 my ($plugin) = @_;575 576 my $outhandle = $self->{'outhandle'};577 my $output_dir = $self->{'cached_dir'};578 579 my $shots_filename = &util::filename_cat($output_dir,"shots.xml");580 581 eval {582 $plugin->{'parser'}->parsefile($shots_filename);583 };584 585 if ($@) {586 print $outhandle "videoconvert.pm: skipping $shots_filename as not conformant to Hive shot syntax\n" if ($self->{'verbosity'} > 1);587 print $outhandle "\n Perl Error:\n $@\n" if ($self->{'verbosity'}>2);588 return 0;589 }590 591 }592 593 sub associate_keyframes_old594 {595 my ($self) = shift(@_);596 597 my ($doc_obj,$section,$plugin) = @_;598 599 my $output_dir = $self->{'cached_dir'};600 601 my $count = 1;602 foreach my $kframe_file (@{$plugin->{'keyframe_fnames'}}) {603 604 my $kframe_filename = &util::filename_cat($output_dir,$kframe_file);605 $doc_obj->associate_file($kframe_filename,"keyframe$count.jpg","image/jpeg",606 $section);607 $count++;608 }609 610 $doc_obj->add_utf8_metadata($section,"NumKeyframes",scalar(@{$plugin->{'keyframe_fnames'}}));611 612 613 # *****614 # $doc_obj->add_metadata ($section, "thumblist", $plugin->{'flowplayer_thumblist'});615 616 }617 618 sub associate_keyframes619 {620 my ($self) = shift(@_);621 622 my ($doc_obj,$section,$plugin) = @_;623 624 my $output_dir = $self->{'cached_dir'};625 my $timeline = $plugin->{'keyframe_timeline'};626 627 my $count = 1;628 629 foreach my $t (sort { $timeline->{$a}->{'keyframeindex'} <=> $timeline->{$b}->{'keyframeindex'} } keys %$timeline)630 {631 my $kframe_file = $timeline->{$t}->{'thumb'};632 my $timestamp = $timeline->{$t}->{'timestamp'};633 634 my $kframe_filename = &util::filename_cat($output_dir,$kframe_file);635 $doc_obj->associate_file($kframe_filename,"keyframe$count.jpg","image/jpeg",636 $section);637 $doc_obj->add_utf8_metadata($section,"KeyframeTimestamp",$timestamp);638 639 $count++;640 }641 642 $doc_obj->add_utf8_metadata($section,"NumKeyframes",scalar(@{$plugin->{'keyframe_fnames'}}));643 644 645 # *****646 # $doc_obj->add_metadata ($section, "thumblist", $plugin->{'flowplayer_thumblist'});647 }648 649 650 651 233 sub ffmpeg_monitor_init 652 234 { … … 693 275 694 276 695 sub flvtool2_monitor_line696 {697 my ($line) = @_;698 699 my $had_error = 0;700 my $generate_dot = 1;701 702 if ($line =~ m/\s+\- /) {703 # ignore tabulated output printed at end of command704 $generate_dot = 0;705 }706 707 if ($line =~ m/^Error:/i) {708 $had_error = 1;709 }710 711 return ($had_error,$generate_dot);712 }713 714 715 716 277 717 278 -
extensions/gsdl-video/trunk/perllib/plugins/VideoPlugin.pm
r18490 r18556 1 1 ###################################################################### 2 2 # 3 # VideoPlugin.pm -- plugin for processing video largely based on ImagePlug3 # VideoPlugin.pm -- plugin for processing video files 4 4 # A component of the Greenstone digital library software 5 5 # from the New Zealand Digital Library Project at the … … 24 24 ########################################################################### 25 25 26 # -- Largely modeled on how ImagePlugin works 27 # -- Can convert to audio as well as video 28 26 29 package VideoPlugin; 27 30 … … 30 33 no strict 'subs'; 31 34 32 use videoconvert;33 35 use XMLParser; 34 36 use gsprintf; 35 37 36 use BasePlugin; 38 use MultimediaPlugin; 39 use VideoConverter; 37 40 38 41 sub BEGIN { 39 @VideoPlugin::ISA = ('BasePlugin'); 40 41 if (!defined $ENV{'GEXTVIDEO'}) { 42 print STDERR "Warning: Greenstone Video extension not detected.\n"; 43 } 44 45 } 46 47 48 # Customized from BasePlugin. Make 'incremental' the default 49 # to avoid Greenstone hashing on the file (which in the case of video 50 # is HUGE) to generate OID. Also supress 'hash' as option so the user 51 # can't choose it. 52 53 our $oidtype_list = 54 [ { 'name' => "auto", 55 'desc' => "{BasePlugin.OIDtype.auto}" }, 56 { 'name' => "assigned", 57 'desc' => "{import.OIDtype.assigned}" }, 58 { 'name' => "incremental", 59 'desc' => "{import.OIDtype.incremental}" }, 60 { 'name' => "dirname", 61 'desc' => "{import.OIDtype.dirname}" } ]; 42 @VideoPlugin::ISA = ('MultimediaPlugin', 'VideoConverter'); 43 } 62 44 63 45 … … 67 49 'type' => "regexp", 68 50 'deft' => &get_default_process_exp(), 69 'reqd' => "no" },70 { 'name' => "OIDtype",71 'desc' => "{import.OIDtype}",72 'type' => "enum",73 'list' => $oidtype_list,74 'deft' => "incremental",75 'reqd' => "no",76 'modegli' => "2" },77 78 { 'name' => "noscaleup",79 'desc' => "{VideoPlug.noscaleup}",80 'type' => "flag",81 'reqd' => "no" },82 { 'name' => "use_ascii_only_filenames",83 'desc' => "{VideoPlug.use_ascii_only_filenames}",84 'type' => "flag",85 'reqd' => "no" },86 { 'name' => "video_excerpt_duration",87 'desc' => "{VideoPlug.video_excerpt_duration}",88 'type' => "string",89 'deft' => "",90 'reqd' => "no" },91 { 'name' => "extractthumbnail",92 'desc' => "{VideoPlug.extractthumbnail}",93 'type' => "flag",94 'deft' => "0",95 'reqd' => "no" },96 { 'name' => "thumbnailsize",97 'desc' => "{VideoPlug.thumbnailsize}",98 'type' => "int",99 'deft' => "100",100 'range' => "1,",101 'reqd' => "no" },102 { 'name' => "thumbnailtype",103 'desc' => "{VideoPlug.thumbnailtype}",104 'type' => "string",105 'deft' => "jpeg",106 'reqd' => "no" },107 { 'name' => "extractscreenview",108 'desc' => "{VideoPlug.extractscreenview}",109 'type' => "flag",110 'deft' => "0",111 'reqd' => "no" },112 { 'name' => "screenviewsize",113 'desc' => "{VideoPlug.screenviewsize}",114 'type' => "int",115 'deft' => "0",116 'range' => "1,",117 'reqd' => "no" },118 { 'name' => "screenviewtype",119 'desc' => "{VideoPlug.screenviewtype}",120 'type' => "string",121 'deft' => "jpg",122 'reqd' => "no" },123 { 'name' => "converttotype",124 'desc' => "{VideoPlug.converttotype}",125 'type' => "string",126 'deft' => "",127 'reqd' => "no" },128 { 'name' => "converttosize",129 'desc' => "{VideoPlug.converttosize}",130 'type' => "int",131 'deft' => "",132 ## 'deft' => "352",133 'reqd' => "no" },134 { 'name' => "converttobitrate",135 'desc' => "{VideoPlug.converttobitrate}",136 'type' => "string",137 'deft' => "200k",138 'reqd' => "no" },139 { 'name' => "extractkeyframes",140 'desc' => "{VideoPlug.extractkeyframes}",141 'type' => "flag",142 'deft' => "0",143 'reqd' => "no" },144 { 'name' => "enablestreaming",145 'desc' => "{VideoPlug.enablestreaming}",146 'type' => "flag",147 'deft' => "1",148 'reqd' => "no" },149 { 'name' => "streamingsize",150 'desc' => "{VideoPlug.streamingsize}",151 'type' => "int",152 'deft' => "352",153 'reqd' => "no" },154 { 'name' => "streamingbitrate",155 'desc' => "{VideoPlug.streamingbitrate}",156 'type' => "string",157 'deft' => "200k",158 'reqd' => "no" },159 { 'name' => "minimumsize",160 'desc' => "{VideoPlug.minimumsize}",161 'type' => "int",162 'deft' => "100",163 'range' => "1,",164 51 'reqd' => "no" } ]; 165 52 53 166 54 my $options = { 'name' => "VideoPlugin", 167 'desc' => "{VideoPlug .desc}",55 'desc' => "{VideoPlugin.desc}", 168 56 'abstract' => "no", 169 57 'inherits' => "yes", 170 58 'args' => $arguments }; 171 172 173 my ($self);174 59 175 60 sub new { … … 181 66 push(@{$hashArgOptLists->{"OptList"}},$options); 182 67 183 $self = new BasePlugin($pluginlist, $inputargs, $hashArgOptLists); 184 185 # Check that ffmpeg is installed and available on the path (except for Windows 95/98) 186 # This test is "inherited" from ImagePlugin where ImageMagick support 187 # for building cannot be used on these two versions of Windows as they 188 # do not have enough flexablity in the use of stdout and stderr to 189 # support how our code works. It seems reasonable to assume the 190 # same is true for VideoPlugin work using ffmpeg. 191 192 if (($ENV{'GSDLOS'} ne "windows" || Win32::IsWinNT())) { 193 194 my $result = `ffmpeg -h 2>&1`; 195 196 197 if (!defined $result || $result !~ m/^FFmpeg version/) { 198 $self->{'ffmpeg_installed'} = 0; 199 print STDERR $result; 200 } 201 else { 202 $self->{'ffmpeg_installed'} = 1; 203 } 204 205 } 206 207 208 # create XML::Parser object for parsing metadata.xml files 209 my $parser; 210 if ($]<5.008) { 211 # Perl 5.6 212 $parser = new XML::Parser('Style' => 'Stream', 213 'Handlers' => {'Char' => \&Char, 214 'Doctype' => \&Doctype 215 }); 216 } 217 else { 218 # Perl 5.8 219 $parser = new XML::Parser('Style' => 'Stream', 220 'ProtocolEncoding' => 'ISO-8859-1', 221 'Handlers' => {'Char' => \&Char, 222 'Doctype' => \&Doctype 223 }); 224 } 68 new VideoConverter($pluginlist, $inputargs, $hashArgOptLists); 69 my $self = new MultimediaPlugin($pluginlist, $inputargs, $hashArgOptLists); 70 71 if ($self->{'info_only'}) { 72 # don't worry about creating the XML parser as all we want is the 73 # list of plugin options 74 return bless $self, $class; 75 } 76 77 78 # create XML::Parser object for parsing keyframe files (produced by hive) 79 my $parser = new XML::Parser('Style' => 'Stream', 80 'Pkg' => 'VideoPlugin', 81 'PluginObj' => $self, 82 'Namespaces' => 1, # strip out namespaces 83 'Handlers' => {'Char' => \&Char, 84 'XMLDecl' => \&XMLDecl, 85 'Entity' => \&Entity, 86 'Doctype' => \&Doctype, 87 'Default' => \&Default 88 }); 225 89 226 90 $self->{'parser'} = $parser; … … 228 92 229 93 return bless $self, $class; 94 } 95 96 97 98 sub begin { 99 my $self = shift (@_); 100 my ($pluginfo, $base_dir, $processor, $maxdocs) = @_; 101 102 $self->SUPER::begin(@_); 103 $self->VideoConverter::begin(@_); 230 104 } 231 105 … … 236 110 237 111 $self->SUPER::init(@_); 238 ## $self->ImageConverter::init(); # ****** VideoConverter::init ??112 $self->VideoConverter::init(@_); 239 113 } 240 114 … … 246 120 } 247 121 248 # this makes no sense for image 249 sub block_cover_image122 123 sub extract_keyframes 250 124 { 251 my $self =shift (@_); 252 my ($filename) = @_; 253 254 return; 255 } 256 257 125 my $self = shift (@_); 126 my ($doc_obj,$originalfilename,$filename) = @_; 127 128 my $section = $doc_obj->get_top_section(); 129 130 my $output_dir = $self->{'cached_dir'}; 131 my $ivideo_root = $self->{'cached_file_root'}; 132 133 # Generate the keyframes with ffmpeg and hive 134 my ($keyframe_cmd,$okeyframe_filename) = $self->keyframe_cmd($originalfilename || $filename); 135 136 my $keyframe_options = { @{$self->{'ffmpeg_monitor'}}, 137 'message_prefix' => "Keyframe", 138 'message' => "Extracting keyframes" }; 139 140 $self->autorun_cached_general_cmd($keyframe_cmd,$okeyframe_filename,$keyframe_options); 141 $self->parse_shot_xml(); 142 $self->associate_keyframes($doc_obj,$section); 143 } 144 145 146 sub enable_streaming 147 { 148 my $self = shift (@_); 149 my ($doc_obj,$originalfilename,$filename,$convertto_regenerated, 150 $video_width,$video_height) = @_; 151 152 my $section = $doc_obj->get_top_section(); 153 154 my $output_dir = $self->{'cached_dir'}; 155 my $ivideo_root = $self->{'cached_file_root'}; 156 157 # Generate the Flash FLV format for streaming purposes 158 my $streamable_regenerated = 0; 159 160 my $optionally_run_general_cmd = "run_uncached_general_cmd"; 161 if ($self->{'enable_cache'}) { 162 $optionally_run_general_cmd 163 = ($convertto_regenerated) ? "regenerate_general_cmd" : "run_cached_general_cmd"; 164 } 165 166 my $streaming_bitrate = $self->{'streamingbitrate'}; 167 my $streaming_size = $self->{'streamingsize'}; 168 169 my $streaming_quality = "high"; 170 171 my ($stream_cmd,$oflash_filename,$oflash_file) 172 = $self->stream_cmd($originalfilename || $filename, 173 $video_width,$video_height, 174 $streaming_quality, 175 $streaming_bitrate, $streaming_size); 176 177 178 my $streamable_options = { @{$self->{'ffmpeg_monitor'}}, 179 'message_prefix' => "Stream", 180 'message' => "Generating streamable video: $oflash_file" }; 181 182 my $streamable_result; 183 my $streamable_had_error; 184 ($streamable_regenerated,$streamable_result,$streamable_had_error) 185 = $self->$optionally_run_general_cmd($stream_cmd,$oflash_filename,$streamable_options); 186 187 if (!$streamable_had_error) { 188 my ($streamseekable_cmd,$ostreamseekable_filename) = $self->streamseekable_cmd($oflash_filename); 189 190 my $streamseekable_options = { @{$self->{'flvtool2_monitor'}}, 191 'message_prefix' => "Stream Seekable", 192 'message' => "Reprocessing video stream to be seekable by timeline: $oflash_file" }; 193 194 if ($streamable_regenerated) { 195 $self->run_general_cmd($streamseekable_cmd,$streamseekable_options); 196 } 197 198 my $streamable_url = $oflash_file; 199 ## $streamable_url =~ s/ /%20/g; 200 my $streamable_url_safe = $self->url_safe($streamable_url); 201 202 $doc_obj->add_utf8_metadata ($section, "streamablevideo", $streamable_url_safe); 203 $doc_obj->associate_file($oflash_filename,$oflash_file,"video/flash", 204 $section); 205 } 206 207 ## my $video_width = $doc_obj->get_metadata_element($section,"VideoWidth"); 208 ## my $video_height = $doc_obj->get_metadata_element($section,"VideoHeight"); 209 210 # 211 # FlowPlayer.swf height+22 pixels 212 # FlowPlayerBlack.swf height+16 pixels 213 # FlowPlayerThermo.swf height+16 pixels 214 # FlowPlayerWhite.swf height+26 pixels 215 my $flashwidth = $video_width; 216 my $flashheight = $video_height + 22; 217 if ($self->{'extractkeyframes'}) { 218 $flashheight += 100; 219 } 220 $doc_obj->add_metadata ($section, "flashwidth", $flashwidth); 221 $doc_obj->add_metadata ($section, "flashheight", $flashheight); 222 223 my $video_server = $ENV{'GEXT_VIDEO_SERVER'}; 224 my $video_prefix = $ENV{'GEXT_VIDEO_PREFIX'}; 225 my $base_url = "$video_server$video_prefix/collect/[collection]/index/assoc/[assocfilepath]/"; 226 my $base_url_safe = $self->url_safe($base_url); 227 $doc_obj->add_utf8_metadata ($section, "baseurl",$base_url_safe); 228 229 $self->{'oflash_file'} = $oflash_file; 230 $self->{'oflash_filename'} = $oflash_filename; 231 232 return $streamable_regenerated; 233 } 234 235 sub extract_thumbnail 236 { 237 my $self = shift (@_); 238 my ($doc_obj,$filename,$convertto_regenerated,$thumbnailtype, 239 $thumbnail_width, $thumbnail_height) = @_; 240 241 my $section = $doc_obj->get_top_section(); 242 243 my $output_dir = $self->{'cached_dir'}; 244 my $ivideo_root = $self->{'cached_file_root'}; 245 246 my $verbosity = $self->{'verbosity'}; 247 my $outhandle = $self->{'outhandle'}; 248 249 250 # Generate the thumbnail with convert, a la ImagePlug 251 252 my $thumbnailfile = &util::filename_cat($output_dir,"$ivideo_root.$thumbnailtype"); 253 254 255 my $optionally_run_general_cmd = "run_uncached_general_cmd"; 256 if ($self->{'enable_cache'}) { 257 $optionally_run_general_cmd 258 = ($convertto_regenerated) ? "regenerate_general_cmd" : "run_cached_general_cmd"; 259 } 260 261 my ($thumb_cmd ,$othumb_filename) 262 = $self->keyframe_thumbnail_cmd($filename,$thumbnailfile,$thumbnail_width,$thumbnail_height); 263 264 my $thumb_options = { 'verbosity' => $verbosity, 265 'outhandle' => $outhandle, 266 'message_prefix' => "Thumbnail", 267 'message' => "Generating thumbnail" }; 268 269 my ($thumb_regenerated,$thumb_result,$thumb_had_error) 270 = $self->$optionally_run_general_cmd($thumb_cmd,$thumbnailfile,$thumb_options); 271 272 # Add the thumbnail as an associated file ... 273 if (-e "$thumbnailfile") { 274 $doc_obj->associate_file("$thumbnailfile", "thumbnail.$thumbnailtype", 275 "image/$thumbnailtype",$section); 276 $doc_obj->add_metadata ($section, "ThumbType", $thumbnailtype); 277 $doc_obj->add_metadata ($section, "Thumb", "thumbnail.$thumbnailtype"); 278 279 $doc_obj->add_utf8_metadata ($section, "thumbicon", "<img src=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[Thumb]\" width=[ThumbWidth] height=[ThumbHeight]>"); 280 ### $doc_obj->add_utf8_metadata ($section, "thumbicon", "<img src=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[Thumb]\">"); 281 } 282 283 # Extract Thumnail metadata from convert output 284 if ($thumb_result =~ m/[0-9]+x[0-9]+=>([0-9]+)x([0-9]+)/) { 285 $doc_obj->add_metadata ($section, "ThumbWidth", $1); 286 $doc_obj->add_metadata ($section, "ThumbHeight", $2); 287 } 288 else { 289 # Two reasons for getting to here: 290 # 1.thumbnail was generated by ffmpeg, not imagemagick convert 291 # 2.thumbnail was cached, so imagemagick convert was not run 292 # Either way, the solution is the same: 293 # => run "identify $thumbnailfile" and parse result 294 295 $thumb_result = `identify \"$thumbnailfile\"`; 296 297 if ($thumb_result =~ m/([0-9]+)x([0-9]+)/) { 298 $doc_obj->add_metadata ($section, "ThumbWidth", $1); 299 $doc_obj->add_metadata ($section, "ThumbHeight", $2); 300 } 301 } 302 } 303 304 305 sub extract_keyframes_montage 306 { 307 my $self = shift (@_); 308 my ($doc_obj,$filename,$thumbnailtype) = @_; 309 310 my $section = $doc_obj->get_top_section(); 311 312 my $output_dir = $self->{'cached_dir'}; 313 my $ivideo_root = $self->{'cached_file_root'}; 314 315 316 # Generate the mosaic with 'montage' 317 my $montagefile = &util::filename_cat($output_dir,"$ivideo_root\_montage.$thumbnailtype"); 318 319 my ($montage_cmd,$omontage_filename) 320 = $self->keyframe_montage_cmd($filename,$montagefile); 321 322 my $montage_options = { 'message_prefix' => "Montage", 323 'message' => "Generating montage" }; 324 325 my ($montage_result,$montage_had_error) 326 = $self->run_general_cmd($montage_cmd,$montage_options); 327 328 # Add the montage as an associated file ... 329 if (-e "$montagefile") { 330 $doc_obj->associate_file("$montagefile", "montage.$thumbnailtype", 331 "image/$thumbnailtype",$section); 332 $doc_obj->add_metadata ($section, "MontageType", $thumbnailtype); 333 $doc_obj->add_metadata ($section, "Montage", "montage.$thumbnailtype"); 334 335 $doc_obj->add_utf8_metadata ($section, "montageicon", "<img src=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[Montage]\" >"); 336 } 337 } 338 339 340 341 sub extract_screenview 342 { 343 my $self = shift (@_); 344 my ($doc_obj,$filename,$screenviewtype, $screenview_width,$screenview_height) = @_; 345 346 my $section = $doc_obj->get_top_section(); 347 348 my $output_dir = $self->{'cached_dir'}; 349 my $ivideo_root = $self->{'cached_file_root'}; 350 351 352 # make the screenview image 353 354 my $screenviewfilename = &util::filename_cat($output_dir,"$ivideo_root.$screenviewtype"); 355 356 357 my ($screenview_cmd,$oscreenview_filename) 358 = $self->keyframe_thumbnail_cmd($filename,$screenviewfilename,$screenview_width,$screenview_height); 359 360 my $screenview_options = { 'message_prefix' => "Screenview", 361 'message' => "Generating screenview image" }; 362 363 my ($result,$had_error) 364 = $self->run_general_cmd($screenview_cmd,$screenview_options); 365 366 # get screenview dimensions, size and type 367 if ($result =~ m/[0-9]+x[0-9]+=>([0-9]+)x([0-9]+)/) { 368 $doc_obj->add_metadata ($section, "ScreenWidth", $1); 369 $doc_obj->add_metadata ($section, "ScreenHeight", $2); 370 } 371 else { 372 $doc_obj->add_metadata ($section, "ScreenWidth", $screenview_width); 373 $doc_obj->add_metadata ($section, "ScreenHeight", $screenview_height); 374 } 375 376 #add the screenview as an associated file ... 377 if (-e "$screenviewfilename") { 378 $doc_obj->associate_file("$screenviewfilename", "screenview.$screenviewtype", 379 "image/$screenviewtype",$section); 380 $doc_obj->add_metadata ($section, "ScreenType", $screenviewtype); 381 $doc_obj->add_metadata ($section, "Screen", "screenview.$screenviewtype"); 382 383 $doc_obj->add_utf8_metadata ($section, "screenicon", "<img src=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[Screen]\" width=[ScreenWidth] height=[ScreenHeight]>"); 384 } else { 385 my $outhandle = $self->{'outhandle'}; 386 print $outhandle "VideoPlugin: couldn't find \"$screenviewfilename\"\n"; 387 } 388 } 258 389 259 390 … … 284 415 my ($video_type, $video_width, $video_height, $video_duration, $video_size, 285 416 $vcodec,$vfps,$atype,$afreq,$achan,$arate) 286 = & videoconvert::identify($filename, $outhandle, $verbosity);417 = &VideoConverter::identify($filename, $outhandle, $verbosity); 287 418 288 419 if ($vfps eq "unknown") { … … 305 436 my $exp_duration = undef; 306 437 307 my $ video_excerpt_duration = $self->{'video_excerpt_duration'};308 309 if ((defined $ video_excerpt_duration) && ($video_excerpt_duration ne "")) {310 $exp_duration = $ video_excerpt_duration;438 my $excerpt_duration = $self->{'excerpt_duration'}; 439 440 if ((defined $excerpt_duration) && ($excerpt_duration ne "")) { 441 $exp_duration = $excerpt_duration; 311 442 my ($hh,$mm,$ss,$ms) = ($exp_duration =~ m/^(\d\d):(\d\d):(\d\d)\.?(\d\d)?/); 312 443 my $excerpt_dur_in_secs = $hh * 3600 + $mm * 60 + $ss; … … 330 461 331 462 my $ascii_only_filenames = $self->{'use_ascii_only_filenames'}; 332 my $videoconvert 333 = new videoconvert($base_dir,$filename,$verbosity,$outhandle,$exp_duration,$ascii_only_filenames); 334 $self->{'videoconvert'} = $videoconvert; 463 $self->init_cache_for_file($filename); 335 464 336 465 my $originalfilename = undef; 337 466 my $type = "unknown"; 338 467 339 my $output_dir = $ videoconvert->{'cached_dir'};340 my $ivideo_root = $ videoconvert->{'file_root'};468 my $output_dir = $self->{'cached_dir'}; 469 my $ivideo_root = $self->{'cached_file_root'}; 341 470 342 471 my $convertto_regenerated = 0; … … 355 484 $filename = &util::filename_cat($output_dir,$file); 356 485 357 my $s_opt = $ videoconvert->optional_frame_scale($converttosize,$video_width,$video_height);486 my $s_opt = $self->optional_frame_scale($converttosize,$video_width,$video_height); 358 487 my $exp_duration = $self->{'exp_duration'}; 359 488 my $t_opt = (defined $exp_duration) ? "-t $exp_duration" : ""; … … 373 502 my $convertto_error; 374 503 375 my $convertto_options = { @{$ videoconvert->{'ffmpeg_monitor'}},504 my $convertto_options = { @{$self->{'ffmpeg_monitor'}}, 376 505 'message_prefix' => "Convert to", 377 506 'message' => "Converting video to $converttotype" }; 378 507 379 508 ($convertto_regenerated,$convertto_result,$convertto_error) 380 = $ videoconvert->run_cached_general_cmd($convertto_command,$filename,$convertto_options);509 = $self->autorun_cached_general_cmd($convertto_command,$filename,$convertto_options); 381 510 382 511 $type = $converttotype; … … 414 543 $file = $file_unicode; 415 544 my $filemeta = $self->filename_to_utf8_metadata($file); 416 my $filemeta_url_safe = $ videoconvert->url_safe($filemeta);545 my $filemeta_url_safe = $self->url_safe($filemeta); 417 546 418 547 $doc_obj->add_utf8_metadata ($section, "Video", $filemeta_url_safe); … … 454 583 455 584 if ($self->{'extractkeyframes'}) { 456 # Generate the keyframes with ffmpeg and hive 457 my ($keyframe_cmd,$okeyframe_filename) = $videoconvert->keyframe_cmd($originalfilename || $filename); 458 459 my $keyframe_options = { @{$videoconvert->{'ffmpeg_monitor'}}, 460 'message_prefix' => "Keyframe", 461 'message' => "Extracting keyframes" }; 462 463 $videoconvert->run_cached_general_cmd($keyframe_cmd,$okeyframe_filename,$keyframe_options); 464 $videoconvert->parse_shot_xml($self); 465 $videoconvert->associate_keyframes($doc_obj,$section,$self); 585 $self->extract_keyframes($doc_obj,$originalfilename,$filename); 466 586 } 467 587 468 588 my $streamable_regenerated = 0; 469 my $optionally_run_general_cmd 470 = ($convertto_regenerated) ? "regenerate_general_cmd" : "run_cached_general_cmd"; 471 472 if ($self->{'enablestreaming'}) { 473 # Generate the Flash FLV format for streaming purposes 474 475 my $streaming_bitrate = $self->{'streamingbitrate'}; 476 my $streaming_size = $self->{'streamingsize'}; 477 478 my $streaming_quality = "high"; 479 480 my ($stream_cmd,$oflash_filename,$oflash_file) 481 = $videoconvert->stream_cmd($originalfilename || $filename, 482 $video_width,$video_height, 483 $streaming_quality, 484 $streaming_bitrate, $streaming_size); 485 486 487 my $streamable_options = { @{$videoconvert->{'ffmpeg_monitor'}}, 488 'message_prefix' => "Stream", 489 'message' => "Generating streamable video: $oflash_file" }; 490 491 my $streamable_result; 492 my $streamable_had_error; 493 ($streamable_regenerated,$streamable_result,$streamable_had_error) 494 = $videoconvert->$optionally_run_general_cmd($stream_cmd,$oflash_filename,$streamable_options); 495 496 if (!$streamable_had_error) { 497 my ($streamseekable_cmd,$ostreamseekable_filename) = $videoconvert->streamseekable_cmd($oflash_filename); 498 499 my $streamseekable_options = { @{$videoconvert->{'flvtool2_monitor'}}, 500 'message_prefix' => "Stream Seekable", 501 'message' => "Reprocessing video stream to be seekable by timeline: $oflash_file" }; 502 503 if ($streamable_regenerated) { 504 $videoconvert->run_general_cmd($streamseekable_cmd,$streamseekable_options); 505 } 506 507 my $streamable_url = $oflash_file; 508 ## $streamable_url =~ s/ /%20/g; 509 my $streamable_url_safe = $videoconvert->url_safe($streamable_url); 510 511 $doc_obj->add_utf8_metadata ($section, "streamablevideo", $streamable_url_safe); 512 $doc_obj->associate_file($oflash_filename,$oflash_file,"video/flash", 513 $section); 514 } 515 516 my $video_width = $doc_obj->get_metadata_element($section,"VideoWidth"); 517 my $video_height = $doc_obj->get_metadata_element($section,"VideoHeight"); 518 519 # 520 # FlowPlayer.swf height+22 pixels 521 # FlowPlayerBlack.swf height+16 pixels 522 # FlowPlayerThermo.swf height+16 pixels 523 # FlowPlayerWhite.swf height+26 pixels 524 my $flashwidth = $video_width; 525 my $flashheight = $video_height + 22; 526 if ($self->{'extractkeyframes'}) { 527 $flashheight += 100; 528 } 529 $doc_obj->add_metadata ($section, "flashwidth", $flashwidth); 530 $doc_obj->add_metadata ($section, "flashheight", $flashheight); 531 532 my $video_server = $ENV{'GEXT_VIDEO_SERVER'}; 533 my $video_prefix = $ENV{'GEXT_VIDEO_PREFIX'}; 534 my $base_url = "$video_server$video_prefix/collect/[collection]/index/assoc/[assocfilepath]/"; 535 my $base_url_safe = $videoconvert->url_safe($base_url); 536 $doc_obj->add_utf8_metadata ($section, "baseurl",$base_url_safe); 537 538 $self->{'oflash_file'} = $oflash_file; 539 $self->{'oflash_filename'} = $oflash_filename; 540 } 541 542 543 # Make the thumbnail image 589 590 if ($self->{'enable_streaming'}) { 591 $streamable_regenerated 592 = $self->enable_streaming($doc_obj,$originalfilename,$filename, 593 $convertto_regenerated, 594 $video_width,$video_height); 595 } 596 597 544 598 my $thumbnailsize = $self->{'thumbnailsize'} || 100; 545 599 my $thumbnailtype = $self->{'thumbnailtype'} || 'jpg'; 546 600 547 my $thumbnailwidth; 548 my $thumbnailheight; 549 550 if ($video_width>$video_height) { 551 my $scale_ratio = $video_height / $video_width; 552 $thumbnailwidth = $thumbnailsize; 553 $thumbnailheight = int($thumbnailsize * $scale_ratio); 554 } 555 else { 556 my $scale_ratio = $video_width / $video_height; 557 $thumbnailwidth = int($thumbnailsize * $scale_ratio); 558 $thumbnailheight = $thumbnailsize; 559 } 560 561 562 my $thumbnailfile = &util::filename_cat($output_dir,"$ivideo_root.$thumbnailtype"); 563 564 565 if ($self->{'extractthumbnail'}) { 566 # Generate the thumbnail with convert, a la ImagePlug 567 my ($thumb_cmd ,$othumb_filename) 568 = $videoconvert->keyframe_thumbnail_cmd($filename,$thumbnailfile,$thumbnailwidth,$thumbnailheight); 569 570 my $thumb_options = { 'verbosity' => $verbosity, 571 'outhandle' => $outhandle, 572 'message_prefix' => "Thumbnail", 573 'message' => "Generating thumbnail" }; 574 575 my ($thumb_regenerated,$thumb_result,$thumb_had_error) 576 = $videoconvert->$optionally_run_general_cmd($thumb_cmd,$thumbnailfile,$thumb_options); 577 578 # Add the thumbnail as an associated file ... 579 if (-e "$thumbnailfile") { 580 $doc_obj->associate_file("$thumbnailfile", "thumbnail.$thumbnailtype", 581 "image/$thumbnailtype",$section); 582 $doc_obj->add_metadata ($section, "ThumbType", $thumbnailtype); 583 $doc_obj->add_metadata ($section, "Thumb", "thumbnail.$thumbnailtype"); 584 585 $doc_obj->add_utf8_metadata ($section, "thumbicon", "<img src=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[Thumb]\" width=[ThumbWidth] height=[ThumbHeight]>"); 586 ### $doc_obj->add_utf8_metadata ($section, "thumbicon", "<img src=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[Thumb]\">"); 587 } 588 589 # Extract Thumnail metadata from convert output 590 if ($thumb_result =~ m/[0-9]+x[0-9]+=>([0-9]+)x([0-9]+)/) { 591 $doc_obj->add_metadata ($section, "ThumbWidth", $1); 592 $doc_obj->add_metadata ($section, "ThumbHeight", $2); 601 602 if ($self->{'create_thumbnail'} eq "true") { 603 604 my $thumbnail_width; 605 my $thumbnail_height; 606 607 if ($video_width>$video_height) { 608 my $scale_ratio = $video_height / $video_width; 609 $thumbnail_width = $thumbnailsize; 610 $thumbnail_height = int($thumbnailsize * $scale_ratio); 593 611 } 594 612 else { 595 # Two reasons for getting to here: 596 # 1.thumbnail was generated by ffmpeg, not imagemagick convert 597 # 2.thumbnail was cached, so imagemagick convert was not run 598 # Either way, the solution is the same: 599 # => run "identify $thumbnailfile" and parse result 600 601 $thumb_result = `identify \"$thumbnailfile\"`; 602 603 if ($thumb_result =~ m/([0-9]+)x([0-9]+)/) { 604 $doc_obj->add_metadata ($section, "ThumbWidth", $1); 605 $doc_obj->add_metadata ($section, "ThumbHeight", $2); 606 } 607 } 613 my $scale_ratio = $video_width / $video_height; 614 $thumbnail_width = int($thumbnailsize * $scale_ratio); 615 $thumbnail_height = $thumbnailsize; 616 } 617 618 619 $self->extract_thumbnail($doc_obj,$filename,$convertto_regenerated, 620 $thumbnailtype, 621 $thumbnail_width,$thumbnail_height); 608 622 } 609 623 610 624 611 625 if ($self->{'extractkeyframes'}) { 612 613 # Generate the mosaic with 'montage' 614 my $montagefile = &util::filename_cat($output_dir,"$ivideo_root\_montage.$thumbnailtype"); 615 616 my ($montage_cmd,$omontage_filename) 617 = $videoconvert->keyframe_montage_cmd($filename,$montagefile); 618 619 my $montage_options = { 'message_prefix' => "Montage", 620 'message' => "Generating montage" }; 621 622 my ($montage_result,$montage_had_error) 623 = $videoconvert->run_general_cmd($montage_cmd,$montage_options); 624 625 # Add the montage as an associated file ... 626 if (-e "$montagefile") { 627 $doc_obj->associate_file("$montagefile", "montage.$thumbnailtype", 628 "image/$thumbnailtype",$section); 629 $doc_obj->add_metadata ($section, "MontageType", $thumbnailtype); 630 $doc_obj->add_metadata ($section, "Montage", "montage.$thumbnailtype"); 631 632 $doc_obj->add_utf8_metadata ($section, "montageicon", "<img src=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[Montage]\" >"); 633 } 634 } 626 $self->extract_keyframes_montage($doc_obj,$filename,$thumbnailtype); 627 } 628 629 my $screenviewsize = $self->{'screenviewsize'}; 630 my $screenviewtype = $self->{'screenviewtype'} || 'jpeg'; 635 631 636 632 # Make a screen-sized version of the picture if requested 637 if ($self->{' extractscreenview'}) {633 if ($self->{'create_screenview'} eq "true") { 638 634 639 635 # To do: if the actual image smaller than the screenview size, 640 636 # we should use the original ! 641 637 642 my $screenviewsize = $self->{'screenviewsize'}; 643 my $screenviewtype = $self->{'screenviewtype'} || 'jpeg'; 644 my $screenviewfilename = &util::filename_cat($output_dir,"$ivideo_root.$screenviewtype"); 645 646 my $screenviewwidth; 647 my $screenviewheight; 638 my $screenview_width; 639 my $screenview_height; 648 640 649 641 if ($video_width>$video_height) { 650 642 my $scale_ratio = $video_height / $video_width; 651 $screenview width = $screenviewsize;652 $screenview height = int($screenviewsize * $scale_ratio);643 $screenview_width = $screenviewsize; 644 $screenview_height = int($screenviewsize * $scale_ratio); 653 645 } 654 646 else { 655 647 my $scale_ratio = $video_width / $video_height; 656 $screenviewwidth = int($screenviewsize * $scale_ratio); 657 $screenviewheight = $screenviewsize; 658 } 659 660 661 # make the screenview image 662 663 my ($screenview_cmd,$oscreenview_filename) 664 = $videoconvert->keyframe_thumbnail_cmd($filename,$screenviewfilename,$screenviewwidth,$screenviewheight); 665 666 my $screenview_options = { 'message_prefix' => "Screenview", 667 'message' => "Generating screenview image" }; 668 669 my ($result,$had_error) 670 = $videoconvert->run_general_cmd($screenview_cmd,$screenview_options); 671 672 # get screenview dimensions, size and type 673 if ($result =~ m/[0-9]+x[0-9]+=>([0-9]+)x([0-9]+)/) { 674 $doc_obj->add_metadata ($section, "ScreenWidth", $1); 675 $doc_obj->add_metadata ($section, "ScreenHeight", $2); 676 } 677 else { 678 $doc_obj->add_metadata ($section, "ScreenWidth", $video_width); 679 $doc_obj->add_metadata ($section, "ScreenHeight", $video_height); 680 } 681 682 #add the screenview as an associated file ... 683 if (-e "$screenviewfilename") { 684 $doc_obj->associate_file("$screenviewfilename", "screenview.$screenviewtype", 685 "image/$screenviewtype",$section); 686 $doc_obj->add_metadata ($section, "ScreenType", $screenviewtype); 687 $doc_obj->add_metadata ($section, "Screen", "screenview.$screenviewtype"); 688 689 $doc_obj->add_utf8_metadata ($section, "screenicon", "<img src=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[Screen]\" width=[ScreenWidth] height=[ScreenHeight]>"); 690 } else { 691 print $outhandle "VideoPlugin: couldn't find \"$screenviewfilename\"\n"; 692 } 648 $screenview_width = int($screenviewsize * $scale_ratio); 649 $screenview_height = $screenviewsize; 650 } 651 652 653 $self->extract_screenview($doc_obj,$filename, 654 $screenviewtype, 655 $screenview_width,$screenview_height); 693 656 } 694 657 … … 703 666 my ($pluginfo, $base_dir, $file, $block_hash, $metadata, $processor, $maxdocs, $total_count, $gli) = @_; 704 667 705 my $outhandle = $self->{'outhandle'}; 706 707 # should we move this to read? What about secondary plugins? 708 print STDERR "<Processing n='$file' p='$self->{'plugin_type'}'>\n" if ($gli); 709 print $outhandle "$self->{'plugin_type'} processing $file\n" 710 if $self->{'verbosity'} > 1; 711 712 my ($filename_full_path, $filename_no_path) = &util::get_full_filenames($base_dir, $file); 713 714 # create a new document 715 my $doc_obj = new doc ($filename_full_path, "indexed_doc", $self->{'file_rename_method'}); 716 717 718 my $top_section = $doc_obj->get_top_section(); 719 720 ### <Video Specific> ### 721 $doc_obj->set_OIDtype ($processor->{'OIDtype'}, $processor->{'OIDmetadata'}); 722 ### </Video Specific> ### 723 724 725 $doc_obj->add_utf8_metadata($top_section, "Plugin", "$self->{'plugin_type'}"); 726 $doc_obj->add_utf8_metadata($top_section, "FileSize", (-s $filename_full_path)); 727 728 # sets the UTF8 filename (Source) for display and sets the url ref to URL encoded version 729 # of the UTF8 filename (SourceFile) for generated files 730 $self->set_Source_metadata($doc_obj, $filename_no_path); 731 732 733 # plugin specific stuff - what args do we need here?? 734 unless (defined ($self->process($pluginfo, $base_dir, $file, $metadata, $doc_obj, $gli))) { 735 print STDERR "<ProcessingError n='$file'>\n" if ($gli); 736 return (-1,undef); 737 } 738 739 # include any metadata passed in from previous plugins 740 # note that this metadata is associated with the top level section 741 my $section = $doc_obj->get_top_section(); 742 # can we merge these two methods?? 743 $self->add_associated_files($doc_obj, $filename_full_path); 744 $self->extra_metadata ($doc_obj, $section, $metadata); 745 $self->auto_extract_metadata($doc_obj); 746 747 # if we haven't found any Title so far, assign one 748 # this was shifted to here from inside read() 749 $self->title_fallback($doc_obj,$section,$filename_no_path); 750 751 $self->add_OID($doc_obj); 752 753 754 ### <Video Specific> ### 668 my ($rv,$doc_obj) = $self->SUPER::read_into_doc_obj(@_); 669 670 if ($rv != 1) { 671 return ($rv,$doc_obj); 672 } 673 755 674 756 675 if (($self->{'enablestreaming'}) && ($self->{'extractkeyframes'})) { 676 my $section = $doc_obj->get_top_section(); 757 677 my $oflash_filename = $self->{'oflash_filename'}; 758 my $videoconvert = $self->{'videoconvert'};759 678 my ($streamkeyframes_cmd,$ostreamkeyframes_filename) 760 = $ videoconvert->streamkeyframes_cmd($oflash_filename,$doc_obj,$section);679 = $self->streamkeyframes_cmd($oflash_filename,$doc_obj,$section); 761 680 762 681 my $verbosity = $self->{'verbosity'}; 763 682 764 my $streamkeyframes_options = { @{$ videoconvert->{'flvtool2_monitor'}},683 my $streamkeyframes_options = { @{$self->{'flvtool2_monitor'}}, 765 684 'message_prefix' => "Stream Keyframes", 766 685 'message' => "Reprocessing video stream to add keyframes on timeline" }; 767 #### this used to be commented out!!! 768 $videoconvert->run_general_cmd($streamkeyframes_cmd,$streamkeyframes_options); 769 } 770 ### </Video Specific> ### 771 772 return (1,$doc_obj); 773 } 774 775 sub add_dummy_text { 776 my $self = shift(@_); 777 my ($doc_obj, $section) = @_; 778 779 # add NoText metadata so we can hide this dummy text in format statements 780 $doc_obj->add_metadata($section, "NoText", "1"); 781 $doc_obj->add_text($section, &gsprintf::lookup_string("{BasePlugin.dummy_text}",1)); 782 783 } 784 785 786 787 788 # do plugin specific processing of doc_obj 789 sub process { 790 my $self = shift (@_); 791 # options?? 792 my ($pluginfo, $base_dir, $file, $metadata, $doc_obj, $gli) = @_; 793 794 my $outhandle = $self->{'outhandle'}; 795 796 my ($filename_full_path, $filename_no_path) = &util::get_full_filenames($base_dir, $file); 797 798 799 if ($self->{'ffmpeg_installed'}) { 800 my $utf8_filename_no_path = $self->filepath_to_utf8($filename_no_path); 801 my $url_encoded_filename = &util::rename_file($utf8_filename_no_path, $self->{'file_rename_method'}); 802 803 804 #run convert to get the thumbnail and extract size and type info 805 my $result = $self->run_convert($base_dir, $filename_full_path, 806 $url_encoded_filename, $doc_obj); 807 808 if (!defined $result) { 809 if ($gli) { 810 print STDERR "<ProcessingError n='$file'>\n"; 811 } 812 print $outhandle "VideoPlugin: couldn't process \"$filename_full_path\"\n"; 813 return (-1,undef); # error during processing 814 } 815 } 816 else { 817 if ($gli) { 818 &gsprintf(STDERR, "<Warning p='VideoPlugin' r='{VideoConverter.noconversionavailable}: {VideoConverter.".$self->{'no_image_conversion_reason'}."}'>"); 819 } 820 # all we do is add the original video file as an associated file, and set up srclink etc 821 my $assoc_file = $doc_obj->get_assocfile_from_sourcefile(); 822 my $section = $doc_obj->get_top_section(); 823 824 $doc_obj->associate_file($filename_full_path, $assoc_file, "", $section); 825 826 $doc_obj->add_metadata ($section, "srclink", "<a href=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[SourceFile]\">"); 827 $doc_obj->add_metadata ($section, "/srclink", "</a>"); 828 $doc_obj->add_metadata ($section, "srcicon", "<img src=\"_httpprefix_/collect/[collection]/index/assoc/[assocfilepath]/[SourceFile]\" width=\"100\">"); 829 830 } 831 #we have no text - adds dummy text and NoText metadata 832 $self->add_dummy_text($doc_obj, $doc_obj->get_top_section()); 833 834 return 1; 835 686 687 $self->run_general_cmd($streamkeyframes_cmd,$streamkeyframes_options); 688 } 689 690 return ($rv,$doc_obj); 836 691 } 837 692 … … 908 763 909 764 910 911 sub Doctype { 765 sub StartDocument {$_[0]->{'PluginObj'}->xml_start_document(@_);} 766 sub XMLDecl {$_[0]->{'PluginObj'}->xml_xmldecl(@_);} 767 sub Entity {$_[0]->{'PluginObj'}->xml_entity(@_);} 768 sub Doctype {$_[0]->{'PluginObj'}->xml_doctype(@_);} 769 sub StartTag {$_[0]->{'PluginObj'}->xml_start_tag(@_);} 770 sub EndTag {$_[0]->{'PluginObj'}->xml_end_tag(@_);} 771 sub Text {$_[0]->{'PluginObj'}->xml_text(@_);} 772 sub PI {$_[0]->{'PluginObj'}->xml_pi(@_);} 773 sub EndDocument {$_[0]->{'PluginObj'}->xml_end_document(@_);} 774 sub Default {$_[0]->{'PluginObj'}->xml_default(@_);} 775 776 777 # This Char function overrides the one in XML::Parser::Stream to overcome a 778 # problem where $expat->{Text} is treated as the return value, slowing 779 # things down significantly in some cases. 780 sub Char { 781 use bytes; # Necessary to prevent encoding issues with XML::Parser 2.31+ 782 $_[0]->{'Text'} .= $_[1]; 783 return undef; 784 } 785 786 sub xml_start_document { 787 my $self = shift(@_); 912 788 my ($expat, $name, $sysid, $pubid, $internal) = @_; 913 789 914 # my $root_tag = $self->{'root_tag'}; 790 } 791 792 # Called for XML declarations 793 sub xml_xmldecl { 794 my $self = shift(@_); 795 my ($expat, $version, $encoding, $standalone) = @_; 796 } 797 798 # Called for XML entities 799 sub xml_entity { 800 my $self = shift(@_); 801 my ($expat, $name, $val, $sysid, $pubid, $ndata) = @_; 802 } 803 804 805 # Called for DOCTYPE declarations - use die to bail out if this doctype 806 # is not meant for this plugin 807 sub xml_doctype { 808 my $self = shift(@_); 809 my ($expat, $name, $sysid, $pubid, $internal) = @_; 810 811 # This test used to be done in xml_start_document 812 # Moved to here as seems more logical 915 813 916 814 if ($name !~ "seg") { 917 die "Root tag $name does not match expected <seg>"; 918 } 919 } 920 921 sub StartTag { 815 die "VideoPlugin: Root tag $name does not match expected <seg>"; 816 } 817 } 818 819 820 sub xml_start_tag { 821 my $self = shift(@_); 922 822 my ($expat, $element) = @_; 923 823 … … 931 831 #$self->{'flowplayer_thumblist'} = "thumbs: \\["; 932 832 933 my $output_dir = $self->{' videoconvert'}->{'cached_dir'};833 my $output_dir = $self->{'cached_dir'}; 934 834 my $cue_filename = &util::filename_cat($output_dir,"on_cue.xml"); 935 835 … … 945 845 my $avg_frame_num = int(($pre_frame_num+$post_frame_num)/2.0)+1; 946 846 947 my $output_dir = $self->{' videoconvert'}->{'cached_dir'};948 my $ivideo_root = $self->{' videoconvert'}->{'file_root'};847 my $output_dir = $self->{'cached_dir'}; 848 my $ivideo_root = $self->{'cached_file_root'}; 949 849 950 850 my $keyframe_index = $self->{'keyframe_index'}; … … 1016 916 } 1017 917 1018 sub EndTag { 918 sub xml_end_tag { 919 my $self = shift(@_); 1019 920 my ($expat, $element) = @_; 1020 921 … … 1032 933 1033 934 1034 sub Text { 1035 my $text = $_; 1036 } 1037 1038 # This Char function overrides the one in XML::Parser::Stream to overcome a 1039 # problem where $expat->{Text} is treated as the return value, slowing 1040 # things down significantly in some cases. 1041 sub Char { 1042 $_[0]->{'Text'} .= $_[1]; 1043 return undef; 1044 } 935 936 937 938 # Called just before start or end tags with accumulated non-markup text in 939 # the $_ variable. 940 sub xml_text { 941 my $self = shift(@_); 942 my ($expat) = @_; 943 } 944 945 # Called for processing instructions. The $_ variable will contain a copy 946 # of the pi. 947 sub xml_pi { 948 my $self = shift(@_); 949 my ($expat, $target, $data) = @_; 950 } 951 952 # Called at the end of the XML document. 953 sub xml_end_document { 954 my $self = shift(@_); 955 my ($expat) = @_; 956 957 $self->close_document(); 958 } 959 960 961 # Called for any characters not handled by the above functions. 962 sub xml_default { 963 my $self = shift(@_); 964 my ($expat, $text) = @_; 965 } 966 1045 967 1046 968 1;
Note:
See TracChangeset
for help on using the changeset viewer.