2
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
* Movie_HelioviewerMovie Class Definition
7
* http://flowplayer.org/plugins/streaming/pseudostreaming.html#prepare
12
* @package Helioviewer
13
* @author Jeff Stys <jeff.stys@nasa.gov>
14
* @author Keith Hughitt <keith.hughitt@nasa.gov>
15
* @author Jaclyn Beck <jaclyn.r.beck@gmail.com>
16
* @license http://www.mozilla.org/MPL/MPL-1.1.html Mozilla Public License 1.1
17
* @link http://launchpad.net/helioviewer.org
19
require_once HV_ROOT_DIR . '/../lib/alphaID/alphaID.php';
20
require_once HV_ROOT_DIR . '/src/php/Database/ImgIndex.php';
21
require_once HV_ROOT_DIR . '/src/php/Helper/DateTimeConversions.php';
22
require_once HV_ROOT_DIR . '/src/php/Helper/HelioviewerEvents.php';
23
require_once HV_ROOT_DIR . '/src/php/Helper/HelioviewerLayers.php';
24
require_once HV_ROOT_DIR . '/src/php/Helper/RegionOfInterest.php';
25
require_once HV_ROOT_DIR . '/src/php/Helper/Serialize.php';
27
* Represents a static (e.g. mp4/webm) movie generated by Helioviewer
29
* Note: For movies, it is easiest to work with Unix timestamps since that
30
* is what is returned from the database. To get from a javascript
31
* Date object to a Unix timestamp, simply use
32
* "date.getTime() * 1000."
33
* (getTime returns the number of miliseconds)
42
* @package Helioviewer
43
* @author Jeff Stys <jeff.stys@nasa.gov>
44
* @author Keith Hughitt <keith.hughitt@nasa.gov>
45
* @author Jaclyn Beck <jaclyn.r.beck@gmail.com>
46
* @license http://www.mozilla.org/MPL/MPL-1.1.html Mozilla Public License 1.1
47
* @link http://launchpad.net/helioviewer.org
49
class Movie_HelioviewerMovie {
79
private $_timestamps = array();
80
private $_frames = array();
85
private $_cached = false;
88
* Prepares the parameters passed in from the api call and makes a
91
* @return {String} a url to the movie, or the movie will display.
93
public function __construct($publicId, $format='mp4') {
96
$this->_cachedir = 'api/HelioviewerMovie';
97
$this->_filename = urlencode($publicId.'.cache');
98
$this->_filepath = $this->_cachedir.'/'.$this->_filename;
100
if ( HV_DISABLE_CACHE !== true ) {
101
$cache = new Helper_Serialize($this->_cachedir,
102
$this->_filename, 60);
103
$info = $cache->readCache($verifyAge=true);
104
if ( $info !== false ) {
105
$this->_cached = true;
109
if ( $this->_cached !== true ) {
110
$info = $this->_loadFromDB($publicId, $format);
111
if ( HV_DISABLE_CACHE !== true ) {
112
if ( $cache->writeCache($info) ) {
113
$this->_cached = true;
118
$this->publicId = $publicId;
119
$this->format = $format;
120
$this->reqStartDate = $info['reqStartDate'];
121
$this->reqEndDate = $info['reqEndDate'];
122
$this->startDate = $info['startDate'];
123
$this->endDate = $info['endDate'];
124
$this->timestamp = $info['timestamp'];
125
$this->modified = $info['modified'];
126
$this->imageScale = (float)$info['imageScale'];
127
$this->frameRate = (float)$info['frameRate'];
128
$this->movieLength = (float)$info['movieLength'];
129
$this->id = (int)alphaID($publicId,true,5,HV_MOVIE_ID_PASS);
130
$this->status = (int)$info['status'];
131
$this->numFrames = (int)$info['numFrames'];
132
$this->width = (int)$info['width'];
133
$this->height = (int)$info['height'];
134
$this->watermark = (bool)$info['watermark'];
135
$this->eventsLabels = (bool)$info['eventsLabels'];
136
$this->scale = (bool)$info['scale'];
137
$this->scaleType = $info['scaleType'];
138
$this->scaleX = (float)$info['scaleX'];
139
$this->scaleY = (float)$info['scaleY'];
140
$this->maxFrames = min((int)$info['maxFrames'],HV_MAX_MOVIE_FRAMES);
143
$this->_layers = new Helper_HelioviewerLayers(
144
$info['dataSourceString']);
145
$this->_events = new Helper_HelioviewerEvents(
146
$info['eventSourceString']);
149
$this->_roi = Helper_RegionOfInterest::parsePolygonString(
150
$info['roi'], $info['imageScale']);
153
private function _dbSetup() {
154
if ( $this->_db === false ) {
155
$this->_db = new Database_ImgIndex();
160
private function _loadFromDB($publicId, $format) {
163
$id = alphaID($publicId, true, 5, HV_MOVIE_ID_PASS);
164
$info = $this->_db->getMovieInformation($id);
166
if ( is_null($info) ) {
167
throw new Exception('Unable to find the requested movie: '.$id,24);
175
* Build the movie frames and movie
177
public function build() {
180
date_default_timezone_set('UTC');
182
if ( $this->status == 2 ) {
186
$this->_db->markMovieAsProcessing($this->id, 'mp4');
189
$this->directory = $this->_buildDir();
191
// If the movie frames have not been built create them
192
if ( !@file_exists($this->directory.'frames') ) {
193
require_once HV_ROOT_DIR .
194
'/src/php/Image/Composite/HelioviewerMovieFrame.php';
196
$t1 = date('Y-m-d H:i:s');
198
// Get timestamps for frames in the key movie layer
199
$this->_getTimeStamps();
201
// Set the actual start and end dates, frame-rate,
202
// movie length, numFrames and dimensions
203
$this->_setMovieProperties();
205
// Build movie frames
206
$this->_buildMovieFrames($this->watermark);
208
$t2 = date('Y-m-d H:i:s');
210
// Update status and log time to build frames
211
$this->_db->finishedBuildingMovieFrames($this->id, $t1, $t2);
214
$this->filename = $this->_buildFilename();
217
catch (Exception $e) {
218
$this->_abort('Error encountered during movie frame compilation: '
219
. $e->getMessage() );
226
$this->_encodeMovie();
228
catch (Exception $e) {
230
$this->_abort('Error encountered during video encoding. ' .
231
'This may be caused by an FFmpeg configuration issue, ' .
232
'or by insufficient permissions in the cache.', $t4 - $t3);
235
// Log buildMovie in statistics table
236
if ( HV_ENABLE_STATISTICS_COLLECTION ) {
237
include_once HV_ROOT_DIR.'/src/php/Database/Statistics.php';
239
$statistics = new Database_Statistics();
240
$statistics->log('buildMovie');
247
* Returns information about the completed movie
249
* @return array A list of movie properties and a URL to the finished movie
251
public function getCompletedMovieInformation($verbose=false) {
254
'frameRate' => $this->frameRate,
255
'numFrames' => $this->numFrames,
256
'startDate' => $this->startDate,
257
'status' => $this->status,
258
'endDate' => $this->endDate,
259
'width' => $this->width,
260
'height' => $this->height,
261
'title' => $this->getTitle(),
262
'thumbnails' => $this->getPreviewImages(),
263
'url' => $this->getURL()
268
'timestamp' => $this->timestamp,
269
'duration' => $this->getDuration(),
270
'imageScale' => $this->imageScale,
271
'layers' => $this->_layers->serialize(),
272
'events' => $this->_events->serialize(),
273
'x1' => $this->_roi->left(),
274
'y1' => $this->_roi->top(),
275
'x2' => $this->_roi->right(),
276
'y2' => $this->_roi->bottom()
278
$info = array_merge($info, $extra);
285
* Returns an array of filepaths to the movie's preview images
287
public function getPreviewImages() {
288
$rootURL = str_replace(HV_CACHE_DIR, HV_CACHE_URL, $this->_buildDir());
292
foreach ( array('icon', 'small', 'medium', 'large', 'full') as $size) {
293
$images[$size] = $rootURL.'preview-'.$size.'.png';
300
* Returns the base filepath for movie without any file extension
302
public function getFilepath($highQuality=false) {
303
return $this->_buildDir().$this->_buildFilename($highQuality);
306
public function getDuration() {
307
return $this->numFrames / $this->frameRate;
310
public function getURL() {
311
return str_replace(HV_CACHE_DIR, HV_CACHE_URL, $this->_buildDir()) .
312
$this->_buildFilename();
315
public function getCurrentFrame() {
316
$dir = dirname($this->getFilepath()) . '/frames/';
317
$pattern = '/^frame([0-9]{1,4})\.bmp$/';
318
$newest_timestamp = 0;
322
if ($handle = @opendir($dir)) {
323
while ( ($fname = readdir($handle)) !== false ) {
324
// Skip current and parent directories ('.', '..')
325
if ( preg_match('/^\.{1,2}$/', $fname) ) {
329
// Skip non-matching file patterns
330
if ( !preg_match($pattern, $fname, $matches) ) {
334
$timedat = @filemtime($dir.'/'.$fname);
336
if ($timedat > $newest_timestamp) {
337
$newest_timestamp = $timedat;
338
$newest_file = $fname;
339
$newest_frame = $matches[1];
345
return ($newest_frame>0) ? (int)$newest_frame : null;
349
* Cancels movie request
351
* @param string $msg Error message
353
private function _abort($msg, $procTime=0) {
355
$this->_db->markMovieAsInvalid($this->id, $procTime);
358
throw new Exception('Unable to create movie: '.$msg);
362
* Determines the directory to store the movie in.
364
* @return string Directory
366
private function _buildDir() {
367
$date = str_replace('-', '/', substr($this->timestamp, 0, 10));
369
return sprintf('%s/movies/%s/%s/', HV_CACHE_DIR, $date,
374
* Determines filename to use for the movie
376
* @param string $extension Extension of the movie format to be created
378
* @return string Movie filename
380
private function _buildFilename($highQuality=false) {
381
$start = str_replace(array(':', '-', ' '), '_', $this->startDate);
382
$end = str_replace(array(':', '-', ' '), '_', $this->endDate);
384
$suffix = ($highQuality && $this->format == 'mp4') ? '-hq' : '';
386
return sprintf("%s_%s_%s%s.%s", $start, $end,
387
$this->_layers->toString(), $suffix, $this->format);
391
* Takes in meta and layer information and creates movie frames from them.
393
* TODO: Use middle frame instead last one...
394
* TODO: Create standardized thumbnail sizes
395
* (e.g. thumbnail-med.png = 480x320, etc)
397
* @return $images an array of built movie frames
399
private function _buildMovieFrames($watermark) {
404
// Movie frame parameters
406
'database' => $this->_db,
408
'interlace' => false,
409
'watermark' => $watermark
412
// Index of preview frame
413
$previewIndex = floor($this->numFrames/2);
415
// Add tolerance for single-frame failures
419
foreach ($this->_timestamps as $time) {
421
$filepath = sprintf('%sframes/frame%d.bmp', $this->directory,
425
$screenshot = new Image_Composite_HelioviewerMovieFrame(
426
$filepath, $this->_layers, $this->_events,
427
$this->eventsLabels, $this->scale, $this->scaleType,
428
$this->scaleX, $this->scaleY, $time, $this->_roi,
431
if ( $frameNum == $previewIndex ) {
432
// Make a copy of frame to be used for preview images
433
$previewImage = $screenshot;
437
array_push($this->_frames, $filepath);
439
catch (Exception $e) {
442
if ($numFailures <= 3) {
443
// Recover if failure occurs on a single frame
447
// Otherwise proprogate exception to be logged
453
$this->_createPreviewImages($previewImage);
457
* Remove movie frames and directory
461
private function _cleanUp() {
462
$dir = $this->directory.'frames/';
464
// Clean up movie frame images that are no longer needed
465
if ( @file_exists($dir) ) {
466
foreach (glob("$dir*") as $image) {
474
* Creates preview images of several different sizes
476
private function _createPreviewImages(&$screenshot) {
478
// Create preview image
479
$preview = $screenshot->getIMagickImage();
480
$preview->setImageCompression(IMagick::COMPRESSION_LZW);
481
$preview->setImageCompressionQuality(PNG_LOW_COMPRESSION);
482
$preview->setInterlaceScheme(IMagick::INTERLACE_PLANE);
484
// Thumbnail sizes to create
486
'large' => array(640, 480),
487
'medium' => array(320, 240),
488
'small' => array(240, 180),
489
'icon' => array( 64, 64)
492
foreach ($sizes as $name=>$dimensions) {
493
$thumb = clone $preview;
494
$thumb->thumbnailImage($dimensions[0], $dimensions[1], true);
496
// Add black border to reach desired preview image sizes
497
$borderWidth = ceil(($dimensions[0]-$thumb->getImageWidth()) /2);
498
$borderHeight = ceil(($dimensions[1]-$thumb->getImageHeight())/2);
500
$thumb->borderImage('black', $borderWidth, $borderHeight);
501
$thumb->cropImage($dimensions[0], $dimensions[1], 0, 0);
503
$thumb->writeImage($this->directory.'preview-'.$name.'.png');
507
$preview->writeImage($this->directory.'preview-full.png');
512
* Builds the requested movie
514
* Makes a temporary directory to store frames in, calculates a timestamp
515
* for every frame, gets the closest image to each timestamp for each
516
* layer. Then takes all layers belonging to one timestamp and makes a
517
* movie frame out of it. When done with all movie frames, phpvideotoolkit
518
* is used to compile all the frames into a movie.
522
private function _encodeMovie() {
524
require_once HV_ROOT_DIR.'/src/php/Movie/FFMPEGEncoder.php';
526
// Compute movie meta-data
527
$layerString = $this->_layers->toHumanReadableString();
530
$dateString = $this->getDateString();
533
$url1 = HV_WEB_ROOT_URL . '/?action=playMovie&id='
534
. $this->publicId.'&format='.$this->format
536
$url2 = HV_WEB_ROOT_URL . '/?action=downloadMovie&id='
537
. $this->publicId.'&format='.$this->format
541
$title = sprintf('%s (%s)', $layerString, $dateString);
544
$description = sprintf(
545
'The Sun as seen through %s from %s.',
546
$layerString, str_replace('-', ' to ', $dateString)
551
'This movie was produced by Helioviewer.org. See the original ' .
552
'at %s or download a high-quality version from %s.', $url1, $url2
556
$filename = str_replace('webm', 'mp4', $this->filename);
558
// Limit frame-rate floating-point precision
559
// https://bugs.launchpad.net/helioviewer.org/+bug/979231
560
$frameRate = round($this->frameRate, 1);
562
// Create and FFmpeg encoder instance
563
$ffmpeg = new Movie_FFMPEGEncoder(
564
$this->directory, $filename, $frameRate, $this->width,
565
$this->height, $title, $description, $comment
571
// Create H.264 videos
573
$ffmpeg->setFormat('mp4');
574
$ffmpeg->createVideo();
575
$ffmpeg->createHQVideo();
576
$ffmpeg->createFlashVideo();
578
// Mark mp4 movie as completed
580
$this->_db->markMovieAsFinished($this->id, 'mp4', $t2 - $t1);
583
// Create a low-quality webm movie for in-browser use if requested
585
$ffmpeg->setFormat('webm');
586
$ffmpeg->createVideo();
588
// Mark movie as completed
590
$this->_db->markMovieAsFinished($this->id, 'webm', $t4 - $t3);
594
* Returns a human-readable title for the video
596
public function getTitle() {
597
date_default_timezone_set('UTC');
599
$layerString = $this->_layers->toHumanReadableString();
600
$dateString = $this->getDateString();
602
return sprintf('%s (%s)', $layerString, $dateString);
606
* Returns a human-readable date string
608
public function getDateString() {
609
date_default_timezone_set('UTC');
611
if (substr($this->startDate, 0, 9) == substr($this->endDate, 0, 9)) {
612
$endDate = substr($this->endDate, 11);
615
$endDate = $this->endDate;
618
return sprintf('%s - %s UTC', $this->startDate, $endDate);
622
* Returns an array of the timestamps for the key movie layer
624
* For single layer movies, the number of frames will be either
625
* HV_MAX_MOVIE_FRAMES, or the number of images available for the
626
* requested time range. For multi-layer movies, the number of frames
627
* included may be reduced to ensure that the total number of
628
* SubFieldImages needed does not exceed HV_MAX_MOVIE_FRAMES
630
private function _getTimeStamps() {
633
$layerCounts = array();
635
// Determine the number of images that are available for the request
636
// duration for each layer
637
foreach ($this->_layers->toArray() as $layer) {
638
$n = $this->_db->getDataCount($this->reqStartDate,
639
$this->reqEndDate, $layer['sourceId']);
641
$layerCounts[$layer['sourceId']] = $n;
644
// Choose the maximum number of frames that can be generated without
645
// exceeded the server limits defined by HV_MAX_MOVIE_FRAMES
647
$imagesRemaining = $this->maxFrames;
648
$layersRemaining = $this->_layers->length();
650
// Sort counts from smallest to largest
653
// Determine number of frames to create
654
foreach($layerCounts as $dataSource => $count) {
655
$numFrames = min($count, ($imagesRemaining / $layersRemaining));
656
$imagesRemaining -= $numFrames;
657
$layersRemaining -= 1;
660
// Number of frames to use
661
$numFrames = floor($numFrames);
663
// Get the entire range of available images between the movie start
665
$entireRange = $this->_db->getDataRange($this->reqStartDate,
666
$this->reqEndDate, $dataSource);
668
// Sub-sample range so that only $numFrames timestamps are returned
669
for ($i = 0; $i < $numFrames; $i++) {
670
$index = round($i * (sizeOf($entireRange) / $numFrames));
671
array_push($this->_timestamps, $entireRange[$index]['date']);
676
* Determines dimensions to use for movie and stores them
680
private function _setMovieDimensions() {
681
$this->width = round($this->_roi->getPixelWidth());
682
$this->height = round($this->_roi->getPixelHeight());
684
// Width and height must be divisible by 2 or ffmpeg will throw
686
if ($this->width % 2 === 1) {
690
if ($this->height % 2 === 1) {
696
* Determines some of the movie details and saves them to the database
699
private function _setMovieProperties() {
702
// Store actual start and end dates that will be used for the movie
703
$this->startDate = $this->_timestamps[0];
704
$this->endDate = $this->_timestamps[sizeOf($this->_timestamps) - 1];
706
$this->filename = $this->_buildFilename();
708
$this->numFrames = sizeOf($this->_timestamps);
710
if ($this->numFrames == 0) {
711
$this->_abort('No images available for the requested time range');
714
if ($this->frameRate) {
715
$this->movieLength = $this->numFrames / $this->frameRate;
718
$this->frameRate = min(30, max(1,
719
$this->numFrames / $this->movieLength) );
722
$this->_setMovieDimensions();
724
// Update movie entry in database with new details
725
$this->_db->storeMovieProperties(
726
$this->id, $this->startDate, $this->endDate, $this->numFrames,
727
$this->frameRate, $this->movieLength, $this->width, $this->height
732
* Adds black border to movie frames if neccessary to guarantee a 16:9
735
* Checks the ratio of width to height and adjusts each dimension so that
736
* the ratio is 16:9. The movie will be padded with a black background in
737
* JP2Image.php using the new width and height.
739
* @return array Width and Height of padded movie frames
741
private function _setAspectRatios() {
742
$width = $this->_roi->getPixelWidth();
743
$height = $this->_roi->getPixelHeight();
745
$ratio = $width / $height;
747
// Adjust height if necessary
748
if ( $ratio > 16/9 ) {
749
$adjust = (9/16) * $width / $height;
753
$dimensions = array('width' => $width,
754
'height' => $height);
760
* Returns HTML for a video player with the requested movie loaded
762
public function getMoviePlayerHTML() {
764
$filepath = str_replace(HV_ROOT_DIR, '../', $this->getFilepath());
765
$css = "width: {$this->width}px; height: {$this->height}px;";
766
$duration = $this->numFrames / $this->frameRate;
771
<title>Helioviewer.org - <?php echo $this -> filename; ?></title>
772
<script type="text/javascript" src="http://html5.kaltura.org/js"></script>
773
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js" type="text/javascript"></script>
776
<div style="text-align: center;">
777
<div style="margin-left: auto; margin-right: auto; <?php echo $css; ?>";>
778
<video style="margin-left: auto; margin-right: auto;" poster="<?php echo "$filepath.bmp"?>" durationHint="<?php echo $duration; ?>">
779
<source src="<?php echo $filepath.'.mp4' ?>" />
780
<source src="<?php echo $filepath.'.webm' ?>" />
781
<source src="<?php echo $filepath.'.flv' ?>" />