Issue #1827606 by Jorrit: Added option to use private S3 files to the Advanced Zencod...
[project/video.git] / transcoders / TranscoderAbstractionFactoryZencoder.inc
1 <?php
2 /**
3 * @file
4 * File containing class TranscoderAbstractionFactoryZencoder
5 */
6
7 /**
8 * Class that handles Zencoder transcoding.
9 */
10 class TranscoderAbstractionFactoryZencoder extends TranscoderAbstractionFactory implements TranscoderFactoryInterface {
11 protected $options = array();
12 private $postbackurl;
13 private $outputdestination;
14
15 public function __construct() {
16 parent::__construct();
17 $this->options['api_key'] = variable_get('video_zencoder_api_key');
18 $this->postbackurl = variable_get('video_zencoder_postback', url('postback/jobs', array('absolute' => TRUE)));
19 $this->outputdestination = variable_get('video_zencoder_output_destination');
20 }
21
22 public function setInput(array $file) {
23 parent::setInput($file);
24 $this->options['input'] = file_create_url($this->settings['input']['uri']);
25
26 if (variable_get('video_zencoder_testing_mode', FALSE)) {
27 $this->options['input'] = variable_get('video_zencoder_test_file_path', 'http://example.com/video.mp4');
28 }
29 }
30
31 public function setOptions(array $options) {
32 foreach ($options as $key => $value) {
33 if (empty($value) || $value === 'none') {
34 continue;
35 }
36
37 switch ($key) {
38 case 'pixel_format':
39 case 'video_preset':
40 case 'default':
41 break;
42 case 'video_extension':
43 $this->options['output']['format'] = $value;
44 break;
45 case 'wxh':
46 $this->options['output']['size'] = $value;
47 break;
48 case 'video_quality':
49 $this->options['output']['quality'] = intval($value);
50 break;
51 case 'video_speed':
52 $this->options['output']['speed'] = intval($value);
53 break;
54 case 'video_upscale':
55 $this->options['output']['upscale'] = $value;
56 break;
57 case 'one_pass':
58 $this->options['output']['one_pass'] = $value == 1;
59 break;
60 case 'video_aspectmode':
61 $this->options['output']['aspect_mode'] = $value;
62 break;
63 case 'bitrate_cap':
64 $this->options['output']['decoder_bitrate_cap'] = intval($value);
65 break;
66 case 'buffer_size':
67 $this->options['output']['decoder_buffer_size'] = intval($value);
68 break;
69 default:
70 if (strncmp('video_watermark_', $key, 16) === 0) {
71 break;
72 }
73 $this->options['output'][$key] = $value;
74 break;
75 }
76 }
77
78 // set notifications
79 $this->options['output']['notifications']['format'] = 'json';
80 $this->options['output']['notifications']['url'] = $this->postbackurl;
81
82 // thumbnails
83 if ($this->options['output']['thumbnails']['number'] > 0) {
84 $this->options['output']['thumbnails'] = array(
85 'format' => $this->options['output']['thumbnails']['format'],
86 'number' => $this->options['output']['thumbnails']['number'],
87 'size' => variable_get('video_thumbnail_size', '320x240'),
88 'prefix' => 'thumbnail-' . $this->settings['input']['fid'],
89 );
90 }
91 else {
92 unset($this->options['output']['thumbnails']);
93 }
94
95 // watermark
96 if (!empty($options['video_watermark_enabled']) && !empty($options['video_watermark_fid'])) {
97 $file = file_load($options['video_watermark_fid']);
98 $audioonly = !empty($options['video_watermark_onlyforaudio']);
99 $isaudio = strncmp($this->settings['input']['filemime'], 'audio/', 6) === 0;
100
101 if (!empty($file) && (!$audioonly || $isaudio)) {
102 $wm = array('url' => file_create_url($file->uri));
103 if (isset($options['video_watermark_y']) && $options['video_watermark_y'] !== '') {
104 $wm['y'] = $options['video_watermark_y'];
105 }
106 if (isset($options['video_watermark_x']) && $options['video_watermark_x'] !== '') {
107 $wm['x'] = $options['video_watermark_x'];
108 }
109 if (isset($options['video_watermark_height']) && $options['video_watermark_height'] !== '') {
110 $wm['height'] = $options['video_watermark_height'];
111 }
112 if (isset($options['video_watermark_width']) && $options['video_watermark_width'] !== '') {
113 $wm['width'] = $options['video_watermark_width'];
114 }
115 $this->options['output']['watermarks'] = array($wm);
116 }
117 }
118
119 return TRUE;
120 }
121
122 public function setOutput($output_directory, $output_name, $overwrite_mode = FILE_EXISTS_REPLACE) {
123 parent::setOutput($output_directory, $output_name, $overwrite_mode);
124 $this->options['output']['label'] = 'video-' . $this->settings['input']['fid'];
125 $this->options['output']['filename'] = $this->settings['filename'];
126 $this->options['output']['public'] = !variable_get('video_zencoder_private', FALSE);
127 $baseurl = NULL;
128
129 if ($this->outputdestination == 's3') {
130 $bucket = variable_get('amazons3_bucket');
131 // For now, silently ignore the "Use Amazon S3 module" setting when the bucket is not found
132 if ($bucket !== NULL) {
133 $baseurl = 's3://' . $bucket . '/';
134 }
135 }
136 elseif ($this->outputdestination == 'rcf') {
137 $username = variable_get('rackspace_cloud_username');
138 $key = variable_get('rackspace_cloud_api_key');
139 $container = variable_get('rackspace_cloud_container');
140 $authurl = variable_get('rackspace_cloud_auth_url');
141 // For now, silently ignore the "Use Rackspace Cloud Files module" setting when the cloudfiles module isn't setup
142 if ($username !== NULL && $key !== NULL && $container !== NULL && $authurl !== NULL) {
143 $scheme = $authurl == 'https://lon.auth.api.rackspacecloud.com' ? 'cf+uk' : 'cf';
144 $baseurl = $scheme . '://' . rawurlencode($username) . ':' . rawurlencode($key) . '@' . $container . '/';
145 }
146 }
147
148 if ($baseurl != NULL) {
149 $this->options['output']['base_url'] = $baseurl . file_uri_target($output_directory) . '/';
150
151 if (isset($this->options['output']['thumbnails'])) {
152 $this->options['output']['thumbnails']['base_url'] = $baseurl . variable_get('video_thumbnail_path', 'videos/thumbnails') . '/' . $this->settings['input']['fid'] . '/';
153 }
154 }
155 }
156
157 /**
158 * For new videos, this function is never called, because all thumbnails are
159 * extracted and saved to the databases during the post back handler in
160 * TranscoderAbstractionFactoryZencoder::processPostback().
161 */
162 public function extractFrames($destinationScheme, $format) {
163 // Check if the job has been completed.
164 // If the job has not been completed, don't bother checking for
165 // thumbnails
166 $fid = $this->settings['input']['fid'];
167 $job = video_jobs::load($fid);
168 if (empty($job)) {
169 return array();
170 }
171 // No thumbnails available yet
172 if ($job->video_status != VIDEO_RENDERING_COMPLETE) {
173 return array();
174 }
175
176 $path = variable_get('video_thumbnail_path', 'videos/thumbnails') . '/' . $fid;
177
178 // Get the file system directory.
179 $dsturibase = $destinationScheme . '://' . $path . '/';
180 file_prepare_directory($dsturibase, FILE_CREATE_DIRECTORY);
181 $dstwrapper = file_stream_wrapper_get_instance_by_scheme($destinationScheme);
182
183 // Find the old base url setting. If it is not present, don't check for legacy thumbnails
184 $base_url = variable_get('video_zencoder_base_url');
185 if (empty($base_url)) {
186 return array();
187 }
188
189 // Where to copy the thumbnails from.
190 $final_path = variable_get('video_zencoder_use_full_path', FALSE) ? drupal_realpath(file_uri_scheme($this->settings['input']['uri']) . '://' . $path) : '/' . $path;
191 $srcuribase = variable_get('video_zencoder_base_url') . $final_path . '/';
192
193 $thumbs = array();
194 // Total thumbs to generate
195 $no_of_thumbnails = variable_get('video_thumbnail_count', 5);
196 for ($i = 0; $i < $no_of_thumbnails; $i++) {
197 $filename = file_munge_filename('thumbnail-' . $fid . '_' . sprintf('%04d', $i) . '.png', '', TRUE);
198 $dsturi = $dsturibase . $filename;
199
200 // Download file from S3, if available
201 if (!file_exists($dsturi)) {
202 $srcuri = $srcuribase . $filename;
203 if (!file_exists($srcuri)) {
204 watchdog('zencoder',
205 'Error downloading thumbnail for video %filename: %thumbpath does not exist.',
206 array('%filename' => $this->settings['input']['filename'], '%thumbpath' => $srcuri),
207 WATCHDOG_ERROR);
208 break;
209 }
210
211 $this->moveFile($srcuri, $dsturi);
212
213 // Delete the source, it is no longer needed
214 drupal_unlink($srcuri);
215 }
216
217 $thumb = new stdClass();
218 $thumb->status = 0;
219 $thumb->filename = $filename;
220 $thumb->uri = $dsturi;
221 $thumb->filemime = $dstwrapper->getMimeType($dsturi);
222 $thumbs[] = $thumb;
223 }
224
225 return !empty($thumbs) ? $thumbs : FALSE;
226 }
227
228 public function execute() {
229 libraries_load('zencoder');
230 $zencoder = new Services_Zencoder();
231
232 try {
233 $encoding_job = $zencoder->jobs->create($this->options);
234
235 $output = new stdClass();
236 $output->filename = $this->settings['filename'];
237 $output->uri = $this->settings['base_url'] . '/' . $this->settings['filename'];
238 $output->filesize = 0;
239 $output->timestamp = time();
240 $output->jobid = intval($encoding_job->id);
241 $output->duration = 0;
242
243 return $output;
244 }
245 catch (Services_Zencoder_Exception $e) {
246 $errors = $e->getErrors();
247 $this->errors['execute'] = $errors;
248 watchdog('zencoder', 'Zencoder reports errors while converting %file:<br/>!errorlist', array('%file' => $this->settings['filename'], '!errorlist' => theme('item_list', array('items' => $errors))), WATCHDOG_ERROR);
249 return FALSE;
250 }
251 }
252
253 public function getName() {
254 return 'Zencoder';
255 }
256
257 public function getValue() {
258 return 'TranscoderAbstractionFactoryZencoder';
259 }
260
261 public function isAvailable(&$errormsg) {
262 registry_rebuild();
263
264 if (!module_exists('zencoderapi')) {
265 $errormsg = t('You must install and enable the Zencoder API module to use Zencoder to transcode videos.');
266 return FALSE;
267 }
268 elseif (!class_exists('Services_Zencoder')) {
269 $errormsg = t('The Zencoder API module has not been setup properly.');
270 return FALSE;
271 }
272
273 return TRUE;
274 }
275
276 public function getVersion() {
277 return '1.2';
278 }
279
280 public function adminSettings() {
281 $t = get_t();
282
283 $form = array();
284 $zencoder_api = variable_get('video_zencoder_api_key', NULL);
285 if (empty($zencoder_api)) {
286 $form['zencoder_user'] = array(
287 '#type' => 'fieldset',
288 '#title' => $t('Zencoder setup'),
289 '#collapsible' => FALSE,
290 '#collapsed' => FALSE,
291 '#description' => $t('Add your email address, password and <em>save configurations</em> to create your Zencoder account. It will help you to transcode and manage your videos using Zencode website. Once you save your configurations then this will automatically create an account on the Zencoder.com and password and all ther other relevent details will be emailed to you.', array('!link' => l($t('Zencoder.com'), 'http://zencoder.com'))),
292 '#states' => array(
293 'visible' => array(
294 ':input[name=video_convertor]' => array('value' => 'TranscoderAbstractionFactoryZencoder'),
295 ),
296 ),
297 );
298 $form['zencoder_user']['zencoder_username'] = array(
299 '#type' => 'textfield',
300 '#title' => $t('Your email address'),
301 '#default_value' => variable_get('site_mail', 'me@localhost'),
302 '#size' => 50,
303 '#description' => $t('Make sure the email is accurate, since we will send all the password details to manage transcoding online and API key details to this.')
304 );
305 $form['zencoder_user']['agree_terms_zencoder'] = array(
306 '#type' => 'checkbox',
307 '#title' => $t('Agree Zencoder !link.', array('!link' => l($t('Terms and Conditions'), 'http://zencoder.com/terms', array('attributes' => array('target' => '_blank'))))),
308 '#default_value' => variable_get('agree_terms_zencoder', TRUE),
309 );
310 }
311 else {
312 // Zencoder API is exists
313 $form['zencoder_info'] = array(
314 '#type' => 'fieldset',
315 '#title' => t('Zencoder'),
316 '#collapsible' => FALSE,
317 '#collapsed' => FALSE,
318 '#states' => array(
319 'visible' => array(
320 ':input[name=video_convertor]' => array('value' => 'TranscoderAbstractionFactoryZencoder'),
321 ),
322 ),
323 );
324 $form['zencoder_info']['video_zencoder_api_key'] = array(
325 '#type' => 'textfield',
326 '#title' => t('Zencoder API key'),
327 '#default_value' => variable_get('video_zencoder_api_key', NULL),
328 '#description' => t('Zencoder API Key. Click <b>Reset to default</b> button to add new account.')
329 );
330 $form['zencoder_info']['video_thumbnail_count_zc'] = array(
331 '#type' => 'textfield',
332 '#title' => t('Number of thumbnails'),
333 '#description' => t('Number of thumbnails to display from video.'),
334 '#default_value' => variable_get('video_thumbnail_count', 5),
335 '#size' => 5,
336 );
337 $form['zencoder_info']['video_thumbnail_size'] = array(
338 '#type' => 'select',
339 '#title' => t('Dimension of thumbnails'),
340 '#default_value' => variable_get('video_thumbnail_size', '320x240'),
341 '#options' => video_utility::getDimensions(),
342 );
343 $form['zencoder_info']['video_zencoder_postback'] = array(
344 '#type' => 'textfield',
345 '#title' => t('Postback URL for Zencoder'),
346 '#description' =>
347 t('Important: Don\'t change this if you don\'t know what you\'re doing. The Postback URL is used by Zencoder to send transcoding status notifications to Drupal.') . '<br/>' .
348 t('Default: %value', array('%value' => url('postback/jobs', array('absolute' => TRUE)))),
349 '#default_value' => $this->postbackurl,
350 );
351 $form['zencoder_info']['video_zencoder_postback_donotvalidate'] = array(
352 '#type' => 'checkbox',
353 '#title' => t('Do not validate the Postback URL'),
354 '#description' => t('The Postback URL is validated by retrieving the URL from the local server. In some cases this fails while it works fine for the Zencoder notification sender. Use this checkbox to disable Postback URL validation.'),
355 '#default_value' => variable_get('video_zencoder_postback_donotvalidate', FALSE),
356 );
357
358 // testing
359 $form['zencoder_info']['testing'] = array(
360 '#type' => 'fieldset',
361 '#title' => t('Testing mode'),
362 '#collapsible' => TRUE,
363 '#collapsed' => TRUE,
364 );
365 $form['zencoder_info']['testing']['video_zencoder_testing_mode'] = array(
366 '#type' => 'checkbox',
367 '#title' => t('Test mode'),
368 '#default_value' => variable_get('video_zencoder_testing_mode', FALSE),
369 '#description' => t('Enable test mode to test upload/playback locally (if you have no public IP to test)')
370 );
371 $form['zencoder_info']['testing']['video_zencoder_test_file_path'] = array(
372 '#type' => 'textfield',
373 '#title' => t('Path to test video file'),
374 '#description' => t('Add the path to a video file for Zencoder to transcode.
375 You must use this file for testing when using a local machine with no public IP
376 address from which Zencoder can download video.'),
377 '#default_value' => variable_get('video_zencoder_test_file_path', 'http://example.com/video.mp4'),
378 );
379
380 // advanced
381 $form['zencoder_info']['advanced'] = array(
382 '#type' => 'fieldset',
383 '#title' => t('Advanced'),
384 '#collapsible' => TRUE,
385 '#collapsed' => TRUE,
386 );
387
388 $tempdestinations = array(
389 '' => t('Zencoder temporary storage') . ' (' . t('default') . ')',
390 );
391 if (module_exists('amazons3') && variable_get('amazons3_bucket', FALSE)) {
392 $tempdestinations['s3'] = t('Amazon S3 bucket %bucket', array('%bucket' => variable_get('amazons3_bucket')));
393 }
394 if (module_exists('cloud_files') && variable_get('rackspace_cloud_container', FALSE)) {
395 $tempdestinations['rcf'] = t('Rackspace Cloud Files container %container', array('%container' => variable_get('rackspace_cloud_container')));
396 }
397
398 if (count($tempdestinations) > 1) {
399 $form['zencoder_info']['advanced']['video_zencoder_output_destination'] = array(
400 '#type' => 'radios',
401 '#title' => t('Location for Zencoder output'),
402 '#default_value' => $this->outputdestination === NULL ? '' : $this->outputdestination,
403 '#options' => $tempdestinations,
404 '#description' => t('Normally, Zencoder uploads its transcoded files to its own Amazon S3 bucket from which the Video module will copy the file to the final destination. Use this setting to use a different location. If the selected location is identical to the final destination, this saves resource intensive copy operations during handling of the postback. The final destination is set per video field and defaults to the public files folder.'),
405 );
406
407 if (isset($tempdestinations['s3'])) {
408 $form['zencoder_info']['advanced']['video_zencoder_output_destination']['#description'] .= '<br/>' .
409 t('To enable Zencoder to upload directly to your Amazon S3 bucket, read the <a href="@zencoder-s3-url">Zencoder manual</a>.', array('@zencoder-s3-url' => url('https://app.zencoder.com/docs/guides/getting-started/working-with-s3')));
410 }
411 }
412
413 if (module_exists('amazons3')) {
414 $form['zencoder_info']['advanced']['video_zencoder_private'] = array(
415 '#type' => 'checkbox',
416 '#title' => t('Store files on Amazon S3 privately'),
417 '#default_value' => variable_get('video_zencoder_private', FALSE),
418 '#description' => t('Files stored privately are only accessible by visitors when <a href="@amazons3-settings">Presigned URLs</a> are enabled. These URLs expire, allow you to control access to the video files. For this setting to work, you must set %destination-setting-name to Amazon S3.', array('@amazons3-settings' => url('admin/config/media/amazons3'), '%destination-setting-name' => t('Location for Zencoder output'))),
419 );
420 }
421 }
422 return $form;
423 }
424
425 public function adminSettingsValidate($form, &$form_state) {
426 $v = $form_state['values'];
427
428 if (variable_get('video_zencoder_api_key', FALSE)) {
429 // Workaround for the use of the same variable in FFmpeg
430 $form_state['values']['video_thumbnail_count'] = $form_state['values']['video_thumbnail_count_zc'];
431 unset($form_state['values']['video_thumbnail_count_zc']);
432
433 // Check the postback URL if validation hasn't been disabled
434 if (empty($v['video_zencoder_postback_donotvalidate'])) {
435 $testurl = $v['video_zencoder_postback'];
436 $testcode = md5(mt_rand(0, REQUEST_TIME));
437 if (strpos($testurl, '?') === FALSE) {
438 $testurl .= '?test=1';
439 }
440 else {
441 $testurl .= '&test=1';
442 }
443 variable_set('video_postback_test', $testcode);
444 $result = drupal_http_request($testurl);
445 variable_del('video_postback_test');
446
447 $error = NULL;
448 if ($result->code != 200) {
449 $error = t('The postback URL cannot be retrieved: @error (@code).', array('@code' => $result->code, '@error' => empty($result->error) ? t('unknown error') : $result->error));
450 }
451 elseif (empty($result->data) || trim($result->data) != $testcode) {
452 $error = t('The postback URL is not valid: returned data contains unexpected value &quot;@value&quot;.', array('@value' => $result->data));
453 }
454
455 if ($error != NULL) {
456 form_error($form['zencoder_info']['video_zencoder_postback'], $error);
457 }
458 }
459 }
460 else {
461 // check terms and condition
462 if ($form_state['values']['agree_terms_zencoder'] == 0) {
463 form_set_error('agree_terms_zencoder', t('You must agree to the !link.', array('!link' => l(t('terms and conditions'), 'http://zencoder.com/terms'))));
464 }
465 // check for email exists
466 // Validate the e-mail address:
467 if ($error = user_validate_mail($form_state['values']['zencoder_username'])) {
468 form_set_error('zencoder_username', $error);
469 }
470
471 // get the API key from zencoder and save it to variable
472 if (!form_get_errors()) {
473 $mail = $form_state['values']['zencoder_username'];
474 $result = $this->createUser($mail);
475 if ($result !== TRUE) {
476 form_set_error('zencoder_username', $result);
477 }
478 else {
479 // Unset the form values because they do not need to be saved.
480 unset($form_state['values']['zencoder_username']);
481 unset($form_state['values']['agree_terms_zencoder']);
482 }
483 }
484 }
485 }
486
487 /**
488 * Create Zencoder user account
489 */
490 protected function createUser($mail) {
491 libraries_load('zencoder');
492 $zencoder = new Services_Zencoder();
493
494 try {
495 // $result is Services_Zencoder_Account
496 $result = $zencoder->accounts->create(array(
497 'terms_of_service' => '1',
498 'email' => $mail,
499 'affiliate_code' => 'drupal-video',
500 ));
501
502 variable_set('video_zencoder_api_key', $result->api_key);
503 drupal_set_message(t('Your Zencoder details are as below.<br/><b>API Key</b> : @api_key<br/> <b>Password</b> : @password<br/> You can now login to the <a href="@zencoder-url">Zencoder website</a> and track your transcoding jobs online. Make sure you <b>save user/pass combination somewhere</b> before you proceed.', array('@api_key' => $result->api_key, '@password' => $result->password, '@zencoder-url' => url('http://zencoder.com'))), 'status');
504
505 return TRUE;
506 }
507 catch (Services_Zencoder_Exception $e) {
508 if ($e->getErrors() == NULL) {
509 return $e->getMessage();
510 }
511
512 $errors = '';
513 foreach ($e->getErrors() as $error) {
514 if ($error == 'Email has already been taken') {
515 drupal_set_message(t('Your account already exists on Zencoder. So <a href="@login-url">login</a> to here and enter API key below.', array('@login-url' => 'https://app.zencoder.com/session/new')));
516 variable_set('video_zencoder_api_key', t('Please enter your API key'));
517 return TRUE;
518 }
519 $errors .= $error;
520 }
521
522 return $errors;
523 }
524 }
525
526 public function processPostback() {
527 if (strcasecmp($_SERVER['REQUEST_METHOD'], 'POST') !== 0) {
528 echo 'This is the Zencoder notification handler. It seems to work fine.';
529 return;
530 }
531
532 ignore_user_abort(TRUE);
533 libraries_load('zencoder');
534 $zencoder = new Services_Zencoder();
535
536 try {
537 $notification = $zencoder->notifications->parseIncoming();
538 } catch (Services_Zencoder_Exception $e) {
539 watchdog('transcoder', 'Postback received from Zencoder could not be decoded: @errormsg', array('@errormsg' => $e->getMessage()));
540 echo 'Bad request';
541 return;
542 }
543
544 if (!isset($notification->job->id)) {
545 watchdog('transcoder', 'Postback received from Zencoder is missing the job-id parameter');
546 echo 'Invalid data';
547 return;
548 }
549
550 // Check output/job state
551 $jobid = intval($notification->job->id);
552 $video_output = db_query('SELECT vid, original_fid, output_fid FROM {video_output} WHERE job_id = :job_id', array(':job_id' => $jobid))->fetch();
553 if (empty($video_output)) {
554 echo 'Not found';
555 return;
556 }
557
558 $fid = intval($video_output->original_fid);
559 watchdog('transcoder', 'Postback received from Zencoder for fid: @fid, Zencoder job id: @jobid.', array('@fid' => $fid, '@jobid' => $jobid));
560
561 // Find the transcoding job.
562 $video = video_jobs::load($fid);
563 if (empty($video)) {
564 echo 'Transcoding job not found in database';
565 return;
566 }
567
568 // Zencoder API 2.1.0 and above use $notification->job->outputs.
569 // For now, only one output is supported.
570 $output = isset($notification->output) ? $notification->output : current($notification->job->outputs);
571
572 // Find all error situations
573 if ($output->state === 'cancelled') {
574 video_jobs::setFailed($video);
575 echo 'Cancelled';
576 return;
577 }
578
579 if ($output->state === 'failed') {
580 $errorlink = t('no specific information given');
581 if (!empty($output->error_message)) {
582 if (!empty($output->error_link)) {
583 $errordetail = l(t($output->error_message), $output->error_link);
584 }
585 else {
586 $errordetail = t($output->error_message);
587 }
588 }
589
590 video_jobs::setFailed($video);
591 watchdog('transcoder', 'Zencoder reports errors in postback for fid @fid, job id @jobid: !errordetail', array('@fid' => $fid, '@jobid' => $jobid, '!errordetail' => $errordetail), WATCHDOG_ERROR);
592 echo 'Failure';
593 }
594
595 if ($notification->job->state !== 'finished') {
596 return;
597 }
598
599 // Move the converted video to its final destination
600 $outputfile = file_load($video_output->output_fid);
601 if (empty($outputfile)) {
602 echo 'Output file not found in database';
603 return;
604 }
605
606 // Sometimes the long duration of the copy() call causes Zencoder
607 // to timeout and retry the notification postback later.
608 // So we only copy the file when it doesn't exist or has a different file size.
609 if (!file_exists($outputfile->uri) || filesize($outputfile->uri) != $output->file_size_in_bytes) {
610 if (!$this->moveFile($output->url, $outputfile->uri)) {
611 video_jobs::setFailed($video);
612 watchdog('transcoder', 'While processing Zencoder postback, failed to copy @source-uri to @target-uri.', array('@source-uri' => $output->url, '@target-uri' => $outputfile->uri), WATCHDOG_ERROR);
613 return;
614 }
615 }
616
617 $outputfile->filesize = $output->file_size_in_bytes;
618 drupal_write_record('file_managed', $outputfile, 'fid');
619
620 // Actual processing of the response
621 $video->duration = round($output->duration_in_ms / 1000);
622 video_jobs::setCompleted($video);
623
624 // Clear the field cache. Normally, node_save() does this, but that function is not invoked in all cases
625 video_utility::clearEntityCache($video->entity_type, $video->entity_id);
626
627 // If there are no thumbnails, quit now.
628 if (empty($output->thumbnails)) {
629 return;
630 }
631
632 // Retrieve the thumbnails from the notification structure
633 // Pre-2.1.0, each thumbnail list was an array, now it is an object
634 $thumbnails = is_array($output->thumbnails[0]) ? $output->thumbnails[0]['images'] : $output->thumbnails[0]->images;
635 if (empty($thumbnails)) {
636 return;
637 }
638
639 // Find the entity to which the file belongs
640 $entity = video_utility::loadEntity($video->entity_type, $video->entity_id);
641 if (empty($entity)) {
642 watchdog('transcoder', 'The entity to which the transcoded video belongs can\'t be found anymore. Entity type: @entity-type, entity id: @entity-id.', array('@entity-type' => $video->entity_type, '@entity-id' => $video->entity_id), WATCHDOG_ERROR);
643 return;
644 }
645
646 // The following information was saved in video_jobs::create()
647 $fieldname = $video->data['field_name'];
648 $field = field_info_field($fieldname);
649 $langcode = $video->data['langcode'];
650 $delta = $video->data['delta'];
651
652 // Insanity checks
653 if (empty($entity->{$fieldname}[$langcode][$delta])) {
654 // The field can't be found anymore. This may be a problem.
655 watchdog('transcoder', 'The field to which video @filename was uploaded doesn\'t seem to exist anymore. Entity type: @entity-type, entity id: @entity-id, field name: @fieldname, field language: @langcode, delta: @delta.', array('@filename' => $video->filename, '@entity-type' => $video->entity_type, '@entity-id' => $video->entity_id, '@fieldname' => $fieldname, '@langcode' => $langcode, '@delta' => $delta), WATCHDOG_WARNING);
656 return;
657 }
658 if ($entity->{$fieldname}[$langcode][$delta]['fid'] != $video->fid) {
659 // The field does not contain the file we uploaded.
660 watchdog('transcoder', 'The field to which video @filename was uploaded doesn\'t seem to contain this video anymore. Entity type: @entity-type, entity id: @entity-id, field name: @fieldname, field language: @langcode, delta: @delta.', array('@filename' => $video->filename, '@entity-type' => $video->entity_type, '@entity-id' => $video->entity_id, '@fieldname' => $fieldname, '@langcode' => $langcode, '@delta' => $delta), WATCHDOG_WARNING);
661 return;
662 }
663
664 // Destination of thumbnails
665 $thumbscheme = !empty($field['settings']['uri_scheme_thumbnails']) ? $field['settings']['uri_scheme_thumbnails'] : 'public';
666 $thumburibase = $thumbscheme . '://' . variable_get('video_thumbnail_path', 'videos/thumbnails') . '/' . $video->fid . '/';
667 file_prepare_directory($thumburibase, FILE_CREATE_DIRECTORY);
668 $thumbwrapper = file_stream_wrapper_get_instance_by_scheme($thumbscheme);
669
670 // Turn the thumbnails into managed files.
671 // Because two jobs for the same video may finish simultaneously, lock here so
672 // there are no errors when inserting the files.
673 if (!lock_acquire('video_zencoder_thumbnails:' . $video->fid, count($thumbnails) * 30)) {
674 if (lock_wait('video_zencoder_thumbnails:' . $video->fid, count($thumbnails) * 30)) {
675 watchdog('transcoder', 'Failed to acquire lock to download thumbnails for @video-filename.', array('@video-filename' => $video->filename), WATCHDOG_ERROR);
676 return;
677 }
678 }
679
680 $existingthumbs = db_query('SELECT f.uri, f.fid, f.filesize FROM {file_managed} f INNER JOIN {video_thumbnails} t ON (f.fid = t.thumbnailfid) WHERE t.videofid = :fid', array(':fid' => $video->fid))->fetchAllAssoc('uri');
681 $thumbs = array();
682 $tnid = 0;
683 foreach ($thumbnails as $thumbnail) {
684 // Pre-2.1.0, each thumbnail was an array
685 $thumbnail = (object)$thumbnail;
686 $urlpath = parse_url($thumbnail->url, PHP_URL_PATH);
687 $ext = video_utility::getExtension($urlpath);
688 $update = array();
689 $thumb = new stdClass();
690 $thumb->uid = $outputfile->uid; // $entity may not have a uid property, so take it from the output file.
691 $thumb->status = FILE_STATUS_PERMANENT;
692 $thumb->filename = 'thumbnail-' . $video->fid . '_' . sprintf('%04d', $tnid++) . '.' . $ext;
693 $thumb->uri = $thumburibase . $thumb->filename;
694 $thumb->filemime = $thumbwrapper->getMimeType($thumb->uri);
695 $thumb->type = 'image'; // For the media module
696 $thumb->filesize = $thumbnail->file_size_bytes;
697 $thumb->timestamp = REQUEST_TIME;
698
699 $shouldcopy = TRUE;
700 if (isset($existingthumbs[$thumb->uri])) {
701 // If the thumbnail has the same size in the database compared to the notification data, don't copy
702 if (file_exists($thumb->uri) && $existingthumbs[$thumb->uri]->filesize == $thumb->filesize) {
703 $shouldcopy = FALSE;
704 }
705 $thumb->fid = intval($existingthumbs[$thumb->uri]->fid);
706 $update = array('fid');
707 }
708
709 if ($shouldcopy && !$this->moveFile($thumbnail->url, $thumb->uri)) {
710 watchdog('transcoder', 'Could not copy @thumbsrc to @thumbdest.', array('@thumbsrc' => $thumbnail->url, '@thumbdest' => $thumb->uri), WATCHDOG_ERROR);
711 continue;
712 }
713
714 drupal_write_record('file_managed', $thumb, $update);
715
716 // Saving to video_thumbnails and file_usage is only necessary when this is a new thumbnail
717 if (!isset($existingthumbs[$thumb->uri])) {
718 db_insert('video_thumbnails')->fields(array('videofid' => $video->fid, 'thumbnailfid' => $thumb->fid))->execute();
719 file_usage_add($thumb, 'file', $video->entity_type, $video->entity_id);
720 }
721
722 $thumbs[$thumb->fid] = $thumb;
723 }
724 lock_release('video_zencoder_thumbnails:' . $video->fid);
725
726 // Clear the field cache. Normally, node_save() does this, but that function is not invoked in all cases
727 video_utility::clearEntityCache($video->entity_type, $video->entity_id);
728
729 // Skip setting the thumbnail if there are no thumbnails or when the current value is already valid
730 $currentthumb = isset($entity->{$fieldname}[$langcode][$delta]['thumbnail']) ? intval($entity->{$fieldname}[$langcode][$delta]['thumbnail']) : 0;
731 if (empty($thumbs) || isset($thumbs[$currentthumb])) {
732 return;
733 }
734
735 // Set a random thumbnail fid on the entity and save the entity
736 $entity->{$fieldname}[$langcode][$delta]['thumbnail'] = array_rand($thumbs);
737
738 switch ($video->entity_type) {
739 case 'node':
740 node_save($entity);
741 break;
742 case 'comment':
743 comment_save($entity);
744 break;
745 default:
746 // entity_save() is supplied by the entity module
747 if (function_exists('entity_save')) {
748 entity_save($video->entity_type, $entity);
749 }
750 break;
751 }
752 }
753
754 private function moveFile($srcurl, $dsturi) {
755 $unlinksrc = FALSE;
756
757 // If the Amazon S3 or Cloud Files module is used, we know that the file is on s3:// or rcf://.
758 // We must move the file, because the original must be deleted.
759 if ($this->outputdestination == 's3' || $this->outputdestination == 'rcf') {
760 $srcuri = $this->outputdestination . ':/' . parse_url($srcurl, PHP_URL_PATH);
761 // Check if the file is already at the right place
762 if ($srcuri === $dsturi) {
763 return TRUE;
764 }
765 // Move the file if the target is also s3:// or rcf://
766 if (file_uri_scheme($dsturi) == $this->outputdestination) {
767 return rename($srcuri, $dsturi);
768 }
769
770 // The src file needs to be removed after copying.
771 $unlinksrc = TRUE;
772 }
773
774 // Check if $srcurl is actually a $uri
775 $srcuri = $srcurl;
776 if (strncmp('http', $srcuri, 4) === 0) {
777 $srcuri = video_utility::urlToUri($srcurl);
778 if ($srcuri === NULL) {
779 $srcuri = $srcurl;
780 }
781 }
782
783 // Check if the file is already at the right place
784 if ($srcuri === $dsturi) {
785 return TRUE;
786 }
787
788 $result = copy($srcuri, $dsturi);
789 if ($result && $unlinksrc) {
790 unlink($srcuri);
791 }
792
793 return $result;
794 }
795
796 /**
797 * Get enabled and supporting codecs by Zencoder.
798 */
799 public function getCodecs() {
800 $auto = t('Default for this extension');
801
802 return array(
803 'encode' => array(
804 'video' => array(
805 '' => $auto,
806 'h264' => 'H.264',
807 'vp8' => 'VP8',
808 'theora' => 'Theora',
809 'vp6' => 'VP6',
810 'mpeg4' => 'MPEG-4',
811 'wmv' => 'WMV',
812 ),
813 'audio' => array(
814 '' => $auto,
815 'aac' => 'AAC',
816 'mp3' => 'MP3',
817 'vorbis' => 'Vorbis',
818 'wma' => 'WMA',
819 )
820 ),
821 'decode' => array(),
822 );
823 }
824
825 public function getAvailableFormats($type = FALSE) {
826 return array(
827 '3g2' => '3G2',
828 '3gp' => '3GP',
829 '3gp2' => '3GP2',
830 '3gpp' => '3GPP',
831 '3gpp2' => '3GPP2',
832 'aac' => 'AAC',
833 'f4a' => 'F4A',
834 'f4b' => 'F4B',
835 'f4v' => 'F4V',
836 'flv' => 'FLV',
837 'm4a' => 'M4A',
838 'm4b' => 'M4B',
839 'm4r' => 'M4R',
840 'm4v' => 'M4V',
841 'mov' => 'MOV',
842 'mp3' => 'MP3',
843 'mp4' => 'MP4',
844 'oga' => 'OGA',
845 'ogg' => 'OGG',
846 'ogv' => 'OGV',
847 'ogx' => 'OGX',
848 'ts' => 'TS',
849 'webm' => 'WebM',
850 'wma' => 'WMA',
851 'wmv' => 'WMV',
852 );
853 }
854
855 public function getPixelFormats() {
856 // Zencoder doesn't support this
857 return array();
858 }
859
860 /**
861 * Reset internal variables to their initial state.
862 */
863 public function reset($keepinput = FALSE) {
864 parent::reset($keepinput);
865
866 if (!$keepinput) {
867 unset($this->options['input']);
868 }
869 unset($this->options['output']);
870 }
871 }