33
37
class Movie_HelioviewerMovie
40
private $_startTimestamp;
41
private $_startDateString;
42
private $_endTimestamp;
43
private $_endDateString;
59
private $_timestamps = array();
60
private $_frames = array();
49
63
* Prepares the parameters passed in from the api call and makes a movie from them.
51
65
* @return {String} a url to the movie, or the movie will display.
53
public function __construct($layers, $startDateString, $endDateString, $roi, $options)
64
$options = array_replace($defaults, $options);
66
$this->_db = new Database_ImgIndex();
67
$this->_layers = $layers;
69
$this->_directory = $this->_createCacheDirectories($options['uuid'], $options['outputDir']);
71
$this->_startDateString = $startDateString;
72
$this->_endDateString = $endDateString;
74
// Also store as timestamps
75
$this->_startTimestamp = toUnixTimestamp($startDateString);
76
$this->_endTimestamp = toUnixTimestamp($endDateString);
78
// Get timestamps for frames in the key movie layer
79
$this->_timestamps = $this->_getTimeStamps();
81
$this->_numFrames = sizeOf($this->_timestamps);
83
if ($this->_numFrames == 0) {
84
$this->_abort("No images available for the requested time range");
87
$this->_filename = $this->_buildFilename($options['filename']);
89
$this->_frameRate = $this->_determineOptimalFrameRate($options['frameRate']);
91
$this->_setMovieDimensions();
94
$images = $this->_buildMovieFrames($options['watermarkOn']);
67
public function __construct($publicId, $format="mp4")
69
$this->_db = new Database_ImgIndex();
71
$id = alphaID($publicId, true, 5, HV_MOVIE_ID_PASS);
72
$info = $this->_db->getMovieInformation($id, $format);
75
$this->publicId = $publicId;
76
$this->format = $format;
77
$this->reqStartDate = $info['reqStartDate'];
78
$this->reqEndDate = $info['reqEndDate'];
79
$this->startDate = $info['startDate'];
80
$this->endDate = $info['endDate'];
81
$this->status = $info['status'];
82
$this->timestamp = $info['timestamp'];
83
$this->imageScale = (float) $info['imageScale'];
84
$this->frameRate = (float) $info['frameRate'];
85
$this->numFrames = (int) $info['numFrames'];
86
$this->maxFrames = min((int) $info['maxFrames'], HV_MAX_MOVIE_FRAMES);
87
$this->width = (int) $info['width'];
88
$this->height = (int) $info['height'];
89
$this->watermark = (bool) $info['watermark'];
92
$this->_layers = new Helper_HelioviewerLayers($info['dataSourceString']);
95
$this->_roi = Helper_RegionOfInterest::parsePolygonString($info['roi'], $info['imageScale']);
99
* Build the movie frames and movie
101
public function build()
103
// Check to make sure we have not already started processing the movie
104
if ($this->status !== "QUEUED") {
105
throw new Exception("The requested movie is either currently being built or has already been built");
108
$this->_db->markMovieAsProcessing($this->id, $this->format);
111
$this->directory = $this->_buildDir();
113
// If the movie frames have not been built create them
114
if (!file_exists($this->directory . "frames")) {
115
require_once 'src/Image/Composite/HelioviewerMovieFrame.php';
119
$this->_getTimeStamps(); // Get timestamps for frames in the key movie layer
120
$this->_setMovieProperties(); // Sets the actual start and end dates, frame-rate, numFrames and dimensions
121
$this->_buildMovieFrames($this->watermark); // Build movie frames
125
$this->_db->finishedBuildingMovieFrames($this->id, $t2 - $t1); // Update status and log time to build frames
127
$this->filename = $this->_buildFilename();
129
} catch (Exception $e) {
130
$this->_abort("Error encountered during movie frame compilation");
97
$this->_build($images);
101
* Returns an array of the timestamps for the key movie layer
103
* For single layer movies, the number of frames will be either HV_MAX_MOVIE_FRAMES, or the number of
104
* images available for the requested time range. For multi-layer movies, the number of frames included
105
* may be reduced to ensure that the total number of SubFieldImages needed does not exceed HV_MAX_MOVIE_FRAMES
107
private function _getTimeStamps()
109
$layerCounts = array();
111
// Determine the number of images that are available for the request duration for each layer
112
foreach ($this->_layers->toArray() as $layer) {
113
$n = $this->_db->getImageCount($this->_startDateString, $this->_endDateString, $layer['sourceId']);
114
$layerCounts[$layer['sourceId']] = $n;
117
// Choose the maximum number of frames that can be generated without exceeded the server limits defined
118
// by HV_MAX_MOVIE_FRAMES
120
$imagesRemaining = HV_MAX_MOVIE_FRAMES;
121
$layersRemaining = $this->_layers->length();
123
// Sort counts from smallest to largest
126
// Determine number of frames to create
127
foreach($layerCounts as $dataSource => $count) {
128
$numFrames = min($count, ($imagesRemaining / $layersRemaining));
129
$imagesRemaining -= $numFrames;
130
$layersRemaining -= 1;
133
// Number of frames to use
134
$numFrames = floor($numFrames);
136
// Get the entire range of available images between the movie start and end time
137
$entireRange = $this->_db->getImageRange($this->_startDateString, $this->_endDateString, $dataSource);
139
// Sub-sample range so that only $numFrames timestamps are returned
140
$timestamps = array();
141
for ($i = 0; $i < $numFrames; $i++) {
142
$index = round($i * (sizeOf($entireRange) / $numFrames));
143
array_push($timestamps, $entireRange[$index]['date']);
149
* Create directories in cache used to store movies
151
private function _createCacheDirectories ($uuid, $dir)
153
// Regular movie requests use UUIDs and event movies use the event identifiers
156
$uuid = uuid_create(UUID_TYPE_DEFAULT);
158
$dir = HV_CACHE_DIR . "/movies/" . $uuid;
161
if (!file_exists($dir)) {
162
mkdir("$dir/frames", 0777, true);
163
chmod("$dir/frames", 0777);
170
* Determines filename to use for storing movie
137
$this->_encodeMovie();
138
} catch (Exception $e) {
140
$this->_abort("Error encountered during video encoding.", $t4 - $t3);
143
// If all of the queued videos have been created remove frames
144
if ($this->_db->getNumUnfinishedMovies($this->id) === 0) {
150
* Returns information about the completed movie
152
* @return array A list of movie properties and a URL to the finished movie
154
public function getCompletedMovieInformation() {
156
"frameRate" => $this->frameRate,
157
"numFrames" => $this->numFrames,
158
"startDate" => $this->startDate,
159
"status" => $this->status,
160
"endDate" => $this->endDate,
161
"width" => $this->width,
162
"height" => $this->height,
163
"thumbnails" => $this->getPreviewImages(),
164
"url" => $this->getURL()
169
* Returns an array of filepaths to the movie's preview images
171
public function getPreviewImages()
173
$rootURL = str_replace(HV_CACHE_DIR, HV_CACHE_URL, $this->_buildDir());
177
foreach (array("icon", "small", "medium", "large", "full") as $size) {
178
$images[$size] = $rootURL . "preview-$size.png";
185
* Returns the base filepath for movie without any file extension
187
public function getFilepath($highQuality=false)
189
return $this->_buildDir() . $this->_buildFilename($highQuality);
192
public function getDuration()
194
return $this->numFrames / $this->frameRate;
197
public function getURL()
199
return str_replace(HV_CACHE_DIR, HV_CACHE_URL, $this->_buildDir()) .
200
$this->_buildFilename();
204
* Cancels movie request
206
* @param string $msg Error message
208
private function _abort($msg, $procTime=0) {
209
$this->_db->markMovieAsInvalid($this->id, $procTime);
211
throw new Exception("Unable to create movie: " . $msg, 1);
215
* Determines the directory to store the movie in.
217
* @return string Directory
219
private function _buildDir ()
221
$date = str_replace("-", "/", substr($this->timestamp, 0, 10));
222
return sprintf("%s/movies/%s/%s/", HV_CACHE_DIR, $date, $this->publicId);
226
* Determines filename to use for the movie
228
* @param string $extension Extension of the movie format to be created
172
* @return string filename
230
* @return string Movie filename
174
private function _buildFilename($filename) {
179
$start = str_replace(array(":", "-", "T", "Z"), "_", $this->_startDateString);
180
$end = str_replace(array(":", "-", "T", "Z"), "_", $this->_endDateString);
182
return sprintf("%s_%s_%s", $start, $end, $this->_layers->toString());
232
private function _buildFilename($highQuality=false) {
233
$start = str_replace(array(":", "-", " "), "_", $this->startDate);
234
$end = str_replace(array(":", "-", " "), "_", $this->endDate);
236
$suffix = ($highQuality && $this->format == "mp4") ? "-hq" : "";
238
return sprintf("%s_%s_%s%s.%s", $start, $end, $this->_layers->toString(), $suffix, $this->format);
186
242
* Takes in meta and layer information and creates movie frames from them.
244
* TODO: Use middle frame instead last one...
245
* TODO: Create standardized thumbnail sizes (e.g. thumbnail-med.png = 480x320, etc)
188
247
* @param {String} $tmpDir the directory where the frames will be stored
190
249
* @return $images an array of built movie frames
192
private function _buildMovieFrames($watermarkOn)
251
private function _buildMovieFrames($watermark)
194
$movieFrames = array();
198
255
// Movie frame parameters
199
256
$options = array(
201
'interlace' => false,
203
'watermarkOn'=> $watermarkOn,
204
'outputDir' => $this->_directory . "/frames"
257
'database' => $this->_db,
259
'interlace' => false,
260
'watermark' => $watermark
263
// Index of preview frame
264
$previewIndex = floor($this->numFrames / 2);
207
266
// Compile frames
208
267
foreach ($this->_timestamps as $time) {
209
$options = array_merge($options, array(
210
'filename' => "frame" . $frameNum
268
$filepath = sprintf("%s/frames/frame%d.bmp", $this->directory, $frameNum);
214
$screenshot = new Image_Composite_HelioviewerCompositeImage($this->_layers, $time, $this->_roi, $options);
215
$filepath = $screenshot->getFilepath();
271
$screenshot = new Image_Composite_HelioviewerMovieFrame($filepath, $this->_layers, $time, $this->_roi, $options);
273
if ($frameNum == $previewIndex) {
274
$previewImage = $screenshot; // Make a copy of frame to be used for preview images
217
array_push($movieFrames, $filepath);
278
array_push($this->_frames, $filepath);
218
279
} catch (Exception $e) {
219
// Recover if failure occurs on a single frame
280
$this->numFrames--; // Recover if failure occurs on a single frame
224
// TODO 2011/02/14: Verify that this is still necessary
225
// Copy the last frame so that it actually shows up in the movie for the same amount of time
226
// as the rest of the frames.
227
$lastImage = dirname($filepath) . "/frame" . $frameNum . "." . $options['format'];
229
copy($filepath, $lastImage);
230
array_push($movieFrames, $lastImage);
283
$this->_createPreviewImages($previewImage);
287
* Remove movie frames and directory
291
private function _cleanUp()
293
$dir = $this->directory . "frames/";
295
// Clean up movie frame images that are no longer needed
296
if (file_exists($dir)) {
297
foreach (glob("$dir*") as $image) {
305
* Creates preview images of several different sizes
307
private function _createPreviewImages(&$screenshot)
232
309
// Create preview image
233
// TODO: Use middle frame instead last one...
234
$imagickImage = $screenshot->getIMagickImage();
235
$imagickImage->setImageCompression(IMagick::COMPRESSION_LZW);
236
$imagickImage->setImageCompressionQuality(PNG_LOW_COMPRESSION);
237
$imagickImage->setInterlaceScheme(IMagick::INTERLACE_PLANE);
238
$imagickImage->writeImage($this->_directory . "/" . $this->_filename . ".png");
239
$imagickImage->destroy();
310
$preview = $screenshot->getIMagickImage();
311
$preview->setImageCompression(IMagick::COMPRESSION_LZW);
312
$preview->setImageCompressionQuality(PNG_LOW_COMPRESSION);
313
$preview->setInterlaceScheme(IMagick::INTERLACE_PLANE);
315
// Thumbnail sizes to create
317
"large" => array(640, 480),
318
"medium" => array(320, 240),
319
"small" => array(240, 180),
320
"icon" => array(64, 64)
323
foreach ($sizes as $name=>$dimensions) {
324
$thumb = $preview->clone();
325
$thumb->thumbnailImage($dimensions[0], $dimensions[1], true);
327
// Add black border to reach desired preview image sizes
328
$borderWidth = ceil(($dimensions[0] - $thumb->getImageWidth()) / 2);
329
$borderHeight = ceil(($dimensions[1] - $thumb->getImageHeight()) / 2);
331
$thumb->borderImage("black", $borderWidth, $borderHeight);
332
$thumb->cropImage($dimensions[0], $dimensions[1], 0, 0);
334
$thumb->writeImage($this->directory . "/preview-$name.png");
337
$preview->writeImage($this->directory . "/preview-full.png");
267
365
* image to each timestamp for each layer. Then takes all layers belonging to one timestamp and makes a movie frame
268
366
* out of it. When done with all movie frames, phpvideotoolkit is used to compile all the frames into a movie.
270
* @param array $builtImages An array of built movie frames (in the form of HelioviewerCompositeImage objects)
274
private function _build($builtImages)
370
private function _encodeMovie()
276
$this->_frames = $builtImages;
372
require_once 'src/Movie/FFMPEGEncoder.php';
373
date_default_timezone_set('UTC');
375
// Compute movie meta-data
376
$layerString = $this->_layers->toHumanReadableString();
379
if (substr($this->startDate, 0, 9) == substr($this->endDate, 0, 9)) {
380
$dateString = sprintf("%s - %s UTC", $this->startDate, substr($this->endDate, 10));
382
$dateString = sprintf("%s - %s UTC", $this->startDate, $this->endDate);
386
$url = "http://www.helioviewer.org/api/?action=downloadMovie&id={$this->id}&format={$this->format}";
389
$title = sprintf("%s (%s)", $layerString, $dateString);
392
$description = sprintf(
393
"The Sun as seen through %s from %s.",
394
$layerString, str_replace("-", " to ", $dateString)
399
"This movie was produced by Helioviewer.org on %s UTC. The original movie can be found at %s.",
400
date("F j, Y, g:i a"), $url
404
$filename = str_replace("webm", "mp4", $this->filename);
278
406
// Create and FFmpeg encoder instance
279
$ffmpeg = new Movie_FFMPEGEncoder($this->_frameRate);
281
// TODO 11/18/2010: add 'ipod' option to movie requests in place of the 'hqFormat' param
285
$ffmpeg->createIpodVideo($this->_directory, $this->_filename, "mp4", $this->_width, $this->_height);
288
// Create a high-quality H.264 video using an MPEG-4 (mp4) container format
289
$ffmpeg->createVideo($this->_directory, $this->_filename . "-hq", "mp4", $this->_width, $this->_height, "ultrafast", 15);
291
// Create a medium-quality H.264 video using an MPEG-4 (mp4) container format
292
$ffmpeg->createVideo($this->_directory, $this->_filename, "mp4", $this->_width, $this->_height);
294
//Create alternative container format options for medium-quality video (.flv)
295
$ffmpeg->createAlternativeVideoFormat($this->_directory, $this->_filename, "mp4", "flv");
407
$ffmpeg = new Movie_FFMPEGEncoder(
408
$this->directory, $filename, $this->frameRate, $this->width,
409
$this->height, $title, $description, $comment
412
// Keep track of processing time for webm/mp4 encoding
415
// Create H.264 videos if they do not already exist
416
if (!file_exists(realpath($this->directory . $filename))) {
417
$ffmpeg->createVideo();
418
$ffmpeg->createHQVideo();
419
$ffmpeg->createFlashVideo();
423
// Mark movie as completed
424
$this->_db->markMovieAsFinished($this->id, "mp4", $t2 - $t1);
429
//Create a Low-quality webm movie for in-browser use if requested
430
if ($this->format == "webm") {
431
$ffmpeg->setFormat("webm");
432
$ffmpeg->createVideo();
436
// Mark movie as completed
437
$this->_db->markMovieAsFinished($this->id, "webm", $t4 - $t3);
442
* Returns an array of the timestamps for the key movie layer
444
* For single layer movies, the number of frames will be either HV_MAX_MOVIE_FRAMES, or the number of
445
* images available for the requested time range. For multi-layer movies, the number of frames included
446
* may be reduced to ensure that the total number of SubFieldImages needed does not exceed HV_MAX_MOVIE_FRAMES
448
private function _getTimeStamps()
450
$layerCounts = array();
452
// Determine the number of images that are available for the request duration for each layer
453
foreach ($this->_layers->toArray() as $layer) {
454
$n = $this->_db->getImageCount($this->reqStartDate, $this->reqEndDate, $layer['sourceId']);
455
$layerCounts[$layer['sourceId']] = $n;
458
// Choose the maximum number of frames that can be generated without exceeded the server limits defined
459
// by HV_MAX_MOVIE_FRAMES
461
$imagesRemaining = $this->maxFrames;
462
$layersRemaining = $this->_layers->length();
464
// Sort counts from smallest to largest
467
// Determine number of frames to create
468
foreach($layerCounts as $dataSource => $count) {
469
$numFrames = min($count, ($imagesRemaining / $layersRemaining));
470
$imagesRemaining -= $numFrames;
471
$layersRemaining -= 1;
474
// Number of frames to use
475
$numFrames = floor($numFrames);
477
// Get the entire range of available images between the movie start and end time
478
$entireRange = $this->_db->getImageRange($this->reqStartDate, $this->reqEndDate, $dataSource);
480
// Sub-sample range so that only $numFrames timestamps are returned
481
for ($i = 0; $i < $numFrames; $i++) {
482
$index = round($i * (sizeOf($entireRange) / $numFrames));
483
array_push($this->_timestamps, $entireRange[$index]['date']);