From 0c8e7ae689eed6291bb2061c9c75e3057b230339 Mon Sep 17 00:00:00 2001 From: Mohamed Mujahid Date: Tue, 6 Jul 2010 17:04:02 +0000 Subject: merging changes from DRUPAL-6--4 --- INSTALL.txt | 259 +-- README.txt | 21 +- cdn/video_s3/includes/S3.php | 1365 ++++++++++++++++ cdn/video_s3/includes/amazon_s3.inc | 145 ++ cdn/video_s3/video_s3.admin.inc | 134 ++ cdn/video_s3/video_s3.info | 8 + cdn/video_s3/video_s3.install | 107 ++ cdn/video_s3/video_s3.module | 114 ++ css/video.css | 134 ++ includes/conversion.inc | 262 +++ includes/metadata.inc | 57 + includes/transcoder.inc | 96 ++ includes/video_helper.inc | 116 ++ js/flowplayer-3.2.0.min.js | 24 + js/jquery.media.js | 458 ++++++ js/jquery.metadata.js | 148 ++ js/video.js | 84 + plugins/video_zencoder/includes/LICENSE | 20 + plugins/video_zencoder/includes/README.markdown | 199 +++ plugins/video_zencoder/includes/Zencoder.php | 239 +++ plugins/video_zencoder/includes/lib/JSON.php | 806 +++++++++ plugins/video_zencoder/includes/zencoder.inc | 280 ++++ plugins/video_zencoder/video_zencoder.admin.inc | 302 ++++ plugins/video_zencoder/video_zencoder.info | 7 + plugins/video_zencoder/video_zencoder.install | 116 ++ plugins/video_zencoder/video_zencoder.module | 104 ++ theme/video-play-dcr.tpl.php | 17 + theme/video-play-divx.tpl.php | 20 + theme/video-play-flash.tpl.php | 20 + theme/video-play-flv.tpl.php | 14 + theme/video-play-quicktime.tpl.php | 32 + theme/video-play-realmedia.tpl.php | 22 + theme/video-play-theora.tpl.php | 23 + theme/video-play-windowsmedia.tpl.php | 21 + transcoders/video_ffmpeg.inc | 312 ++++ transcoders/video_ffmpeg_wrapper.inc | 243 +++ types/uploadfield/uploadfield.theme.inc | 18 + types/videoftp/videoftp.info | 9 + types/videoftp/videoftp.install | 25 + types/videoftp/videoftp.module | 200 +++ types/videoftp/videoftp.theme.inc | 57 + types/videoftp/videoftp_widget.inc | 313 ++++ video.info | 7 +- video.install | 167 +- video.module | 1977 ++++++++--------------- video.theme.inc | 105 ++ video_formatter.inc | 177 ++ views/video.views.inc | 77 + views/video_views_handler_field_data.inc | 60 + 49 files changed, 7886 insertions(+), 1635 deletions(-) create mode 100644 cdn/video_s3/includes/S3.php create mode 100644 cdn/video_s3/includes/amazon_s3.inc create mode 100644 cdn/video_s3/video_s3.admin.inc create mode 100644 cdn/video_s3/video_s3.info create mode 100644 cdn/video_s3/video_s3.install create mode 100644 cdn/video_s3/video_s3.module create mode 100644 css/video.css create mode 100644 includes/conversion.inc create mode 100644 includes/metadata.inc create mode 100644 includes/transcoder.inc create mode 100644 includes/video_helper.inc create mode 100644 js/flowplayer-3.2.0.min.js create mode 100644 js/jquery.media.js create mode 100644 js/jquery.metadata.js create mode 100644 js/video.js create mode 100644 plugins/video_zencoder/includes/LICENSE create mode 100644 plugins/video_zencoder/includes/README.markdown create mode 100644 plugins/video_zencoder/includes/Zencoder.php create mode 100644 plugins/video_zencoder/includes/lib/JSON.php create mode 100644 plugins/video_zencoder/includes/zencoder.inc create mode 100644 plugins/video_zencoder/video_zencoder.admin.inc create mode 100644 plugins/video_zencoder/video_zencoder.info create mode 100644 plugins/video_zencoder/video_zencoder.install create mode 100644 plugins/video_zencoder/video_zencoder.module create mode 100644 theme/video-play-dcr.tpl.php create mode 100644 theme/video-play-divx.tpl.php create mode 100644 theme/video-play-flash.tpl.php create mode 100644 theme/video-play-flv.tpl.php create mode 100644 theme/video-play-quicktime.tpl.php create mode 100644 theme/video-play-realmedia.tpl.php create mode 100644 theme/video-play-theora.tpl.php create mode 100644 theme/video-play-windowsmedia.tpl.php create mode 100644 transcoders/video_ffmpeg.inc create mode 100644 transcoders/video_ffmpeg_wrapper.inc create mode 100644 types/uploadfield/uploadfield.theme.inc create mode 100644 types/videoftp/videoftp.info create mode 100644 types/videoftp/videoftp.install create mode 100644 types/videoftp/videoftp.module create mode 100644 types/videoftp/videoftp.theme.inc create mode 100644 types/videoftp/videoftp_widget.inc create mode 100644 video.theme.inc create mode 100644 video_formatter.inc create mode 100644 views/video.views.inc create mode 100644 views/video_views_handler_field_data.inc diff --git a/INSTALL.txt b/INSTALL.txt index 5f478a1..88a2214 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -2,209 +2,66 @@ REQUIREMENTS ------------------------------------ -Optional: - Views support requires the Views module to be activated. - video_image module require image module to be activated. +Required : + CCK Module. + Filefield Module. INSTALL INSTRUCTIONS FOR VIDEO.MODULE ------------------------------------- -- See : http://video.heidisoft.com/docs/users-guide-3 - -- Copy video folder to modules/ -- Login as site administrator -- Activate video.module as usual (administer->modules) -- Activate optional plugin modules (called video_something) -- Set access control as you like (administer->access control) -- (optional) Activate video.module blocks (administer->blocks) -- (optional) Activate video views using views.module - * If you are upgrading from a previous version of the video module, - go to admin -> modules and choose "Save". This will flush the - views cache to ensure you can see the views. - * If you previously configured the video ffmpeg helper sub-module, - re-copy and re-edit the video_render.php and video_scheduler.php from - the folder modules/video/plugins/video_ffmpeg_helper to the root - of your web site. - * Go to admin -> views. You will see a default view supplied for - video. You can also create a new one by choosing "Add." In the - "Fields" section, you should see selections for "Video: (node info)". - - -UPGRADING FROM DRUPAL 4.7 to CVS --------------------------------- - -The upgrade process is automated by the video.install file, so -be sure you have the correct version of video.install to match -the version of video.module you intend to use. If you downloaded -a tarball of Video from drupal.org and over-wrote everything in -your Video module directory, you should be fine. - -Run the update.php script that comes in the Drupal distribution - http://www.example.com/update.php - - -UPGRADING FROM 4.6 OR 4.6 CVS ------------------------------ - -Starting in Drupal 4.7, the video module has an automated upgrade. -If you installed and used the stable video 4.6 release, the -update.php script that upgrades Drupal will also update the -video database schema. http://www.example.com/update.php - -If you decided to use a CVS version of Video for Drupal 4.6, you -can still take advantage of the automated ugprade, but you will -need to select an update version number when you run update.php. - -The following is a key of upgrade version numbers for Video CVS -database schemas between the Drupal 4.6 and Drupal 4.7 -changes. When you find a database schema that matches your -existing schema, select the corresponding update.php version -number when you run update.php. You can get your current database -schema by looking at the old video.mysql file you used to create -your video table or execute the following SQL query: - `DESCRIBE video;` - -Some differences between versions are subtle, but important, so -make sure you verify your video table is an actual fit, not just -"close enough". - -VIDEO UPDATE.PHP VERSION 2 --------------------------- - - CREATE TABLE video ( - nid int(10) unsigned NOT NULL default '0', - vidfile text NOT NULL, - videox smallint(4) NOT NULL default '0', - videoy smallint(4) NOT NULL default '0', - size bigint(13) default NULL, - clicks int(10) unsigned NOT NULL default '0', - video_bitrate int(11) default NULL, - audio_bitrate int(11) default NULL, - audio_sampling_rate int(11) default NULL, - audio_channels enum('','stereo','mono') default NULL, - playtime_seconds int(11) default NULL, - PRIMARY KEY (nid) - ) TYPE=MyISAM COMMENT='size is in bytes'; - - -VIDEO UPDATE.PHP VERSION 3 --------------------------- - - CREATE TABLE video ( - nid int(10) unsigned NOT NULL default '0', - vidfile text NOT NULL, - videox smallint(4) NOT NULL default '0', - videoy smallint(4) NOT NULL default '0', - size bigint(13) default NULL, - download_counter int(10) unsigned NOT NULL default '0', - play_counter int(10) unsigned NOT NULL default '0', - video_bitrate int(11) default NULL, - audio_bitrate int(11) default NULL, - audio_sampling_rate int(11) default NULL, - audio_channels enum('','5.1','stereo','mono') default NULL, - playtime_seconds int(11) default NULL, - download_folder varchar(255) NULL default NULL, - disable_multidownload tinyint(1) NOT NULL default '0', - use_play_folder tinyint(1) NOT NULL default '0', - custom_field_1 varchar(255) NULL default NULL, - custom_field_2 varchar(255) NULL default NULL, - custom_field_3 varchar(255) NULL default NULL, - custom_field_4 varchar(255) NULL default NULL, - custom_field_5 text NULL default NULL, - custom_field_6 text NULL default NULL, - PRIMARY KEY (nid) - ) TYPE=MyISAM COMMENT='size is in bytes'; - - -VIDEO UPDATE.PHP VERSION 4 --------------------------- - - -- $Id$ - CREATE TABLE video ( - nid int(10) unsigned NOT NULL default '0', - vidfile text NOT NULL default '', - videox smallint(4) unsigned NOT NULL default '0', - videoy smallint(4) unsigned NOT NULL default '0', - size bigint(13) unsigned default NULL, - download_counter int(10) unsigned NOT NULL default '0', - play_counter int(10) unsigned NOT NULL default '0', - video_bitrate int(11) unsigned default NULL, - audio_bitrate int(11) unsigned default NULL, - audio_sampling_rate int(11) unsigned default NULL, - audio_channels enum('','5.1','stereo','mono') default NULL, - playtime_seconds int(11) unsigned default NULL, - download_folder varchar(255) NULL default NULL, - disable_multidownload tinyint(1) unsigned NOT NULL default '0', - use_play_folder tinyint(1) unsigned NOT NULL default '0', - custom_field_1 varchar(255) NULL default NULL, - custom_field_2 varchar(255) NULL default NULL, - custom_field_3 varchar(255) NULL default NULL, - custom_field_4 varchar(255) NULL default NULL, - custom_field_5 text NULL default NULL, - custom_field_6 text NULL default NULL, - PRIMARY KEY (nid) - ) TYPE=MyISAM COMMENT='size is in bytes'; - - -VIDEO UPDATE.PHP VERSION 5 --------------------------- - - -- $Id$ - CREATE TABLE video ( - vid int(10) unsigned NOT NULL default '0', - nid int(10) unsigned NOT NULL default '0', - vidfile text NOT NULL default '', - videox smallint(4) unsigned NOT NULL default '0', - videoy smallint(4) unsigned NOT NULL default '0', - size bigint(13) unsigned default NULL, - download_counter int(10) unsigned NOT NULL default '0', - play_counter int(10) unsigned NOT NULL default '0', - video_bitrate int(10) unsigned default NULL, - audio_bitrate int(10) unsigned default NULL, - audio_sampling_rate int(10) unsigned default NULL, - audio_channels enum('','5.1','stereo','mono') default NULL, - playtime_seconds int(10) unsigned default NULL, - download_folder varchar(255) NULL default NULL, - disable_multidownload tinyint(1) unsigned NOT NULL default '0', - use_play_folder tinyint(1) unsigned NOT NULL default '0', - custom_field_1 varchar(255) NULL default NULL, - custom_field_2 varchar(255) NULL default NULL, - custom_field_3 varchar(255) NULL default NULL, - custom_field_4 varchar(255) NULL default NULL, - custom_field_5 text NULL default NULL, - custom_field_6 text NULL default NULL, - serialized_data text NULL default NULL, - PRIMARY KEY (vid) - ) TYPE=MyISAM COMMENT='size is in bytes'; - -VIDEO UPDATE.PHP VERSION 6 --------------------------- - - -- $Id$ - CREATE TABLE video ( - vid int(10) unsigned NOT NULL default '0', - nid int(10) unsigned NOT NULL default '0', - vidfile text NOT NULL default '', - videox smallint(4) unsigned NOT NULL default '0', - videoy smallint(4) unsigned NOT NULL default '0', - size bigint(13) unsigned default NULL, - download_counter int(10) unsigned NOT NULL default '0', - play_counter int(10) unsigned NOT NULL default '0', - video_bitrate int(10) unsigned default NULL, - audio_bitrate int(10) unsigned default NULL, - audio_sampling_rate int(10) unsigned default NULL, - audio_channels enum('','5.1','stereo','mono') default NULL, - playtime_seconds int(10) unsigned default NULL, - download_folder varchar(255) NULL default NULL, - disable_multidownload tinyint(1) unsigned NOT NULL default '0', - use_play_folder tinyint(1) unsigned NOT NULL default '0', - custom_field_1 varchar(255) NULL default NULL, - custom_field_2 varchar(255) NULL default NULL, - custom_field_3 varchar(255) NULL default NULL, - custom_field_4 varchar(255) NULL default NULL, - custom_field_5 text NULL default NULL, - custom_field_6 text NULL default NULL, - serialized_data text NULL default NULL, - PRIMARY KEY (vid) - ) TYPE=MyISAM /*!40100 DEFAULT CHARACTER SET utf8 */; +- See : http://video.heidisoft.com/content/welcome-video-module-documentation + + +MIGRATE INSTRUCTIONS TO VIDEO.MODULE +------------------------------------- +- See : http://drupal.org/node/713482 + + +FFMPEG CONFIGURATIONS +--------------------- + +This helper module facilitates uploading new videos using the video module. It +features a batch processing queue for videos to be transcoded and automatic +thumbnail generation. + +Install instructions +-------------------- + +1. Activate the auto conversion and auto thumbnails when cerating content +2. Setup it's advanced options to meet your needs +3. Move (or symlink) video_render.php and video_scheduler.php into your Drupal root +4. Check permissions of the files and folders (/tmp/video and files/* must be writa +ble by the webserver or the user executling the cron job) +5. You now have two options to execute the video_scheduler.php script: + + 5.1 Schedule the execution of video_scheduler.php using unix cron + + The crontab should look something like this: + + # m h dom mon dow user command + */20 * * * * www-data cd /absolute/path/to/drupal/ ; php video_scheduler.php http://www.example.com/path_to_drupal + + This will execute the video_scheduler every 20 minutes. + + Note that the video_scheduler doesn't produce any output and cannot be called + from the web. It will, however, put some information in the watchdog. + + + +Troubleshooting +------------------------ + +Configuring and installing ffmpeg in a web server environment might be pretty difficult. +In order to help you troubleshoot the transcoding process the ffmpeg helper puts debugging +informations on the drupal logs. I strongly suggest to have a look at them if you are experiencing + problems with transcoding. + +The ffmpeg puts in the drupal logs the commands it was trying to execute. You might try +to rerun them on a command shell in order understand what went wrong. + +or contact a Drupal developer + + + + diff --git a/README.txt b/README.txt index 94366f6..ac08195 100644 --- a/README.txt +++ b/README.txt @@ -1,27 +1,30 @@ // $Id$ - -VIDEO MODULE ------------ //TODO + This module add the possibility to create video nodes which are containers to embed videos into drupal pages. -For installation and upgrade instructions see INSTALL.txt -http://video.heidisoft.com/docs/users-guide-3 + to embed videos into drupal pages, using CCK and filefield support. +This module add the possibility to create video nodes which are containers + to embed videos into drupal pages, using CCK and filefield support. For information about supported file types see FILE_TYPES.txt -http://video.heidisoft.com/features +http://video.heidisoft.com/content/welcome-video-module-documentation For general instructions read video.module handbook: - http://video.heidisoft.com/docs/users-guide +http://video.heidisoft.com/content/features Please submit bugs/features/support requests at: - http://drupal.org/node/add/project_issue/video - http://video.heidisoft.com/contact + http://video.heidisoft.com/content/welcome-video-module-documentation + http://video.heidisoft.com/contact + Maintainers ----------- - Glen Marianko: twitter at demoforum, glenm at demoforum dot com + Heshan Wanigasooriya :heshan at heidisoft dot com, heshanmw at gmail dot com + Dennis : http://drupal.org/user/384543 + Glen Marianko : http://drupal.org/usr/527446 http://linkedin.com/in/glenergetic twitter: @glenergetic diff --git a/cdn/video_s3/includes/S3.php b/cdn/video_s3/includes/S3.php new file mode 100644 index 0000000..70d3ad8 --- /dev/null +++ b/cdn/video_s3/includes/S3.php @@ -0,0 +1,1365 @@ +getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + $results = array(); + if (!isset($rest->body->Buckets)) return $results; + + if ($detailed) { + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) + $results['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->ID + ); + $results['buckets'] = array(); + foreach ($rest->body->Buckets->Bucket as $b) + $results['buckets'][] = array( + 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate) + ); + } else + foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name; + + return $results; + } + + + /* + * Get contents for a bucket + * + * If maxKeys is null this method will loop through truncated result sets + * + * @param string $bucket Bucket name + * @param string $prefix Prefix + * @param string $marker Marker (last file listed) + * @param string $maxKeys Max keys (maximum number of keys to return) + * @param string $delimiter Delimiter + * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes + * @return array | false + */ + public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) { + $rest = new S3Request('GET', $bucket, ''); + if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); + if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker); + if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys); + if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); + $response = $rest->getResponse(); + if ($response->error === false && $response->code !== 200) + $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status'); + if ($response->error !== false) { + trigger_error(sprintf("S3::getBucket(): [%s] %s", $response->error['code'], $response->error['message']), E_USER_WARNING); + return false; + } + + $results = array(); + + $nextMarker = null; + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strtotime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $nextMarker = (string)$c->Key; + } + + if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) + foreach ($response->body->CommonPrefixes as $c) + $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); + + if (isset($response->body, $response->body->IsTruncated) && + (string)$response->body->IsTruncated == 'false') return $results; + + if (isset($response->body, $response->body->NextMarker)) + $nextMarker = (string)$response->body->NextMarker; + + // Loop through truncated results if maxKeys isn't specified + if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true') + do { + $rest = new S3Request('GET', $bucket, ''); + if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); + $rest->setParameter('marker', $nextMarker); + if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); + + if (($response = $rest->getResponse(true)) == false || $response->code !== 200) break; + + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strtotime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $nextMarker = (string)$c->Key; + } + + if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) + foreach ($response->body->CommonPrefixes as $c) + $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); + + if (isset($response->body, $response->body->NextMarker)) + $nextMarker = (string)$response->body->NextMarker; + + } while ($response !== false && (string)$response->body->IsTruncated == 'true'); + + return $results; + } + + + /** + * Put a bucket + * + * @param string $bucket Bucket name + * @param constant $acl ACL flag + * @param string $location Set as "EU" to create buckets hosted in Europe + * @return boolean + */ + public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) { + $rest = new S3Request('PUT', $bucket, ''); + $rest->setAmzHeader('x-amz-acl', $acl); + + if ($location !== false) { + $dom = new DOMDocument; + $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration'); + $locationConstraint = $dom->createElement('LocationConstraint', strtoupper($location)); + $createBucketConfiguration->appendChild($locationConstraint); + $dom->appendChild($createBucketConfiguration); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + } + $rest = $rest->getResponse(); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Delete an empty bucket + * + * @param string $bucket Bucket name + * @return boolean + */ + public static function deleteBucket($bucket) { + $rest = new S3Request('DELETE', $bucket); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::deleteBucket({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Create input info array for putObject() + * + * @param string $file Input file + * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) + * @return array | false + */ + public static function inputFile($file, $md5sum = true) { + if (!file_exists($file) || !is_file($file) || !is_readable($file)) { + trigger_error('S3::inputFile(): Unable to open input file: '.$file, E_USER_WARNING); + return false; + } + return array('file' => $file, 'size' => filesize($file), + 'md5sum' => $md5sum !== false ? (is_string($md5sum) ? $md5sum : + base64_encode(md5_file($file, true))) : ''); + } + + + /** + * Create input array info for putObject() with a resource + * + * @param string $resource Input resource to read from + * @param integer $bufferSize Input byte size + * @param string $md5sum MD5 hash to send (optional) + * @return array | false + */ + public static function inputResource(&$resource, $bufferSize, $md5sum = '') { + if (!is_resource($resource) || $bufferSize < 0) { + trigger_error('S3::inputResource(): Invalid resource or buffer size', E_USER_WARNING); + return false; + } + $input = array('size' => $bufferSize, 'md5sum' => $md5sum); + $input['fp'] =& $resource; + return $input; + } + + + /** + * Put an object + * + * @param mixed $input Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param array $requestHeaders Array of request headers or content type as a string + * @return boolean + */ + public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array()) { + if ($input === false) return false; + $rest = new S3Request('PUT', $bucket, $uri); + + if (is_string($input)) $input = array( + 'data' => $input, 'size' => strlen($input), + 'md5sum' => base64_encode(md5($input, true)) + ); + + // Data + if (isset($input['fp'])) + $rest->fp =& $input['fp']; + elseif (isset($input['file'])) + $rest->fp = @fopen($input['file'], 'rb'); + elseif (isset($input['data'])) + $rest->data = $input['data']; + + // Content-Length (required) + if (isset($input['size']) && $input['size'] >= 0) + $rest->size = $input['size']; + else { + if (isset($input['file'])) + $rest->size = filesize($input['file']); + elseif (isset($input['data'])) + $rest->size = strlen($input['data']); + } + + // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) + if (is_array($requestHeaders)) + foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v); + elseif (is_string($requestHeaders)) // Support for legacy contentType parameter + $input['type'] = $requestHeaders; + + // Content-Type + if (!isset($input['type'])) { + if (isset($requestHeaders['Content-Type'])) + $input['type'] =& $requestHeaders['Content-Type']; + elseif (isset($input['file'])) + $input['type'] = self::__getMimeType($input['file']); + else + $input['type'] = 'application/octet-stream'; + } + + // We need to post with Content-Length and Content-Type, MD5 is optional + if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) { + $rest->setHeader('Content-Type', $input['type']); + if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']); + + $rest->setAmzHeader('x-amz-acl', $acl); + foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); + $rest->getResponse(); + } else + $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters'); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) { + trigger_error(sprintf("S3::putObject(): [%s] %s", $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Put an object from a file (legacy function) + * + * @param string $file Input file path + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) { + return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Put an object from a string (legacy function) + * + * @param string $string Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') { + return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Get an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param mixed $saveTo Filename or resource to write to + * @return mixed + */ + public static function getObject($bucket, $uri, $saveTo = false) { + $rest = new S3Request('GET', $bucket, $uri); + if ($saveTo !== false) { + if (is_resource($saveTo)) + $rest->fp =& $saveTo; + else + if (($rest->fp = @fopen($saveTo, 'wb')) !== false) + $rest->file = realpath($saveTo); + else + $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); + } + if ($rest->response->error === false) $rest->getResponse(); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) { + trigger_error(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s", + $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING); + return false; + } + return $rest->response; + } + + + /** + * Get object information + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param boolean $returnInfo Return response information + * @return mixed | false + */ + public static function getObjectInfo($bucket, $uri, $returnInfo = true) { + $rest = new S3Request('HEAD', $bucket, $uri); + $rest = $rest->getResponse(); + if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false; + } + + + /** + * Copy an object + * + * @param string $bucket Source bucket name + * @param string $uri Source object URI + * @param string $bucket Destination bucket name + * @param string $uri Destination object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Optional array of x-amz-meta-* headers + * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.) + * @return mixed | false + */ + public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array()) { + $rest = new S3Request('PUT', $bucket, $uri); + $rest->setHeader('Content-Length', 0); + foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v); + foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); + $rest->setAmzHeader('x-amz-acl', $acl); + $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, $srcUri)); + if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0) + $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return isset($rest->body->LastModified, $rest->body->ETag) ? array( + 'time' => strtotime((string)$rest->body->LastModified), + 'hash' => substr((string)$rest->body->ETag, 1, -1) + ) : false; + } + + + /** + * Set logging for a bucket + * + * @param string $bucket Bucket name + * @param string $targetBucket Target bucket (where logs are stored) + * @param string $targetPrefix Log prefix (e,g; domain.com-) + * @return boolean + */ + public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) { + // The S3 log delivery group has to be added to the target bucket's ACP + if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false) { + // Only add permissions to the target bucket when they do not exist + $aclWriteSet = false; + $aclReadSet = false; + foreach ($acp['acl'] as $acl) + if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery') { + if ($acl['permission'] == 'WRITE') $aclWriteSet = true; + elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true; + } + if (!$aclWriteSet) $acp['acl'][] = array( + 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE' + ); + if (!$aclReadSet) $acp['acl'][] = array( + 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP' + ); + if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp); + } + + $dom = new DOMDocument; + $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus'); + $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); + if ($targetBucket !== null) { + if ($targetPrefix == null) $targetPrefix = $bucket . '-'; + $loggingEnabled = $dom->createElement('LoggingEnabled'); + $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket)); + $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix)); + // TODO: Add TargetGrants? + $bucketLoggingStatus->appendChild($loggingEnabled); + } + $dom->appendChild($bucketLoggingStatus); + + $rest = new S3Request('PUT', $bucket, ''); + $rest->setParameter('logging', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::setBucketLogging({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get logging status for a bucket + * + * This will return false if logging is not enabled. + * Note: To enable logging, you also need to grant write access to the log group + * + * @param string $bucket Bucket name + * @return array | false + */ + public static function getBucketLogging($bucket) { + $rest = new S3Request('GET', $bucket, ''); + $rest->setParameter('logging', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getBucketLogging({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + if (!isset($rest->body->LoggingEnabled)) return false; // No logging + return array( + 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket, + 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix, + ); + } + + + /** + * Disable bucket logging + * + * @param string $bucket Bucket name + * @return boolean + */ + public static function disableBucketLogging($bucket) { + return self::setBucketLogging($bucket, null); + } + + + /** + * Get a bucket's location + * + * @param string $bucket Bucket name + * @return string | false + */ + public static function getBucketLocation($bucket) { + $rest = new S3Request('GET', $bucket, ''); + $rest->setParameter('location', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getBucketLocation({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US'; + } + + + /** + * Set object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) + * @return boolean + */ + public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) { + $dom = new DOMDocument; + $dom->formatOutput = true; + $accessControlPolicy = $dom->createElement('AccessControlPolicy'); + $accessControlList = $dom->createElement('AccessControlList'); + + // It seems the owner has to be passed along too + $owner = $dom->createElement('Owner'); + $owner->appendChild($dom->createElement('ID', $acp['owner']['id'])); + $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name'])); + $accessControlPolicy->appendChild($owner); + + foreach ($acp['acl'] as $g) { + $grant = $dom->createElement('Grant'); + $grantee = $dom->createElement('Grantee'); + $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + if (isset($g['id'])) { // CanonicalUser (DisplayName is omitted) + $grantee->setAttribute('xsi:type', 'CanonicalUser'); + $grantee->appendChild($dom->createElement('ID', $g['id'])); + } elseif (isset($g['email'])) { // AmazonCustomerByEmail + $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail'); + $grantee->appendChild($dom->createElement('EmailAddress', $g['email'])); + } elseif ($g['type'] == 'Group') { // Group + $grantee->setAttribute('xsi:type', 'Group'); + $grantee->appendChild($dom->createElement('URI', $g['uri'])); + } + $grant->appendChild($grantee); + $grant->appendChild($dom->createElement('Permission', $g['permission'])); + $accessControlList->appendChild($grant); + } + + $accessControlPolicy->appendChild($accessControlList); + $dom->appendChild($accessControlPolicy); + + $rest = new S3Request('PUT', $bucket, $uri); + $rest->setParameter('acl', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return mixed | false + */ + public static function getAccessControlPolicy($bucket, $uri = '') { + $rest = new S3Request('GET', $bucket, $uri); + $rest->setParameter('acl', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + + $acp = array(); + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) { + $acp['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName + ); + } + if (isset($rest->body->AccessControlList)) { + $acp['acl'] = array(); + foreach ($rest->body->AccessControlList->Grant as $grant) { + foreach ($grant->Grantee as $grantee) { + if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser + $acp['acl'][] = array( + 'type' => 'CanonicalUser', + 'id' => (string)$grantee->ID, + 'name' => (string)$grantee->DisplayName, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail + $acp['acl'][] = array( + 'type' => 'AmazonCustomerByEmail', + 'email' => (string)$grantee->EmailAddress, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->URI)) // Group + $acp['acl'][] = array( + 'type' => 'Group', + 'uri' => (string)$grantee->URI, + 'permission' => (string)$grant->Permission + ); + else continue; + } + } + } + return $acp; + } + + + /** + * Delete an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return boolean + */ + public static function deleteObject($bucket, $uri) { + $rest = new S3Request('DELETE', $bucket, $uri); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::deleteObject(): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get a query string authenticated URL + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param integer $lifetime Lifetime in seconds + * @param boolean $hostBucket Use the bucket name as the hostname + * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) + * @return string + */ + public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) { + $expires = time() + $lifetime; + $uri = str_replace('%2F', '/', rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea) + return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', + $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, + urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}"))); + } + + /** + * Get upload POST parameters for form uploads + * + * @param string $bucket Bucket name + * @param string $uriPrefix Object URI prefix + * @param constant $acl ACL constant + * @param integer $lifetime Lifetime in seconds + * @param integer $maxFileSize Maximum filesize in bytes (default 5MB) + * @param string $successRedirect Redirect URL or 200 / 201 status code + * @param array $amzHeaders Array of x-amz-meta-* headers + * @param array $headers Array of request headers or content type as a string + * @param boolean $flashVars Includes additional "Filename" variable posted by Flash + * @return object + */ + public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) { + // Create policy object + $policy = new stdClass; + $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (time() + $lifetime)); + $policy->conditions = array(); + $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj); + $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj); + + $obj = new stdClass; // 200 for non-redirect uploads + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + $obj->success_action_status = (string)$successRedirect; + else // URL + $obj->success_action_redirect = $successRedirect; + array_push($policy->conditions, $obj); + + array_push($policy->conditions, array('starts-with', '$key', $uriPrefix)); + if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', '')); + foreach (array_keys($headers) as $headerKey) + array_push($policy->conditions, array('starts-with', '$'.$headerKey, '')); + foreach ($amzHeaders as $headerKey => $headerVal) { + $obj = new stdClass; $obj->{$headerKey} = (string)$headerVal; array_push($policy->conditions, $obj); + } + array_push($policy->conditions, array('content-length-range', 0, $maxFileSize)); + $policy = base64_encode(str_replace('\/', '/', json_encode($policy))); + + // Create parameters + $params = new stdClass; + $params->AWSAccessKeyId = self::$__accessKey; + $params->key = $uriPrefix.'${filename}'; + $params->acl = $acl; + $params->policy = $policy; unset($policy); + $params->signature = self::__getHash($params->policy); + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + $params->success_action_status = (string)$successRedirect; + else + $params->success_action_redirect = $successRedirect; + foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; + foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; + return $params; + } + + /** + * Create a CloudFront distribution + * + * @param string $bucket Bucket name + * @param boolean $enabled Enabled (true/false) + * @param array $cnames Array containing CNAME aliases + * @param string $comment Use the bucket name as the hostname + * @return array | false + */ + public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = '') { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('POST', '', '2008-06-30/distribution', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontDistributionConfigXML($bucket.'.s3.amazonaws.com', $enabled, $comment, (string)microtime(true), $cnames); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 201) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", '$comment'): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } elseif ($rest->body instanceof SimpleXMLElement) + return self::__parseCloudFrontDistributionConfig($rest->body); + return false; + } + + + /** + * Get CloudFront distribution info + * + * @param string $distributionId Distribution ID from listDistributions() + * @return array | false + */ + public static function getDistribution($distributionId) { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2008-06-30/distribution/'.$distributionId, 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getDistribution($distributionId): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } elseif ($rest->body instanceof SimpleXMLElement) { + $dist = self::__parseCloudFrontDistributionConfig($rest->body); + $dist['hash'] = $rest->headers['hash']; + return $dist; + } + return false; + } + + + /** + * Update a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @return array | false + */ + public static function updateDistribution($dist) { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('PUT', '', '2008-06-30/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontDistributionConfigXML($dist['origin'], $dist['enabled'], $dist['comment'], $dist['callerReference'], $dist['cnames']); + $rest->size = strlen($rest->data); + $rest->setHeader('If-Match', $dist['hash']); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::updateDistribution({$dist['id']}, ".(int)$enabled.", '$comment'): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } else { + $dist = self::__parseCloudFrontDistributionConfig($rest->body); + $dist['hash'] = $rest->headers['hash']; + return $dist; + } + return false; + } + + + /** + * Delete a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @return boolean + */ + public static function deleteDistribution($dist) { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com'); + $rest->setHeader('If-Match', $dist['hash']); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get a list of CloudFront distributions + * + * @return array + */ + public static function listDistributions() { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2008-06-30/distribution', 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::listDistributions(): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) { + $list = array(); + if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) { + //$info['marker'] = (string)$rest->body->Marker; + //$info['maxItems'] = (int)$rest->body->MaxItems; + //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false; + } + foreach ($rest->body->DistributionSummary as $summary) { + $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary); + } + return $list; + } + return array(); + } + + + /** + * Get a DistributionConfig DOMDocument + * + * @internal Used to create XML in createDistribution() and updateDistribution() + * @param string $bucket Origin bucket + * @param boolean $enabled Enabled (true/false) + * @param string $comment Comment to append + * @param string $callerReference Caller reference + * @param array $cnames Array of CNAME aliases + * @return string + */ + private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array()) { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $distributionConfig = $dom->createElement('DistributionConfig'); + $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2008-06-30/'); + $distributionConfig->appendChild($dom->createElement('Origin', $bucket)); + $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference)); + foreach ($cnames as $cname) + $distributionConfig->appendChild($dom->createElement('CNAME', $cname)); + if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment)); + $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false')); + $dom->appendChild($distributionConfig); + return $dom->saveXML(); + } + + + /** + * Parse a CloudFront distribution config + * + * @internal Used to parse the CloudFront DistributionConfig node to an array + * @param object &$node DOMNode + * @return array + */ + private static function __parseCloudFrontDistributionConfig(&$node) { + $dist = array(); + if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName)) { + $dist['id'] = (string)$node->Id; + $dist['status'] = (string)$node->Status; + $dist['time'] = strtotime((string)$node->LastModifiedTime); + $dist['domain'] = (string)$node->DomainName; + } + if (isset($node->CallerReference)) + $dist['callerReference'] = (string)$node->CallerReference; + if (isset($node->Comment)) + $dist['comment'] = (string)$node->Comment; + if (isset($node->Enabled, $node->Origin)) { + $dist['origin'] = (string)$node->Origin; + $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false; + } elseif (isset($node->DistributionConfig)) { + $dist = array_merge($dist, self::__parseCloudFrontDistributionConfig($node->DistributionConfig)); + } + if (isset($node->CNAME)) { + $dist['cnames'] = array(); + foreach ($node->CNAME as $cname) $dist['cnames'][(string)$cname] = (string)$cname; + } + return $dist; + } + + + /** + * Grab CloudFront response + * + * @internal Used to parse the CloudFront S3Request::getResponse() output + * @param object &$rest S3Request instance + * @return object + */ + private static function __getCloudFrontResponse(&$rest) { + $rest->getResponse(); + if ($rest->response->error === false && isset($rest->response->body) && + is_string($rest->response->body) && substr($rest->response->body, 0, 5) == 'response->body = simplexml_load_string($rest->response->body); + // Grab CloudFront errors + if (isset($rest->response->body->Error, $rest->response->body->Error->Code, + $rest->response->body->Error->Message)) { + $rest->response->error = array( + 'code' => (string)$rest->response->body->Error->Code, + 'message' => (string)$rest->response->body->Error->Message + ); + unset($rest->response->body); + } + } + return $rest->response; + } + + + /** + * Get MIME type for file + * + * @internal Used to get mime types + * @param string &$file File path + * @return string + */ + public static function __getMimeType(&$file) { + $type = false; + // Fileinfo documentation says fileinfo_open() will use the + // MAGIC env var for the magic file + if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && + ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) { + if (($type = finfo_file($finfo, $file)) !== false) { + // Remove the charset and grab the last content-type + $type = explode(' ', str_replace('; charset=', ';charset=', $type)); + $type = array_pop($type); + $type = explode(';', $type); + $type = trim(array_shift($type)); + } + finfo_close($finfo); + + // If anyone is still using mime_content_type() + } elseif (function_exists('mime_content_type')) + $type = trim(mime_content_type($file)); + + if ($type !== false && strlen($type) > 0) return $type; + + // Otherwise do it the old fashioned way + static $exts = array( + 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', + 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon', + 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf', + 'zip' => 'application/zip', 'gz' => 'application/x-gzip', + 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain', + 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', + 'css' => 'text/css', 'js' => 'text/javascript', + 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', + 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', + 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', + 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' + ); + $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION)); + return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream'; + } + + + /** + * Generate the auth string: "AWS AccessKey:Signature" + * + * @internal Used by S3Request::getResponse() + * @param string $string String to sign + * @return string + */ + public static function __getSignature($string) { + return 'AWS '.self::$__accessKey.':'.self::__getHash($string); + } + + + /** + * Creates a HMAC-SHA1 hash + * + * This uses the hash extension if loaded + * + * @internal Used by __getSignature() + * @param string $string String to sign + * @return string + */ + private static function __getHash($string) { + return base64_encode(extension_loaded('hash') ? + hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1( + (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . + pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^ + (str_repeat(chr(0x36), 64))) . $string))))); + } + +} + +final class S3Request { + private $verb, $bucket, $uri, $resource = '', $parameters = array(), + $amzHeaders = array(), $headers = array( + 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' + ); + public $fp = false, $size = 0, $data = false, $response; + + + /** + * Constructor + * + * @param string $verb Verb + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return mixed + */ + function __construct($verb, $bucket = '', $uri = '', $defaultHost = 's3.amazonaws.com') { + $this->verb = $verb; + $this->bucket = strtolower($bucket); + $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; + + if ($this->bucket !== '') { + $this->headers['Host'] = $this->bucket.'.'.$defaultHost; + $this->resource = '/'.$this->bucket.$this->uri; + } else { + $this->headers['Host'] = $defaultHost; + //$this->resource = strlen($this->uri) > 1 ? '/'.$this->bucket.$this->uri : $this->uri; + $this->resource = $this->uri; + } + $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); + + $this->response = new STDClass; + $this->response->error = false; + } + + + /** + * Set request parameter + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setParameter($key, $value) { + $this->parameters[$key] = $value; + } + + + /** + * Set request header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setHeader($key, $value) { + $this->headers[$key] = $value; + } + + + /** + * Set x-amz-meta-* header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setAmzHeader($key, $value) { + $this->amzHeaders[$key] = $value; + } + + + /** + * Get the S3 response + * + * @return object | false + */ + public function getResponse() { + $query = ''; + if (sizeof($this->parameters) > 0) { + $query = substr($this->uri, -1) !== '?' ? '?' : '&'; + foreach ($this->parameters as $var => $value) + if ($value == null || $value == '') $query .= $var.'&'; + // Parameters should be encoded (thanks Sean O'Dea) + else $query .= $var.'='.rawurlencode($value).'&'; + $query = substr($query, 0, -1); + $this->uri .= $query; + + if (array_key_exists('acl', $this->parameters) || + array_key_exists('location', $this->parameters) || + array_key_exists('torrent', $this->parameters) || + array_key_exists('logging', $this->parameters)) + $this->resource .= $query; + } + $url = ((S3::$useSSL && extension_loaded('openssl')) ? + 'https://':'http://').$this->headers['Host'].$this->uri; + //var_dump($this->bucket, $this->uri, $this->resource, $url); + + // Basic setup + $curl = curl_init(); + curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); + + if (S3::$useSSL) { + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1); + } + + curl_setopt($curl, CURLOPT_URL, $url); + + // Headers + $headers = array(); $amz = array(); + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + foreach ($this->headers as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + + // Collect AMZ headers for signature + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value; + + // AMZ headers must be sorted + if (sizeof($amz) > 0) { + sort($amz); + $amz = "\n".implode("\n", $amz); + } else $amz = ''; + + // Authorization string (CloudFront stringToSign should only contain a date) + $headers[] = 'Authorization: ' . S3::__getSignature( + $this->headers['Host'] == 'cloudfront.amazonaws.com' ? $this->headers['Date'] : + $this->verb."\n".$this->headers['Content-MD5']."\n". + $this->headers['Content-Type']."\n".$this->headers['Date'].$amz."\n".$this->resource + ); + + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); + curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); + curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback')); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + + // Request types + switch ($this->verb) { + case 'GET': break; + case 'PUT': case 'POST': // POST only used for CloudFront + if ($this->fp !== false) { + curl_setopt($curl, CURLOPT_PUT, true); + curl_setopt($curl, CURLOPT_INFILE, $this->fp); + if ($this->size >= 0) + curl_setopt($curl, CURLOPT_INFILESIZE, $this->size); + } elseif ($this->data !== false) { + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); + curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); + if ($this->size >= 0) + curl_setopt($curl, CURLOPT_BUFFERSIZE, $this->size); + } else + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); + break; + case 'HEAD': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD'); + curl_setopt($curl, CURLOPT_NOBODY, true); + break; + case 'DELETE': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + default: break; + } + + // Execute, grab errors + if (curl_exec($curl)) + $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + else + $this->response->error = array( + 'code' => curl_errno($curl), + 'message' => curl_error($curl), + 'resource' => $this->resource + ); + + @curl_close($curl); + + // Parse body into XML + if ($this->response->error === false && isset($this->response->headers['type']) && + $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) { + $this->response->body = simplexml_load_string($this->response->body); + + // Grab S3 errors + if (!in_array($this->response->code, array(200, 204)) && + isset($this->response->body->Code, $this->response->body->Message)) { + $this->response->error = array( + 'code' => (string)$this->response->body->Code, + 'message' => (string)$this->response->body->Message + ); + if (isset($this->response->body->Resource)) + $this->response->error['resource'] = (string)$this->response->body->Resource; + unset($this->response->body); + } + } + + // Clean up file resources + if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp); + + return $this->response; + } + + + /** + * CURL write callback + * + * @param resource &$curl CURL resource + * @param string &$data Data + * @return integer + */ + private function __responseWriteCallback(&$curl, &$data) { + if ($this->response->code == 200 && $this->fp !== false) + return fwrite($this->fp, $data); + else + $this->response->body .= $data; + return strlen($data); + } + + + /** + * CURL header callback + * + * @param resource &$curl CURL resource + * @param string &$data Data + * @return integer + */ + private function __responseHeaderCallback(&$curl, &$data) { + if (($strlen = strlen($data)) <= 2) return $strlen; + if (substr($data, 0, 4) == 'HTTP') + $this->response->code = (int)substr($data, 9, 3); + else { + list($header, $value) = explode(': ', trim($data), 2); + if ($header == 'Last-Modified') + $this->response->headers['time'] = strtotime($value); + elseif ($header == 'Content-Length') + $this->response->headers['size'] = (int)$value; + elseif ($header == 'Content-Type') + $this->response->headers['type'] = $value; + elseif ($header == 'ETag') + $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; + elseif (preg_match('/^x-amz-meta-.*$/', $header)) + $this->response->headers[$header] = is_numeric($value) ? (int)$value : $value; + } + return $strlen; + } + +} diff --git a/cdn/video_s3/includes/amazon_s3.inc b/cdn/video_s3/includes/amazon_s3.inc new file mode 100644 index 0000000..53f5ff9 --- /dev/null +++ b/cdn/video_s3/includes/amazon_s3.inc @@ -0,0 +1,145 @@ +access_key = variable_get('amazon_s3_access_key', ''); + $this->secret_key = variable_get('amazon_s3_secret_access_key',''); + $this->ssl = variable_get('amazon_s3_ssl',FALSE); + $this->limit = variable_get('amazon_s3_limit',5); + $this->bucket = variable_get('amazon_s3_bucket', ''); + } + + public function connect($access_key = '', $secret_key = '', $ssl = FALSE) { + $access_key = $access_key ? $access_key : $this->access_key; + $secret_key = $secret_key ? $secret_key : $this->secret_key; + $ssl = $ssl ? $ssl : $this->ssl; + // Make our connection to Amazon. + $this->s3 = new S3($access_key, $secret_key, $ssl); + } + + /* + * Verifies the existence of a file id, returns the row or false if none found. + */ + public function verify($fid) { + $sql = db_query("SELECT * FROM {video_s3} WHERE fid=%d", $fid); + $row = db_fetch_object($sql); + return $row; + } + + /* + * Gets a video object from the database. + */ + public function get($fid) { + $sql = db_query("SELECT * FROM {video_s3} WHERE fid=%d AND status=%d", $fid, VIDEO_S3_ACTIVE); + $row = db_fetch_object($sql); + return $row; + } + + /* + * Inserts file object into the database. + */ + public function insert($fid) { + db_query("INSERT INTO {video_s3} (fid, status) VALUES (%d, %d)", $fid, VIDEO_S3_PENDING); + } + + /* + * Updates the database after a successful transfer to amazon. + */ + public function update($video) { + $result = db_query("UPDATE {video_s3} SET bucket='%s', filename='%s', filepath='%s', filemime='%s', filesize='%s', status=%d, completed=%d WHERE vid=%d", + $video->bucket, $video->filename, $video->filepath, $video->filemime, $video->filesize, VIDEO_S3_ACTIVE, time(), $video->vid); + return $result; + } + + public function working($vid) { + db_query("UPDATE {video_s3} SET status=%d WHERE vid=%d", VIDEO_S3_WORKING, $vid); + } + + public function failed($vid) { + db_query("UPDATE {video_s3} SET status=%d WHERE vid=%d", VIDEO_S3_FAILED, $vid); + } + + public function delete($fid) { + // Lets get our file no matter the status and delete it. + if($video = $this->verify($fid)) { + if($video->bucket) { + // It has been pushed to amazon so lets remove it. + $this->s3->deleteObject($video->bucket, $video->filename); + } + // Lets delete our record from the database. + db_query("DELETE FROM {video_s3} WHERE vid=%d", $video->vid); + } + } + + /* + * Selects the pending queue to be transfered to amazon. + */ + public function queue() { + $video = false; + $sql = db_query("SELECT vid, fid FROM {video_s3} WHERE status=%d LIMIT %d", VIDEO_S3_PENDING, $this->limit); + while($row = db_fetch_object($sql)) { + // We need to check if this file id exists in our transcoding table. + $sql_video = db_query("SELECT * FROM {video_files} WHERE fid=%d", $row->fid); + if($sql_video_row = db_fetch_object($sql_video)) { + // This is a transcoded file, lets verify it has been transcoded and if so lets push it to amazon. + module_load_include('inc', 'video', '/includes/conversion'); + if($sql_video_row->status == VIDEO_RENDERING_COMPLETE) { + $video = $sql_video_row; + } + } + else { + // This is a regular video file, lets get our file object from the files table and push it to amazon. + $sql_files = db_query("SELECT * FROM {files} WHERE fid=%d", $row->fid); + if($sql_files_row = db_fetch_object($sql_files)) { + $video = $sql_files_row; + } + } + // If we have a video lets go ahead and send it. + if($video) { + // Update our status to working. + $this->working($row->vid); + $filepath = $video->filepath; + $filename = basename($video->filepath); + if ($this->s3->putObjectFile($filepath, $this->bucket, $filename, S3::ACL_PUBLIC_READ)) { + // Update our table. + $video->bucket = $this->bucket; + $video->vid = $row->vid; + $prefix = $this->ssl ? 'https://' : 'http://'; + $video->filepath = $prefix . $video->bucket .'.s3.amazonaws.com/'. $filename; + if($this->update($video)) { + watchdog('amazon_s3', t('Successfully uploaded our file: !file into the bucket %bucket on the Amazon S3 server.', array('!file' => $filepath, '%bucket' => $this->bucket)), array(), WATCHDOG_INFO); + } + } + else { + watchdog('amazon_s3', 'Failed to upload our file to the amazon s3 server.', array(), WATCHDOG_ERROR); + $this->failed($row->vid); + } + } + else { + watchdog('amazon_s3', 'We did not find the file id: '.$row->fid.' or it is still queued for ffmpeg processing.', array(), WATCHDOG_DEBUG); + } + } + } +} \ No newline at end of file diff --git a/cdn/video_s3/video_s3.admin.inc b/cdn/video_s3/video_s3.admin.inc new file mode 100644 index 0000000..ea038a4 --- /dev/null +++ b/cdn/video_s3/video_s3.admin.inc @@ -0,0 +1,134 @@ + 'checkbox', + '#title' => t('Enable Amazon S3.'), + '#default_value' => variable_get('amazon_s3', FALSE), + '#description' => t('If you would like to use Amazon S3 to store and serve up your video files enable this and fill out your access keys.'), + ); + $form['amazon_s3_ssl'] = array( + '#type' => 'checkbox', + '#title' => t('Enable SSL?'), + '#default_value' => variable_get('amazon_s3_ssl', FALSE), + '#description' => t('If you would like to use ssl when transfering your files enable this option.'), + ); + $form['amazon_s3_access_key'] = array( + '#type' => 'textfield', + '#title' => t('Access Key ID'), + '#default_value' => variable_get('amazon_s3_access_key', ''), + '#size' => 50, + ); + $form['amazon_s3_secret_access_key'] = array( + '#type' => 'password', + '#title' => t('Secret Access Key'), + '#default_value' => variable_get('amazon_s3_secret_access_key', ''), + '#description' => t('Once saved, you do not need to re-enter your secret key. If you need to update your key, then fill this out to update it.'), + '#size' => 50, + ); + //@todo Maybe move this to the admin settings page instead of global? + $form['amazon_s3_bucket'] = array( + '#type' => 'textfield', + '#title' => t('Bucket'), + '#description' => t('Enter the bucket you wish to store your videos in. If the bucket doesn\'t exist the system will attempt to create it.'), + '#default_value' => variable_get('amazon_s3_bucket', ''), + '#size' => 50, + ); + $form['#validate'] = array('video_s3_admin_settings_validate'); + + //lets show our buckets in table format with a delete link. + if(variable_get('amazon_s3', FALSE)) { //@todo add permissions + //were enabled, that means they have successfully connected and created a bucket. + module_load_include('inc', 'video_s3', '/includes/amazon_s3'); + $s3 = new video_amazon_s3; + $s3->connect(); + $buckets = $s3->s3->listBuckets(); + // Setup our header. + $header = array(t('Bucket Name'), t('Total Objects'), t('Actions')); + $rows = array(); + foreach($buckets as $bucket) { + $objects = count($s3->s3->getBucket($bucket)); + $actions = l(t('Delete'), 'admin/settings/video/amazon_s3/bucket/'. $bucket .'/delete'); + $rows[] = array($bucket, $objects, $actions); + } + $form['amazon_info'] = array( + '#type' => 'fieldset', + '#title' => t('Amazon S3 Information'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['amazon_info']['buckets'] = array( + '#type' => 'markup', + '#value' => theme('table', $header, $rows), + ); + } + return system_settings_form($form); +} + +function video_s3_admin_settings_validate(&$form, &$form_state) { + // Check for CURL + if (!extension_loaded('curl') && !@dl(PHP_SHLIB_SUFFIX == 'so' ? 'curl.so' : 'php_curl.dll')) { + form_set_error('amazon_s3', t('The CURL extension is not loaded.')); + } else { + $bucket = $form_state['values']['amazon_s3_bucket']; + // S3 buckets must contain only lower case alphanumeric characters, dots and dashes. + if(!preg_match("/^[a-z.-]+$/", $bucket)) { + form_set_error('amazon_s3_bucket', t('S3 buckets must contain only lower case alphanumeric characters, dots and dashes.')); + } + else { + $access_key = $form_state['values']['amazon_s3_access_key']; + // check our secret key. + if(!empty($form_state['values']['amazon_s3_secret_access_key'])) { + $secret_key = $form_state['values']['amazon_s3_secret_access_key']; + } + else { + // Add our secret key back in to persist its value. + $form_state['values']['amazon_s3_secret_access_key'] = variable_get('amazon_s3_secret_access_key',''); + $secret_key = variable_get('amazon_s3_secret_access_key',''); + } + $ssl = isset($form_state['values']['amazon_s3_ssl']) && $form_state['values']['amazon_s3_ssl'] ? TRUE : FALSE; + // Lets verify our credentials and verify our bucket exists, if not attempt to create it. + module_load_include('inc', 'video_s3', '/includes/amazon_s3'); + $s3 = new video_amazon_s3; + $s3->connect($access_key, $secret_key, $ssl); + $buckets = $s3->s3->listBuckets(); + if ( !$buckets || !(in_array( $bucket, $buckets ))) { + // Create a bucket with public read access + if ($s3->s3->putBucket($bucket, S3::ACL_PUBLIC_READ)) { + drupal_set_message(t('Successfully created the bucket %bucket.', array('%bucket' => $bucket))); + } + else { + form_set_error('amazon_s3_bucket', t('Could not verify or create the bucket %bucket.', array('%bucket' => $bucket))); + } + } + } + } +} + +/* + * Deletes a bucket from your Amazon S3 server. + */ +function video_s3_bucket_delete($bucket) { + module_load_include('inc', 'video_s3', '/includes/amazon_s3'); + $s3 = new video_amazon_s3; + $s3->connect(); + $buckets = $s3->s3->listBuckets(); + if(is_array($buckets) && in_array($bucket, $buckets)) { + if($s3->s3->deleteBucket($bucket)) { + drupal_set_message(t('Successfully deleted the bucket %bucket', array('%bucket' => $bucket))); + } + else { + drupal_set_message(t('Could not delete the bucket %bucket', array('%bucket' => $bucket)), 'error'); + } + } + else { + drupal_set_message(t('The bucket %bucket does not exist for deletion.', array('%bucket' => $bucket)), 'error'); + } + drupal_goto('admin/settings/video/amazon_s3'); +} \ No newline at end of file diff --git a/cdn/video_s3/video_s3.info b/cdn/video_s3/video_s3.info new file mode 100644 index 0000000..57898fc --- /dev/null +++ b/cdn/video_s3/video_s3.info @@ -0,0 +1,8 @@ +;$Id$ + +name = Amazon S3 on Video +description = Leverages the Video module and Amazon Simple Storage Service (Amazon S3) to serve and store your video's saving bandwidth. +package = "Video" +dependencies[] = video +core = 6.x +version = 6.x-4.x-dev \ No newline at end of file diff --git a/cdn/video_s3/video_s3.install b/cdn/video_s3/video_s3.install new file mode 100644 index 0000000..b43734a --- /dev/null +++ b/cdn/video_s3/video_s3.install @@ -0,0 +1,107 @@ + t('Store video s3 cdn'), + 'fields' => array( + 'vid' => array( + 'description' => t('Auto Increment id'), + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'fid' => array( + 'description' => t('Original file id'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'nid' => array( + 'description' => t('Node id'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'bucket' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The bucket the video is stored in.'), + ), + 'filename' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The filename of the video.'), + ), + 'filepath' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The filepath of the video.'), + ), + 'filemime' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The filemime of the video.'), + ), + 'filesize' => array( + 'description' => t('Filesize of the video.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'status' => array( + 'description' => t('Status of the cdn transfer'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'completed' => array( + 'description' => t('Time of successful completion to amazon.'), + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'status' => array('status'), + 'file' => array('fid'), + ), + 'primary key' => array('vid'), + ); + return $schema; +} + +/** + * Implementation of hook_install(). + */ +function video_s3_install() { + drupal_install_schema('video_s3'); +} + +/** + * Implementation of hook_uninstall(). + */ +function video_s3_uninstall() { + drupal_uninstall_schema('video_s3'); + // Delete our variables. + variable_del('amazon_s3'); + variable_del('amazon_s3_ssl'); + variable_del('amazon_s3_access_key'); + variable_del('amazon_s3_secret_access_key'); + variable_del('amazon_s3_bucket'); +} \ No newline at end of file diff --git a/cdn/video_s3/video_s3.module b/cdn/video_s3/video_s3.module new file mode 100644 index 0000000..2355872 --- /dev/null +++ b/cdn/video_s3/video_s3.module @@ -0,0 +1,114 @@ + 'Amazon S3', + 'description' => 'Configure your Amazon S3 settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('video_s3_admin_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'video_s3.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 10, + ); + $items['admin/settings/video/amazon_s3/bucket/%/delete'] = array( + 'title' => 'Delete Bucket', + 'page callback' => 'video_s3_bucket_delete', + 'page arguments' => array(5), + 'access arguments' => array('administer amazon s3'), + 'file' => 'video_s3.admin.inc', + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/* + * Implementation of hook_cron(). + */ +function video_s3_cron() { + module_load_include('inc', 'video_s3', '/includes/amazon_s3'); + $s3 = new video_amazon_s3; + $s3->connect(); + // Lets run our queue. + $s3->queue(); +} + +/** + * Implementation of hook_file_delete(). + */ +function video_s3_file_delete($file) { + module_load_include('inc', 'video_s3', '/includes/amazon_s3'); + $s3 = new video_amazon_s3; + $s3->connect(); + // Lets run our queue. + $s3->delete($file->fid); +} + +/* + * Implementation of hook_form_alter(). + */ +function video_s3_form_alter(&$form, &$form_state, $form_id) { + if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { + $form['buttons']['submit']['#submit'][] = 'video_s3_node_update_submit'; + } +} + +/* + * Submit hanlder to update our s3 table to include the node id. + */ +function video_s3_node_update_submit($form, &$form_state) { + //lets update our video rending table to include the node id created + if (isset($form_state['nid']) && isset($form_state['values']['video_id']) && is_array($form_state['values']['video_id'])) { + foreach($form_state['values']['video_id'] as $fid) { + //lets update our table to include the nid + db_query("UPDATE {video_s3} SET nid=%d WHERE fid=%d", $form_state['nid'], $fid); + } + } +} + +/** + * Implementing hook_video_submit + * @param $element + * @param $form_state + */ +function video_s3_video_submit(&$element, &$form_state) { + $file = $element['#value']; + //we need to check if this fid has already been added to the database AND that there is in fact a fid + if (is_array($file) && isset($file['fid']) && !empty($file['fid'])) { + module_load_include('inc', 'video_s3', '/includes/amazon_s3'); + $s3 = new video_amazon_s3; + $s3->connect(); + // Lets verify that we haven't added this video already. Multiple validation fails will cause this to be ran more than once + if(!$video = $s3->verify($file['fid'])) { + // Video has not been added to the queue yet so lets add it. + $s3->insert($file['fid']); + drupal_set_message(t('Video submission queued for transfer to your Amazon S3 server. Will be there shortly.')); + } + } +} diff --git a/css/video.css b/css/video.css new file mode 100644 index 0000000..f369145 --- /dev/null +++ b/css/video.css @@ -0,0 +1,134 @@ +/* $Id$ */ + +.filefield-element .filefield-file-info, .video_thumbnail { + min-width: 100px; + min-height: 100px; +} + +.filefield-element .imagefield-preview { + min-width: 100px; + min-height: 100px; +} + +.filefield-element .imagefield-text { + max-width: 40em; /* Reflect the maxlength of the ALT attribute (80 characters). */ +} + +.filefield-element .uploadfield-text { + max-width: 40em; +} + +.video_thumbnail { + min-width: 100px; + min-height: 100px; +} + +.uploadfield { + +} + +.uploadfield-nodelink { + +} + +.uploadfield-video-thumb { + +} + +.uploadfield-video-thumb img{ + width : 250px; +/* min-width: 100px; */ + min-height: 210px; + +} + +.uploadfield-video-thumb span{ + /* This is the overlay image for use as a play button. */ + background: url(../images/play.png) no-repeat; + + /* This is the size of our button. DOES NOT WORK WITH DEFAULT THUMB SIZE! */ + width: 50px; + height: 112px; + position : absolute; + margin : 65px 100px; +} + +.uploadfield-video-thumb span:hover { + background-position: -50px 0px; +} + +.video-width-text { + width:10px; +} + +.video-thumb-selection{ + +} + +.video-thumb-selection .form-item { + +} + +.video-thumbnails{ +/* display : block;*/ +} +.video-thumbnails .form-item{ + float:left; + margin-right:10px; + max-width:30%; + padding-right:10px; +} + +.video-bypass-auto-conversion{ + +} +.video-default-thumbnail{ + +} + +.video-data{ + +} + +.video_image_teaser { + float: left; + padding: 0.5em; +} + +br.video_image_clear { + clear: both; +} + +.video_image_view { + /* inser here rules for node page image */ +} + +.video-ffmpeg-helper-inprogress { + border: 1px solid red; + padding: .5em; +} + +/* override filefield entry */ +.widget-edit{ + max-width:70%; +} + +.widget-edit .form-item { white-space: normal !important; } + +.admin_flv_player_wrapper { display: none; } +/* + * VIDEO OJBECT FIXES CROSS BROWSER + */ +* html object.video-object { + display: none; +} + +* html object.video-object/**/ { + display: inline; +} + +* html object.video-object { + display/**/: none; +} + + \ No newline at end of file diff --git a/includes/conversion.inc b/includes/conversion.inc new file mode 100644 index 0000000..3f1165e --- /dev/null +++ b/includes/conversion.inc @@ -0,0 +1,262 @@ +select_queue()) { + foreach ($videos as $video) { + $this->process($video); + } + } + } + + /** + * Select videos from our queue + * + * @return + * An array containing all the videos to be proccessed. + */ + private function select_queue() { + $total_videos = variable_get('video_ffmpeg_instances', 5); + $videos = array(); + $result = db_query_range('SELECT f.*, vf.vid, vf.nid, vf.dimensions, vf.status as video_status FROM {video_files} vf LEFT JOIN {files} f ON vf.fid = f.fid WHERE vf.status = %d AND f.status = %d ORDER BY f.timestamp', + VIDEO_RENDERING_PENDING, FILE_STATUS_PERMANENT, 0, $total_videos); + + while ($row = db_fetch_object($result)) { + $videos[] = $row; + } + return $videos; + } + + /** + * Process the video through ffmpeg. + * + * @param $video + * This can either be the file object or the file id (fid) + * + * @return + * TRUE of FALSE if video was converted successfully. + */ + public function process($video) { + if (is_object($video) && isset($video->fid)) { + $return = $this->render($video); + } + else { + $video_object = $this->load_video($video); + $return = $this->render($video_object); + } + return $return; + } + + private function render($video) { + if (!is_object($video)) { + watchdog('video_conversion', 'Video object is not present', array(), WATCHDOG_ERROR); + return FALSE; + } + // Make sure this video is pending or do nothing. + if($video->video_status == VIDEO_RENDERING_PENDING) { + // This will update our current video status to active. + $this->change_status($video->vid, VIDEO_RENDERING_ACTIVE); + // Get the converted file object + //we are going to move our video to an "original" folder + //we are going to transcode the video to the "converted" folder + $pathinfo = pathinfo($video->filepath); + $original = $pathinfo['dirname'] .'/original'; + $converted = $pathinfo['dirname'] .'/converted'; + + if (!field_file_check_directory($original, FILE_CREATE_DIRECTORY)) { + watchdog('video_transcoder', 'Video conversion failed. Could not create the directory: '.$orginal, array(), WATCHDOG_ERROR); + return false; + } + if (!field_file_check_directory($converted, FILE_CREATE_DIRECTORY)) { + watchdog('video_transcoder', 'Video conversion failed. Could not create the directory: '.$converted, array(), WATCHDOG_ERROR); + return false; + } + + $original = $original .'/'. $video->filename; + //lets move our video and then convert it. + if(file_move($video, $original)) { + //update our filename after the move to maintain filename uniqueness. + $converted = $converted .'/'. pathinfo($video->filepath, PATHINFO_FILENAME) .'.'. $this->video_extension(); + // Update our filepath since we moved it + $update = drupal_write_record('files', $video, 'fid'); + //call our transcoder + $command_output = $this->convert_video($video, $converted); + //lets check to make sure our file exists, if not error out + if(!file_exists($converted) || !filesize($converted)) { + watchdog('video_conversion', 'Video conversion failed. FFMPEG reported the following output: '.$command_output, array(), WATCHDOG_ERROR); + $this->change_status($video->vid, VIDEO_RENDERING_FAILED); + return FALSE; + } + // Setup our converted video object + $video_info = pathinfo($converted); + //update our converted video + $video->converted = new stdClass(); + $video->converted->vid = $video->vid; + $video->converted->filename = $video_info['basename']; + $video->converted->filepath = $converted; + $video->converted->filemime = file_get_mimetype($converted); + $video->converted->filesize = filesize($converted); + $video->converted->status = VIDEO_RENDERING_COMPLETE; + $video->converted->completed = time(); + //clear our cache so our video path is updated. + cache_clear_all('*', 'cache_content', true); + // Update our video_files table with the converted video information. + $result = db_query("UPDATE {video_files} SET filename='%s', filepath='%s', filemime='%s', filesize=%d, status=%d, completed=%d WHERE vid=%d", + $video->converted->filename, $video->converted->filepath, $video->converted->filemime, $video->converted->filesize, $video->converted->status, $video->converted->completed, $video->converted->vid); + + // Update our node id to published. We do not do a node_load as it causes editing problems when saving. + db_query("UPDATE {node} SET status=%d WHERE nid=%d", 1, $video->nid); + watchdog('video_conversion', 'Successfully converted %orig to %dest', array('%orig' => $video->filepath, '%dest' => $video->converted->filepath), WATCHDOG_INFO); + return TRUE; + } + else { + watchdog('video_conversion', 'Cound not move the video to the original folder.', array(), WATCHDOG_ERROR); + $this->change_status($video->vid, VIDEO_RENDERING_FAILED); + return FALSE; + } + } + return NULL; + } + + /** + * Calls the transcoder class to convert the video. + * + * @param $job + * Video object to be transcoded + * + * @return + * TRUE or FALSE + */ + private function convert_video($video, $converted) { + //get our dimensions and pass them along. + $dimensions = $this->dimensions($video); + module_load_include('inc', 'video', '/includes/transcoder'); + $transcoder = new video_transcoder; + return $transcoder->convert_video($video, $converted, $dimensions); + } + + private function video_extension() { + module_load_include('inc', 'video', '/includes/transcoder'); + $transcoder = new video_transcoder; + return $transcoder->video_converted_extension(); + } + + /* + * Function determines the dimensions you want and compares with the actual wxh of the video. + * + * If they are not exact or the aspect ratio does not match, we then figure out how much padding + * we should add. We will either add a black bar on the top/bottom or on the left/right. + * + * @TODO I need to look more at this function. I don't really like the guess work here. Need to implement + * a better way to check the end WxH. Maybe compare the final resolution to our defaults? I don't think + * that just checking to make sure the final number is even is accurate enough. + */ + public function dimensions($video) { + //lets setup our dimensions. Make sure our aspect ratio matches the dimensions to be used, if not lets add black bars. + $aspect_ratio = _video_aspect_ratio($video->filepath); + $ratio = $aspect_ratio['ratio']; + $width = $aspect_ratio ['width']; + $height = $aspect_ratio['height']; + + $wxh = explode('x', $video->dimensions); + $output_width = $wxh[0]; + $output_height = $wxh[1]; + $output_ratio = number_format($output_width / $output_height, 4); + + if($output_ratio != $ratio && $width && $height) { + $options = array(); + // Figure out our black bar padding. + if ($ratio < $output_width / $output_height) { + $end_width = $output_height * $ratio; + $end_height = $output_height; + } + else { + $end_height = $output_width / $ratio; + $end_width = $output_width; + } + + // We need to get back to an even resolution and maybe compare with our defaults? + // @TODO Make this more exact on actual video dimensions instead of making sure the wxh are even numbers + + if ($end_width == $output_width) { + // We need to pad the top/bottom of the video + $padding = round($output_height - $end_height); + $pad1 = $pad2 = floor($padding / 2); + if ($pad1 %2 !== 0) { + $pad1++; + $pad2--; + } + $options[] = '-padtop '. $pad1; + $options[] = '-padbottom '. $pad2; + } + else { + // We are padding the left/right of the video. + $padding = round($output_width - $end_width); + $pad1 = $pad2 = floor($padding / 2); //@todo does padding need to be an even number? + if ($pad1 %2 !== 0) { + $pad1++; + $pad2--; + } + $options[] = '-padleft '. $pad1; + $options[] = '-padright '. $pad2; + } + + $end_width = round($end_width) %2 !==0 ? round($end_width) + 1 : round($end_width); + $end_height = round($end_height) %2 !==0 ? round($end_height) + 1 : round($end_height); + //add our size to the beginning to make sure it hits our -s + array_unshift($options, $end_width .'x'. $end_height); + return implode(' ', $options); + } + else { + return $video->dimensions; + } + } + + /** + * Load a file based on the file id ($fid) + * + * @param $fid + * Integer of the file id to be loaded. + */ + public function load_video($fid) { + $result = db_query('SELECT f.*, vf.vid, vf.nid, vf.dimensions, vf.status as video_status FROM {video_files} vf LEFT JOIN {files} f ON vf.fid = f.fid WHERE f.fid=vf.fid AND f.fid = %d', $fid); + return db_fetch_object($result); + } + + /** + * Load a converted video based on the file id ($fid) + * + * @todo: Need to figure something out here for multiple files (HTML 5) + * @param $fid + * Integer of the file id to be loaded. + */ + public function load_converted_video($fid) { + $result = db_query('SELECT * FROM {video_files} WHERE fid = %d', $fid); + return db_fetch_object($result); + } + + /** + * Change the status of the file. + * + * @param (int) $vid + * @param (int) $status + */ + public function change_status($vid, $status) { + $result = db_query('UPDATE {video_files} SET status = %d WHERE vid = %d ', $status, $vid); + } +} +?> \ No newline at end of file diff --git a/includes/metadata.inc b/includes/metadata.inc new file mode 100644 index 0000000..905a7fa --- /dev/null +++ b/includes/metadata.inc @@ -0,0 +1,57 @@ +params['cmd_path'] = variable_get('video_metadata_path', $this->meta_command_path); + } + + public function run_command($options) { + $command = $this->nice .' '. $this->params['cmd_path'].' '.$options.' 2>&1'; + watchdog('video_metadata', 'Executing command: '. $command, array(), WATCHDOG_DEBUG); + ob_start(); + passthru($command, $command_return); + $output = ob_get_contents(); + ob_end_clean(); + return $output; + } + + public function process($video) { + $command_output = $this->run_command($this->meta_command .' '. $video); + return $command_output; + } + + + public function admin_settings() { + $form = array(); + $form['video_metadata'] = array( + '#type' => 'checkbox', + '#title' => t('Enable Metadata'), + '#default_value' => variable_get('video_metadata', FALSE), + '#description' => t('Enables metadata for videos by using flvtool2. It cuts FLV files and adds cue Points (onCuePoint). If you are converting your files to FLV then this is highly recommended.'), + ); + $form['video_metadata_path'] = array( + '#type' => 'textfield', + '#title' => t('Path to FLVTool2'), + '#description' => t('Absolute path to flvtool2.'), + '#default_value' => variable_get('video_metadata_path', $this->meta_command_path), + ); + $form['video_metadata_dimensions'] = array( + '#type' => 'textarea', + '#title' => t('Selectable Dimensions when uploading videos.'), + '#description' => t('Enter one dimension per line as Video Resolutions. Each resolution must be in the form of WxH where W=Width and H=Height in pixels. Example dimensions are 1280x720.'), + '#default_value' => variable_get("video_metadata_dimensions", video_default_dimensions()), + ); + return $form; + } +} \ No newline at end of file diff --git a/includes/transcoder.inc b/includes/transcoder.inc new file mode 100644 index 0000000..96b9f57 --- /dev/null +++ b/includes/transcoder.inc @@ -0,0 +1,96 @@ +transcoder = new $transcoder; + } + else { + drupal_set_message(t('The transcoder is not configured properly.'), 'error'); + } + } + + public function generate_thumbnails($video) { + return $this->transcoder->generate_thumbnails($video); + } + + public function convert_video($video, $converted, $dimensions) { + $output = $this->transcoder->convert_video($video, $converted, $dimensions); + // If they are using metadata. + if (variable_get('video_metadata', FALSE)) { + module_load_include('inc', 'video', '/includes/metadata'); + $metadata = new video_metadata; + $metadata->process($converted); + } + return $output; + } + + public function admin_settings() { + $form = array(); + $options = $this->_transcoders(); + $form['vid_convertor'] = array( + '#type' => 'radios', + '#title' => t('Video transcoder'), + '#default_value' => variable_get('vid_convertor', 'video_ffmpeg'), + '#options' => $options['radios'], + '#description' => t('Selecting a video transcoder will help you convert videos and generate thumbnails. !list', array('!list' => theme('item_list', $options['help']))), + '#prefix' => '
', + '#suffix' => '
', + ); + $form = $form + $options['admin_settings']; + return $form; + } + + private function _transcoders() { + // Lets find our transcoder classes and build our radio options + // We do this by scanning our transcoders folder + $form = array('radios' => array(), 'help' => array(), 'admin_settings' => array()); + $path = drupal_get_path('module', 'video') .'/transcoders'; + $files = file_scan_directory($path, '^.*\.inc$'); + foreach($files as $file) { + module_load_include('inc', 'video', '/transcoders/' . $file->name); + $focus = new $file->name; + $form['radios'][$focus->get_value()] = $focus->get_name(); + $form['help'][] = $focus->get_help(); + $form['admin_settings'] = $form['admin_settings'] + $focus->admin_settings(); + } + //we need to move our video/thumbnail fieldsets to the bottom of our form as they are used for each trancoder + $autothumb = $form['admin_settings']['autothumb']; + $autoconv = $form['admin_settings']['autoconv']; + unset($form['admin_settings']['autothumb'], $form['admin_settings']['autoconv']); + $form['admin_settings']['autothumb'] = $autothumb; + $form['admin_settings']['autoconv'] = $autoconv; + return $form; + } + + public function get_dimensions($video) { + return $this->transcoder->get_dimensions($video); + } + + public function video_converted_extension() { + return $this->transcoder->video_converted_extension(); + } +} + +interface transcoder_interface { + public function run_command($command); + public function generate_thumbnails($video); + public function convert_video($video, $converted, $dimensions); + public function get_playtime($video); + public function get_name(); + public function get_value(); + public function get_help(); + public function admin_settings(); +} \ No newline at end of file diff --git a/includes/video_helper.inc b/includes/video_helper.inc new file mode 100644 index 0000000..3eaa5d1 --- /dev/null +++ b/includes/video_helper.inc @@ -0,0 +1,116 @@ +fid = $element['#item']['fid']; + $video->original = $element['#item']; + $video->filepath = $element['#item']['filepath']; + $video->url = file_create_url($element['#item']['filepath']); + $video->extension = pathinfo($element['#item']['filename'], PATHINFO_EXTENSION); + $video->width = trim($dimensions[0]); + $video->height = trim($dimensions[1]); + $video->player_width = trim($player_dimensions[0]); + $video->player_height = trim($player_dimensions[1]); + $video->thumbnail = $this->thumbnail_object($element); + $video->formatter = $element['#formatter']; + $video->autoplay = variable_get('video_autoplay', TRUE); + $video->autobuffering = variable_get('video_autobuffering', TRUE); + $video->theora_player = variable_get('video_ogg_player', 'http://theora.org/cortado.jar'); + + // TODO : add hook_video_load API to load videos + // Lets find out if we have pushed this file to the cdn if enabled. + $cdn = false; + if(variable_get('amazon_s3', FALSE)) { + module_load_include('inc', 'video_s3', '/includes/amazon_s3'); + $s3 = new video_amazon_s3; + if($amazon = $s3->get($video->fid)) { + $cdn = true; + // Fix our filepath + $video->filepath = $amazon->filepath; + $video->url = $amazon->filepath; + $video->extension = pathinfo($amazon->filepath, PATHINFO_EXTENSION); + } + } + // If no cdn, lets find out if we have transcoded this file and update our paths. + if(!$cdn) { + //lets find out if we are overriding this video with a converted one. + if (isset($field['widget']['autoconversion']) && $field['widget']['autoconversion'] && !$element['#item']['data']['bypass_autoconversion']) { + module_load_include('inc', 'video', '/includes/conversion'); + $conversion = new video_conversion; + $converted = $conversion->load_converted_video($video->fid); + $video->filepath = $converted->filepath; + $video->url = file_create_url($converted->filepath); + $video->extension = pathinfo($converted->filepath, PATHINFO_EXTENSION); + } + } + // Moved to last to recheck incase we changed our extension above. + $video->flash_player = variable_get('video_extension_'.$video->extension.'_flash_player', ''); + + // Return our object + return $video; + } + + public function thumbnail_object($element) { + $field = content_fields($element['#field_name'], $element['#type_name']); + // Build our thumbnail object + $thumbnail = new stdClass(); + $thumbnail->filepath = ''; + $thumbnail->url = ''; + //@todo future enhancements for our thumbnails + $thumbnail->alt = ''; + $thumbnail->title = ''; + $thumbnail->description = ''; + + // Setup our thumbnail path. + $use_default_img = $element['#item']['data']['use_default_video_thumb']; + if ($use_default_img && !empty($field['widget']['default_video_thumb']['filepath'])) { + $thumbnail->filepath = $field['widget']['default_video_thumb']['filepath']; + } + elseif ($element['#item']['data']['video_thumb']) { + $thumbnail->filepath = $element['#item']['data']['video_thumb']; + } + else { + //need some type of default if nothing is present + //drupal_set_message(t('No thumbnail has been configured for the video.'), 'error'); + } + //lets check for an imagecache preset + if (isset($element['imagecache_preset'])) { + $thumbnail->url = imagecache_create_url($element['imagecache_preset'], $thumbnail->filepath); + $thumbnail->filepath = imagecache_create_path($element['imagecache_preset'], $thumbnail->filepath); + } else { + $thumbnail->url = file_create_url($thumbnail->filepath); + } + + //swftools appends sites/default/files to the front of our path... + //@todo Is this a setting? Need to figure this out. + $thumbnail->swfthumb = $thumbnail->filepath; + // Return our object + return $thumbnail; + } +} \ No newline at end of file diff --git a/js/flowplayer-3.2.0.min.js b/js/flowplayer-3.2.0.min.js new file mode 100644 index 0000000..2baa001 --- /dev/null +++ b/js/flowplayer-3.2.0.min.js @@ -0,0 +1,24 @@ +/* + * flowplayer.js 3.2.0. The Flowplayer API + * + * Copyright 2009 Flowplayer Oy + * + * This file is part of Flowplayer. + * + * Flowplayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Flowplayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Flowplayer. If not, see . + * + * Date: 2010-05-03 20:23:59 +0000 (Mon, 03 May 2010) + * Revision: 468 + */ +(function(){function g(o){console.log("$f.fireEvent",[].slice.call(o))}function k(q){if(!q||typeof q!="object"){return q}var o=new q.constructor();for(var p in q){if(q.hasOwnProperty(p)){o[p]=k(q[p])}}return o}function m(t,q){if(!t){return}var o,p=0,r=t.length;if(r===undefined){for(o in t){if(q.call(t[o],o,t[o])===false){break}}}else{for(var s=t[0];p1){var t=arguments[1],q=(arguments.length==3)?arguments[2]:{};if(typeof t=="string"){t={src:t}}t=i({bgcolor:"#000000",version:[9,0],expressInstall:"http://static.flowplayer.org/swf/expressinstall.swf",cachebusting:true},t);if(typeof o=="string"){if(o.indexOf(".")!=-1){var s=[];m(n(o),function(){s.push(new b(this,k(t),k(q)))});return new d(s)}else{var r=c(o);return new b(r!==null?r:o,t,q)}}else{if(o){return new b(o,t,q)}}}return null};i(window.$f,{fireEvent:function(){var o=[].slice.call(arguments);var q=$f(o[0]);return q?q._fireEvent(o.slice(1)):null},addPlugin:function(o,p){b.prototype[o]=p;return $f},each:m,extend:i});if(typeof jQuery=="function"){jQuery.fn.flowplayer=function(q,p){if(!arguments.length||typeof arguments[0]=="number"){var o=[];this.each(function(){var r=$f(this);if(r){o.push(r)}});return arguments.length?o[arguments[0]]:new d(o)}return this.each(function(){$f(this,k(q),p?k(p):{})})}}})();(function(){var h=document.all,j="http://www.adobe.com/go/getflashplayer",c=typeof jQuery=="function",e=/(\d+)[^\d]+(\d+)[^\d]*(\d*)/,b={width:"100%",height:"100%",id:"_"+(""+Math.random()).slice(9),allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:[3,0],onFail:null,expressInstall:null,w3c:false,cachebusting:false};if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}})}function i(l,f){if(f){for(key in f){if(f.hasOwnProperty(key)){l[key]=f[key]}}}return l}function a(f,n){var m=[];for(var l in f){if(f.hasOwnProperty(l)){m[l]=n(f[l])}}return m}window.flashembed=function(f,m,l){if(typeof f=="string"){f=document.getElementById(f.replace("#",""))}if(!f){return}if(typeof m=="string"){m={src:m}}return new d(f,i(i({},b),m),l)};var g=i(window.flashembed,{conf:b,getVersion:function(){var f;try{f=navigator.plugins["Shockwave Flash"].description.slice(16)}catch(n){try{var l=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");f=l&&l.GetVariable("$version")}catch(m){}}f=e.exec(f);return[f[1],f[3]]},asString:function(l){if(l===null||l===undefined){return null}var f=typeof l;if(f=="object"&&l.push){f="array"}switch(f){case"string":l=l.replace(new RegExp('(["\\\\])',"g"),"\\$1");l=l.replace(/^\s?(\d+\.?\d+)%/,"$1pct");return'"'+l+'"';case"array":return"["+a(l,function(o){return g.asString(o)}).join(",")+"]";case"function":return'"function()"';case"object":var m=[];for(var n in l){if(l.hasOwnProperty(n)){m.push('"'+n+'":'+g.asString(l[n]))}}return"{"+m.join(",")+"}"}return String(l).replace(/\s/g," ").replace(/\'/g,'"')},getHTML:function(o,l){o=i({},o);var n=''}o.width=o.height=o.id=o.w3c=o.src=null;o.onFail=o.version=o.expressInstall=null;for(var m in o){if(o[m]){n+=''}}var p="";if(l){for(var f in l){if(l[f]){var q=l[f];p+=f+"="+(/function|object/.test(typeof q)?g.asString(q):q)+"&"}}p=p.slice(0,-1);n+='"}n+="";return n},isSupported:function(f){return k[0]>f[0]||k[0]==f[0]&&k[1]>=f[1]}});var k=g.getVersion();function d(f,n,m){if(g.isSupported(n.version)){f.innerHTML=g.getHTML(n,m)}else{if(n.expressInstall&&g.isSupported([6,65])){f.innerHTML=g.getHTML(i(n,{src:n.expressInstall}),{MMredirectURL:location.href,MMplayerType:"PlugIn",MMdoctitle:document.title})}else{if(!f.innerHTML.replace(/\s/g,"")){f.innerHTML="

Flash version "+n.version+" or greater is required

"+(k[0]>0?"Your version is "+k:"You have no flash plugin installed")+"

"+(f.tagName=="A"?"

Click here to download latest version

":"

Download latest version from here

");if(f.tagName=="A"){f.onclick=function(){location.href=j}}}if(n.onFail){var l=n.onFail.call(this);if(typeof l=="string"){f.innerHTML=l}}}}if(h){window[n.id]=document.getElementById(n.id)}i(this,{getRoot:function(){return f},getOptions:function(){return n},getConf:function(){return m},getApi:function(){return f.firstChild}})}if(c){jQuery.tools=jQuery.tools||{version:"3.2.0"};jQuery.tools.flashembed={conf:b};jQuery.fn.flashembed=function(l,f){return this.each(function(){$(this).data("flashembed",flashembed(this,l,f))})}}})(); \ No newline at end of file diff --git a/js/jquery.media.js b/js/jquery.media.js new file mode 100644 index 0000000..dcffa3c --- /dev/null +++ b/js/jquery.media.js @@ -0,0 +1,458 @@ +/* + * jQuery Media Plugin for converting elements into rich media content. + * + * Examples and documentation at: http://malsup.com/jquery/media/ + * Copyright (c) 2007-2008 M. Alsup + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * @author: M. Alsup + * @version: 0.92 (24-SEP-2009) + * @requires jQuery v1.1.2 or later + * $Id$ + * + * Supported Media Players: + * - Flash + * - Quicktime + * - Real Player + * - Silverlight + * - Windows Media Player + * - iframe + * + * Supported Media Formats: + * Any types supported by the above players, such as: + * Video: asf, avi, flv, mov, mpg, mpeg, mp4, qt, smil, swf, wmv, 3g2, 3gp + * Audio: aif, aac, au, gsm, mid, midi, mov, mp3, m4a, snd, rm, wav, wma + * Other: bmp, html, pdf, psd, qif, qtif, qti, tif, tiff, xaml + * + * Thanks to Mark Hicken and Brent Pedersen for helping me debug this on the Mac! + * Thanks to Dan Rossi for numerous bug reports and code bits! + * Thanks to Skye Giordano for several great suggestions! + * Thanks to Richard Connamacher for excellent improvements to the non-IE behavior! + */ +;(function($) { + +/** + * Chainable method for converting elements into rich media. + * + * @param options + * @param callback fn invoked for each matched element before conversion + * @param callback fn invoked for each matched element after conversion + */ +$.fn.media = function(options, f1, f2) { + if (options == 'undo') { + return this.each(function() { + var $this = $(this); + var html = $this.data('media.origHTML'); + if (html) + $this.replaceWith(html); + }); + } + + return this.each(function() { + if (typeof options == 'function') { + f2 = f1; + f1 = options; + options = {}; + } + var o = getSettings(this, options); + // pre-conversion callback, passes original element and fully populated options + if (typeof f1 == 'function') f1(this, o); + + var r = getTypesRegExp(); + var m = r.exec(o.src.toLowerCase()) || ['']; + + o.type ? m[0] = o.type : m.shift(); + for (var i=0; i < m.length; i++) { + fn = m[i].toLowerCase(); + if (isDigit(fn[0])) fn = 'fn' + fn; // fns can't begin with numbers + if (!$.fn.media[fn]) + continue; // unrecognized media type + // normalize autoplay settings + var player = $.fn.media[fn+'_player']; + if (!o.params) o.params = {}; + if (player) { + var num = player.autoplayAttr == 'autostart'; + o.params[player.autoplayAttr || 'autoplay'] = num ? (o.autoplay ? 1 : 0) : o.autoplay ? true : false; + } + var $div = $.fn.media[fn](this, o); + + $div.css('backgroundColor', o.bgColor).width(o.width); + + if (o.canUndo) { + var $temp = $('
').append(this); + $div.data('media.origHTML', $temp.html()); // store original markup + } + + // post-conversion callback, passes original element, new div element and fully populated options + if (typeof f2 == 'function') f2(this, $div[0], o, player.name); + break; + } + }); +}; + +/** + * Non-chainable method for adding or changing file format / player mapping + * @name mapFormat + * @param String format File format extension (ie: mov, wav, mp3) + * @param String player Player name to use for the format (one of: flash, quicktime, realplayer, winmedia, silverlight or iframe + */ +$.fn.media.mapFormat = function(format, player) { + if (!format || !player || !$.fn.media.defaults.players[player]) return; // invalid + format = format.toLowerCase(); + if (isDigit(format[0])) format = 'fn' + format; + $.fn.media[format] = $.fn.media[player]; + $.fn.media[format+'_player'] = $.fn.media.defaults.players[player]; +}; + +// global defautls; override as needed +$.fn.media.defaults = { + standards: false, // use object tags only (no embeds for non-IE browsers) + canUndo: true, // tells plugin to store the original markup so it can be reverted via: $(sel).mediaUndo() + width: 400, + height: 400, + autoplay: 0, // normalized cross-player setting + bgColor: '#ffffff', // background color + params: { wmode: 'transparent'}, // added to object element as param elements; added to embed element as attrs + attrs: {}, // added to object and embed elements as attrs + flvKeyName: 'file', // key used for object src param (thanks to Andrea Ercolino) + flashvars: {}, // added to flash content as flashvars param/attr + flashVersion: '7', // required flash version + expressInstaller: null, // src for express installer + + // default flash video and mp3 player (@see: http://jeroenwijering.com/?item=Flash_Media_Player) + flvPlayer: 'mediaplayer.swf', + mp3Player: 'mediaplayer.swf', + + // @see http://msdn2.microsoft.com/en-us/library/bb412401.aspx + silverlight: { + inplaceInstallPrompt: 'true', // display in-place install prompt? + isWindowless: 'true', // windowless mode (false for wrapping markup) + framerate: '24', // maximum framerate + version: '0.9', // Silverlight version + onError: null, // onError callback + onLoad: null, // onLoad callback + initParams: null, // object init params + userContext: null // callback arg passed to the load callback + } +}; + +// Media Players; think twice before overriding +$.fn.media.defaults.players = { + flash: { + name: 'flash', + title: 'Flash', + types: 'flv,mp3,swf', + mimetype: 'application/x-shockwave-flash', + pluginspage: 'http://www.adobe.com/go/getflashplayer', + ieAttrs: { + classid: 'clsid:d27cdb6e-ae6d-11cf-96b8-444553540000', + type: 'application/x-oleobject', + codebase: 'http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=' + $.fn.media.defaults.flashVersion + } + }, + quicktime: { + name: 'quicktime', + title: 'QuickTime', + mimetype: 'video/quicktime', + pluginspage: 'http://www.apple.com/quicktime/download/', + types: 'aif,aiff,aac,au,bmp,gsm,mov,mid,midi,mpg,mpeg,mp4,m4a,psd,qt,qtif,qif,qti,snd,tif,tiff,wav,3g2,3gp', + ieAttrs: { + classid: 'clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B', + codebase: 'http://www.apple.com/qtactivex/qtplugin.cab' + } + }, + realplayer: { + name: 'real', + title: 'RealPlayer', + types: 'ra,ram,rm,rpm,rv,smi,smil', + mimetype: 'audio/x-pn-realaudio-plugin', + pluginspage: 'http://www.real.com/player/', + autoplayAttr: 'autostart', + ieAttrs: { + classid: 'clsid:CFCDAA03-8BE4-11cf-B84B-0020AFBBCCFA' + } + }, + winmedia: { + name: 'winmedia', + title: 'Windows Media', + types: 'asx,asf,avi,wma,wmv', + mimetype: $.browser.mozilla && isFirefoxWMPPluginInstalled() ? 'application/x-ms-wmp' : 'application/x-mplayer2', + pluginspage: 'http://www.microsoft.com/Windows/MediaPlayer/', + autoplayAttr: 'autostart', + oUrl: 'url', + ieAttrs: { + classid: 'clsid:6BF52A52-394A-11d3-B153-00C04F79FAA6', + type: 'application/x-oleobject' + } + }, + // special cases + iframe: { + name: 'iframe', + types: 'html,pdf' + }, + silverlight: { + name: 'silverlight', + types: 'xaml' + } +}; + +// +// everything below here is private +// + + +// detection script for FF WMP plugin (http://www.therossman.org/experiments/wmp_play.html) +// (hat tip to Mark Ross for this script) +function isFirefoxWMPPluginInstalled() { + var plugs = navigator.plugins; + for (var i = 0; i < plugs.length; i++) { + var plugin = plugs[i]; + if (plugin['filename'] == 'np-mswmp.dll') + return true; + } + return false; +} + +var counter = 1; + +for (var player in $.fn.media.defaults.players) { + var types = $.fn.media.defaults.players[player].types; + $.each(types.split(','), function(i,o) { + if (isDigit(o[0])) o = 'fn' + o; + $.fn.media[o] = $.fn.media[player] = getGenerator(player); + $.fn.media[o+'_player'] = $.fn.media.defaults.players[player]; + }); +}; + +function getTypesRegExp() { + var types = ''; + for (var player in $.fn.media.defaults.players) { + if (types.length) types += ','; + types += $.fn.media.defaults.players[player].types; + }; + return new RegExp('\\.(' + types.replace(/,/ig,'|') + ')\\b$'); +}; + +function getGenerator(player) { + return function(el, options) { + return generate(el, options, player); + }; +}; + +function isDigit(c) { + return '0123456789'.indexOf(c) > -1; +}; + +// flatten all possible options: global defaults, meta, option obj +function getSettings(el, options) { + options = options || {}; + var $el = $(el); + var cls = el.className || ''; + // support metadata plugin (v1.0 and v2.0) + var meta = $.metadata ? $el.metadata() : $.meta ? $el.data() : {}; + meta = meta || {}; + var w = meta.width || parseInt(((cls.match(/w:(\d+)/)||[])[1]||0)); + var h = meta.height || parseInt(((cls.match(/h:(\d+)/)||[])[1]||0)); + + if (w) meta.width = w; + if (h) meta.height = h; + if (cls) meta.cls = cls; + + var a = $.fn.media.defaults; + var b = options; + var c = meta; + + var p = { params: { bgColor: options.bgColor || $.fn.media.defaults.bgColor } }; + var opts = $.extend({}, a, b, c); + $.each(['attrs','params','flashvars','silverlight'], function(i,o) { + opts[o] = $.extend({}, p[o] || {}, a[o] || {}, b[o] || {}, c[o] || {}); + }); + + if (typeof opts.caption == 'undefined') opts.caption = $el.text(); + + // make sure we have a source! + opts.src = opts.src || $el.attr('href') || $el.attr('src') || 'unknown'; + return opts; +}; + +// +// Flash Player +// + +// generate flash using SWFObject library if possible +$.fn.media.swf = function(el, opts) { + if (!window.SWFObject && !window.swfobject) { + // roll our own + if (opts.flashvars) { + var a = []; + for (var f in opts.flashvars) + a.push(f + '=' + opts.flashvars[f]); + if (!opts.params) opts.params = {}; + opts.params.flashvars = a.join('&'); + } + return generate(el, opts, 'flash'); + } + + var id = el.id ? (' id="'+el.id+'"') : ''; + var cls = opts.cls ? (' class="' + opts.cls + '"') : ''; + var $div = $(''); + + // swfobject v2+ + if (window.swfobject) { + $(el).after($div).appendTo($div); + if (!el.id) el.id = 'movie_player_' + counter++; + + // replace el with swfobject content + swfobject.embedSWF(opts.src, el.id, opts.width, opts.height, opts.flashVersion, + opts.expressInstaller, opts.flashvars, opts.params, opts.attrs); + } + // swfobject < v2 + else { + $(el).after($div).remove(); + var so = new SWFObject(opts.src, 'movie_player_' + counter++, opts.width, opts.height, opts.flashVersion, opts.bgColor); + if (opts.expressInstaller) so.useExpressInstall(opts.expressInstaller); + + for (var p in opts.params) + if (p != 'bgColor') so.addParam(p, opts.params[p]); + for (var f in opts.flashvars) + so.addVariable(f, opts.flashvars[f]); + so.write($div[0]); + } + + if (opts.caption) $('
').appendTo($div).html(opts.caption); + return $div; +}; + +// map flv and mp3 files to the swf player by default +$.fn.media.flv = $.fn.media.mp3 = function(el, opts) { + var src = opts.src; + var player = /\.mp3\b/i.test(src) ? $.fn.media.defaults.mp3Player : $.fn.media.defaults.flvPlayer; + var key = opts.flvKeyName; + src = encodeURIComponent(src); + opts.src = player; + opts.src = opts.src + '?'+key+'=' + (src); + var srcObj = {}; + srcObj[key] = src; + opts.flashvars = $.extend({}, srcObj, opts.flashvars ); + return $.fn.media.swf(el, opts); +}; + +// +// Silverlight +// +$.fn.media.xaml = function(el, opts) { + if (!window.Sys || !window.Sys.Silverlight) { + if ($.fn.media.xaml.warning) return; + $.fn.media.xaml.warning = 1; + alert('You must include the Silverlight.js script.'); + return; + } + + var props = { + width: opts.width, + height: opts.height, + background: opts.bgColor, + inplaceInstallPrompt: opts.silverlight.inplaceInstallPrompt, + isWindowless: opts.silverlight.isWindowless, + framerate: opts.silverlight.framerate, + version: opts.silverlight.version + }; + var events = { + onError: opts.silverlight.onError, + onLoad: opts.silverlight.onLoad + }; + + var id1 = el.id ? (' id="'+el.id+'"') : ''; + var id2 = opts.id || 'AG' + counter++; + // convert element to div + var cls = opts.cls ? (' class="' + opts.cls + '"') : ''; + var $div = $(''); + $(el).after($div).remove(); + + Sys.Silverlight.createObjectEx({ + source: opts.src, + initParams: opts.silverlight.initParams, + userContext: opts.silverlight.userContext, + id: id2, + parentElement: $div[0], + properties: props, + events: events + }); + + if (opts.caption) $('
').appendTo($div).html(opts.caption); + return $div; +}; + +// +// generate object/embed markup +// +function generate(el, opts, player) { + var $el = $(el); + var o = $.fn.media.defaults.players[player]; + + if (player == 'iframe') { + var o = $(''); + o.attr('src', opts.src); + o.css('backgroundColor', o.bgColor); + } + else if ($.browser.msie) { + var a = [''); + var p = ['']; + for (var key in opts.params) + p.push(''); + var o = document.createElement(a.join('')); + for (var i=0; i < p.length; i++) + o.appendChild(document.createElement(p[i])); + } + else if (o.standards) { + // Rewritten to be standards compliant by Richard Connamacher + var a = [''); + a.push(''); + for (var key in opts.params) { + if (key == 'wmode' && player != 'flash') // FF3/Quicktime borks on wmode + continue; + a.push(''); + } + // Alternate HTML + a.push('

'+o.title+' Required

'+o.title+' is required to view this media. Download Here.

'); + a.push(''); + } + else { + var a = [''); + } + // convert element to div + var id = el.id ? (' id="'+el.id+'"') : ''; + var cls = opts.cls ? (' class="' + opts.cls + '"') : ''; + var $div = $(''); + $el.after($div).remove(); + ($.browser.msie || player == 'iframe') ? $div.append(o) : $div.html(a.join('')); + if (opts.caption) $('
').appendTo($div).html(opts.caption); + return $div; +}; + + +})(jQuery); diff --git a/js/jquery.metadata.js b/js/jquery.metadata.js new file mode 100644 index 0000000..e011f52 --- /dev/null +++ b/js/jquery.metadata.js @@ -0,0 +1,148 @@ +/* + * Metadata - jQuery plugin for parsing metadata from elements + * + * Copyright (c) 2006 John Resig, Yehuda Katz, J�örn Zaefferer, Paul McLanahan + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id$ + * + */ + +/** + * Sets the type of metadata to use. Metadata is encoded in JSON, and each property + * in the JSON will become a property of the element itself. + * + * There are four supported types of metadata storage: + * + * attr: Inside an attribute. The name parameter indicates *which* attribute. + * + * class: Inside the class attribute, wrapped in curly braces: { } + * + * elem: Inside a child element (e.g. a script tag). The + * name parameter indicates *which* element. + * html5: Values are stored in data-* attributes. + * + * The metadata for an element is loaded the first time the element is accessed via jQuery. + * + * As a result, you can define the metadata type, use $(expr) to load the metadata into the elements + * matched by expr, then redefine the metadata type and run another $(expr) for other elements. + * + * @name $.metadata.setType + * + * @example

This is a p

+ * @before $.metadata.setType("class") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from the class attribute + * + * @example

This is a p

+ * @before $.metadata.setType("attr", "data") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a "data" attribute + * + * @example

This is a p

+ * @before $.metadata.setType("elem", "script") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a nested script element + * + * @example

This is a p

+ * @before $.metadata.setType("html5") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a series of data-* attributes + * + * @param String type The encoding type + * @param String name The name of the attribute to be used to get metadata (optional) + * @cat Plugins/Metadata + * @descr Sets the type of encoding to be used when loading metadata for the first time + * @type undefined + * @see metadata() + */ + +(function($) { + +$.extend({ + metadata : { + defaults : { + type: 'class', + name: 'metadata', + cre: /({.*})/, + single: 'metadata' + }, + setType: function( type, name ){ + this.defaults.type = type; + this.defaults.name = name; + }, + get: function( elem, opts ){ + var settings = $.extend({},this.defaults,opts); + // check for empty string in single property + if ( !settings.single.length ) settings.single = 'metadata'; + + var data = $.data(elem, settings.single); + // returned cached data if it already exists + if ( data ) return data; + + data = "{}"; + + var getData = function(data) { + if(typeof data != "string") return data; + + if( data.indexOf('{') < 0 ) { + data = eval("(" + data + ")"); + } + } + + var getObject = function(data) { + if(typeof data != "string") return data; + + data = eval("(" + data + ")"); + return data; + } + + if ( settings.type == "html5" ) { + var object = {}; + $( elem.attributes ).each(function() { + var name = this.nodeName; + if(name.match(/^data-/)) name = name.replace(/^data-/, ''); + else return true; + object[name] = getObject(this.nodeValue); + }); + } else { + if ( settings.type == "class" ) { + var m = settings.cre.exec( elem.className ); + if ( m ) + data = m[1]; + } else if ( settings.type == "elem" ) { + if( !elem.getElementsByTagName ) return; + var e = elem.getElementsByTagName(settings.name); + if ( e.length ) + data = $.trim(e[0].innerHTML); + } else if ( elem.getAttribute != undefined ) { + var attr = elem.getAttribute( settings.name ); + if ( attr ) + data = attr; + } + object = getObject(data.indexOf("{") < 0 ? "{" + data + "}" : data); + } + + $.data( elem, settings.single, object ); + return object; + } + } +}); + +/** + * Returns the metadata object for the first member of the jQuery object. + * + * @name metadata + * @descr Returns element's metadata object + * @param Object opts An object contianing settings to override the defaults + * @type jQuery + * @cat Plugins/Metadata + */ +$.fn.metadata = function( opts ){ + return $.metadata.get( this[0], opts ); +}; + +})(jQuery); \ No newline at end of file diff --git a/js/video.js b/js/video.js new file mode 100644 index 0000000..e724e8b --- /dev/null +++ b/js/video.js @@ -0,0 +1,84 @@ +// $Id$ + +/** + * @file + * Adds some show/hide to the admin form to make the UXP easier. + * + */ + +$(document).ready(function() { + //lets see if we have any jmedia movies + if($.fn.media) { + $('.jmedia').media(); + } + + video_hide_all_options(); + $("input[name='vid_convertor']").change(function() { + video_hide_all_options(); + }); + + $('.video_select').each(function() { + var ext = $(this).attr('rel'); + $('select', this).change(function() { + if($(this).val() == 'video_play_flv') { + $('#flv_player_'+ext).show(); + } else { + $('#flv_player_'+ext).hide(); + } + }); + if($('select', this).val() == 'video_play_flv') { + $('#flv_player_'+ext).show(); + } + }); + + if(Drupal.settings.video) { + $.fn.media.defaults.flvPlayer = Drupal.settings.video.flvplayer; + + } + + //lets setup our colorbox videos + $('.video-box').each(function() { + var url = $(this).attr('href'); + var data = $(this).metadata(); + var width = data.width; + var height= data.height; + var player = Drupal.settings.video.player; //player can be either jwplayer or flowplayer. + $(this).colorbox({ + html: '', + onComplete:function() { + if(player == 'flowplayer') { + flowplayer("video-overlay", Drupal.settings.video.flvplayer, { + clip: { + autoPlay: Drupal.settings.video.autoplay, + autoBuffering: Drupal.settings.video.autobuffer + } + }); + } else { + $('#video-overlay').media({ flashvars: {autostart: Drupal.settings.video.autoplay}, width:width, height:height }); + } + }, + }); + }); +}); + +function video_hide_all_options() { + $("input[name='vid_convertor']").each(function() { + var id = $(this).val(); + $('#'+id).hide(); + if ($(this).is(':checked')) { + $('#' + id).show(); + } + }); +} + +function videoftp_thumbnail_change() { + // Add handlers for the video thumbnail radio buttons to update the large thumbnail onchange. + $(".video-thumbnails input").each(function() { + var path = $(this).val(); + if($(this).is(':checked')) { + var holder = $(this).attr('rel'); + $('.'+holder+' img').attr('src', Drupal.settings.basePath + path); + } + }); + +} diff --git a/plugins/video_zencoder/includes/LICENSE b/plugins/video_zencoder/includes/LICENSE new file mode 100644 index 0000000..d1f0a51 --- /dev/null +++ b/plugins/video_zencoder/includes/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2010 Zencoder + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/video_zencoder/includes/README.markdown b/plugins/video_zencoder/includes/README.markdown new file mode 100644 index 0000000..0be2256 --- /dev/null +++ b/plugins/video_zencoder/includes/README.markdown @@ -0,0 +1,199 @@ +Zencoder API PHP Library +========================== + +Author: [Steve Heffernan](http://www.steveheffernan.com) (steve (a) zencoder (.) com) +Company: [Zencoder - Online Video Encoder](http://zencoder.com) +Version: 1.1 +Date: 2010-06-04 +Repository: + +For more details on the Zencoder API requirements visit + + + +ENCODING JOB +------------ +The ZencoderJob object creates an encoding job using [cURL](http://zencoder.com/docs/glossary/curl/) +to send [JSON](http://zencoder.com/docs/glossary/json/) formatted parameters to Zencoder's encoding API. + +### Step 1 +Visit the [API builder](https://app.zencoder.com/api_builder) in your account, +and execute a successful encoding job. + +### Step 2 +Copy the successful JSON string, starting with the first curly brace "{", +and pass it as the parameters for a new ZencoderJob object. Execute the script on your server to test that it works. + +#### Example +
+    created) {
+      // Success
+      echo "w00t! \n\n";
+      echo "Job ID: ".$encoding_job->id."\n";
+      echo "Output '".$encoding_job->outputs["web"]->label."' ID: ".$encoding_job->outputs["web"]->id."\n";
+      // Store Job/Output IDs to update their status when notified or to check their progress.
+    } else {
+      // Failed
+      echo "Fail :(\n\n";
+      echo "Errors:\n";
+      foreach($encoding_job->errors as $error) {
+        echo $error."\n";
+      }
+    }
+
+    echo "\nAll Job Attributes:\n";
+    var_dump($encoding_job);
+
+    ?>
+    
+ +### Step 3 +Modify the above script to meet your needs. +Your [API Request History](https://app.zencoder.com/api_requests) may come in handy. +You can revisit your [API builder](https://app.zencoder.com/api_builder) to add/update parameters of the JSON. + +You can translate the JSON string into nested associative arrays so that you can dynamically change attributes like "input". +The previous JSON example would become: + + $encoding_job = new ZencoderJob(array( + "api_key" => "93h630j1dsyshjef620qlkavnmzui3", + "input" => "s3://bucket-name/file-name.avi", + "outputs" => array( + array( + "label" => "web" + ) + ) + )); + + +GENERAL API REQUESTS +-------------------- +A general API request can be used for all API functionality including **Job Listing**, **Job Details**, **Account Creation**, **Account Details** (even Job Creation if desired). See the [API docs](http://zencoder.com/docs/api/) for all possible API requests. +The first argument is the **API URL**. +The second argument is your **API Key**. +The third argument is the **request parameters** if needed. It can either be a JSON string or an array of parameters. + + +#### Example Job List Request + + $request = new ZencoderRequest( + 'https://app.zencoder/api/jobs', + '93h630j1dsyshjef620qlkavnmzui3' + ); + + if ($request->successful) { + print_r($request->results); + } else { + foreach($request->errors as $error) { + echo $error."\n"; + } + } + +#### Example Account Creation Request + + $request = new ZencoderRequest( + 'https://app.zencoder/api/account', + false, // API key isn't needed for new account creation + array( + "terms_of_service" => "1", + "email" => "test@example.com", + "password" => "1234" + ) + ); + + if ($request->successful) { + print_r($request->results); + } else { + foreach($request->errors as $error) { + echo $error."\n"; + } + } + + +NOTIFICATION HANDLING +---------------------- +The ZencoderOutputNotification class is used to capture and parse JSON data sent from +Zencoder to your app when an output file has been completed. + + + +### Step 1 +Create a script to receive notifications, and upload it to a location on your server that is publicly accessible. + +#### Example + output->state == "finished") { + echo "w00t!\n"; + + // If you're encoding to multiple outputs and only care when all of the outputs are finished + // you can check if the entire job is finished. + if($notification->job->state == "finished") { + echo "Dubble w00t!"; + } + } elseif ($notification->output->state == "cancelled") { + echo "Cancelled!\n"; + } else { + echo "Fail!\n"; + echo $notification->output->error_message."\n"; + echo $notification->output->error_link; + } + + ?> + +### Step 2 +In the parameters for an encoding job, add the URL for your script to the notifications array of each output you want to be notified for. +Then submit the job to test if it works. + +**You can view the results at:** + + +#### Example + ... + "outputs" => array( + array( + "label" => "web", + "notifications" => array("http://example.com.com/encoding/notification.php") + ), + array( + "label" => "iPhone", + "notifications" => array("http://example.com.com/encoding/notification.php") + ) + ) + ... + + +### Step 3 +Modify the above script to meet your needs. +Your [notifications page](https://app.zencoder.com/notifications) will come in handy. + +VERSIONS +--------- + Version 1.1 - 2010-06-04 Added General API Requests + Version 1.0 - 2010-04-02 Jobs and Notifications. \ No newline at end of file diff --git a/plugins/video_zencoder/includes/Zencoder.php b/plugins/video_zencoder/includes/Zencoder.php new file mode 100644 index 0000000..df52526 --- /dev/null +++ b/plugins/video_zencoder/includes/Zencoder.php @@ -0,0 +1,239 @@ +encode($value); } + function json_decode($value) { return $GLOBALS['JSON_OBJECT']->decode($value); } +} + +class ZencoderJob { + + var $new_job_url = "https://app.zencoder.com/api/jobs"; //https://app.zencoder.com/api/jobs + //https://zencoder-staging.heroku.com/api/jobs + var $new_job_params = array(); + var $new_job_json; + var $created = false; + var $errors = array(); + + // Attributes + var $id; + var $outputs = array(); + + // Initialize + function ZencoderJob($params, $options = array()) { + + // Build using params if not sending request + if($options["build"]) { + $this->update_attributes($params); + return true; + } + + $this->new_job_params = $params; + $this->created = $this->create(); + } + + // Send Job Request to API + function create() { + // Send request + $request = new ZencoderRequest($this->new_job_url, false, $this->new_job_params); + + if($request->successful) { + $this->update_attributes($request->results); + return true; + } else { + $this->errors = array_merge($this->errors, $request->errors); + return false; + } + } + + // Add/Update attributes on the job object. + function update_attributes($attributes = array()) { + foreach($attributes as $attr_name => $attr_value) { + // Create output file objects + if($attr_name == "outputs" && is_array($attr_value)) { + $this->create_outputs($attr_value); + } elseif (!function_exists($this->$attr_name)) { + $this->$attr_name = $attr_value; + } + } + } + + // Create output file objects from returned parameters. + // Use the Label for the key if avaiable. + function create_outputs($outputs = array()) { + foreach($outputs as $output_attrs) { + if($output_attrs["label"]) { + $this->outputs[$output_attrs["label"]] = new ZencoderOutputFile($output_attrs); + } else { + $this->outputs[] = new ZencoderOutputFile($output_attrs); + } + } + } +} + +class ZencoderOutputFile { + + var $id; + var $label; + var $url; + var $state; + var $error_message; + var $error_link; + + function ZencoderOutputFile($attributes = array()) { + $this->update_attributes($attributes); + } + + // Add/Update attributes on the file object. + function update_attributes($attributes = array()) { + foreach($attributes as $attr_name => $attr_value) { + if(!function_exists($this->$attr_name)) { + $this->$attr_name = $attr_value; + } + } + } +} + +// General API request class +class ZencoderRequest { + + var $successful = false; + var $errors = array(); + var $raw_results; + var $results; + + function ZencoderRequest($url, $api_key = "", $params = "") { + + // Add api_key to url if supplied + if($api_key) { + $url .= "?api_key=".$api_key; + } + + // Get JSON + if(is_string($params)) { + $json = trim($params); + } else if(is_array($params)) { + $json = json_encode($params); + } else { + $json = false; + } + + // Create request + $request = new ZencoderCURL($url, $json); + + // Check for connection errors + if ($request->connected == false) { + $this->errors[] = $request->error; + return; + } + + $status_code = intval($request->status_code); + $this->raw_results = $request->results; + + // Parse returned JSON + $this->results = json_decode($this->raw_results, true); + + // Return based on HTTP status code + if($status_code >= 200 && $status_code <= 206) { + $this->successful = true; + } else { + // Get job request errors if any + if(is_array($this->results["errors"])) { + foreach($this->results["errors"] as $error) { + $this->errors[] = $error; + } + } else { + $this->errors[] = "Unknown Error\n\nHTTP Status Code: ".$request->status_code."\n"."Raw Results: \n".$request->raw_results; + } + } + } +} + +// ZencoderCURL +// The connection class to perform the actual request to the surver +// using cURL http://php.net/manual/en/book.curl.php +class ZencoderCURL { + + var $options = array( + CURLOPT_RETURNTRANSFER => 1, // Return content of the url + CURLOPT_HEADER => 0, // Don't return the header in result + CURLOPT_HTTPHEADER => array("Content-Type: application/json", "Accept: application/json"), + CURLOPT_CONNECTTIMEOUT => 0, // Time in seconds to timeout send request. 0 is no timeout. + CURLOPT_FOLLOWLOCATION => 1, // Follow redirects. + ); + + var $connected; + var $results; + var $status_code; + var $error; + + // Initialize + function ZencoderCURL($url, $json, $options = array()) { + + // Add library details to request + $this->options[CURLOPT_HTTPHEADER][] = "Zencoder-Library-Name: ".ZENCODER_LIBRARY_NAME; + $this->options[CURLOPT_HTTPHEADER][] = "Zencoder-Library-Version: ".ZENCODER_LIBRARY_VERSION; + + // If posting data + if($json) { + $this->options[CURLOPT_POST] = 1; + $this->options[CURLOPT_POSTFIELDS] = $json; + } + + // Add cURL options to defaults (can't use array_merge) + foreach($options as $option_key => $option) { + $this->options[$option_key] = $option; + } + + // Initialize session + $ch = curl_init($url); + + // Set transfer options + curl_setopt_array($ch, $this->options); + + // Execute session and store returned results + $this->results = curl_exec($ch); + + // Store the HTTP status code given (201, 404, etc.) + $this->status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + // Check for cURL error + if (curl_errno($ch)) { + $this->error = 'cURL connection error ('.curl_errno($ch).'): '.htmlspecialchars(curl_error($ch)).' Search'; + $this->connected = false; + } else { + $this->connected = true; + } + + // Close session + curl_close($ch); + } +} + +// Capture incoming notifications from Zencoder to your app +class ZencoderOutputNotification { + + var $output; + var $job; + + function ZencoderOutputNotification($params) { + if($params["output"]) $this->output = new ZencoderOutputFile($params["output"]); + if($params["job"]) $this->job = new ZencoderJob($params["job"], array("build" => true)); + } + + function catch_and_parse() { + $notificiation_data = json_decode(trim(file_get_contents('php://input')), true); + return new ZencoderOutputNotification($notificiation_data); + } +} diff --git a/plugins/video_zencoder/includes/lib/JSON.php b/plugins/video_zencoder/includes/lib/JSON.php new file mode 100644 index 0000000..229799b --- /dev/null +++ b/plugins/video_zencoder/includes/lib/JSON.php @@ -0,0 +1,806 @@ + + * @author Matt Knapp + * @author Brett Stimmerman + * @copyright 2005 Michal Migurski + * @version CVS: $Id$ + * @license http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 + */ + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_SLICE', 1); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_STR', 2); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_ARR', 3); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_OBJ', 4); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_CMT', 5); + +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_LOOSE_TYPE', 16); + +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_SUPPRESS_ERRORS', 32); + +/** + * Converts to and from JSON format. + * + * Brief example of use: + * + * + * // create a new instance of Services_JSON + * $json = new Services_JSON(); + * + * // convert a complexe value to JSON notation, and send it to the browser + * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); + * $output = $json->encode($value); + * + * print($output); + * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] + * + * // accept incoming POST data, assumed to be in JSON notation + * $input = file_get_contents('php://input', 1000000); + * $value = $json->decode($input); + * + */ +class Services_JSON +{ + /** + * constructs a new JSON instance + * + * @param int $use object behavior flags; combine with boolean-OR + * + * possible values: + * - SERVICES_JSON_LOOSE_TYPE: loose typing. + * "{...}" syntax creates associative arrays + * instead of objects in decode(). + * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. + * Values which can't be encoded (e.g. resources) + * appear as NULL instead of throwing errors. + * By default, a deeply-nested resource will + * bubble up with an error, so all return values + * from encode() should be checked with isError() + */ + function Services_JSON($use = 0) + { + $this->use = $use; + } + + /** + * convert a string from one UTF-16 char to one UTF-8 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf16 UTF-16 character + * @return string UTF-8 character + * @access private + */ + function utf162utf8($utf16) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + } + + $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); + + switch(true) { + case ((0x7F & $bytes) == $bytes): + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x7F & $bytes); + + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + function utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + + switch(strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + function encode($var) + { + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + + case 'NULL': + return 'null'; + + case 'integer': + return (int) $var; + + case 'double': + case 'float': + return (float) $var; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\'.$var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4})); + $c += 4; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4}), + ord($var{$c + 5})); + $c += 5; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + + return '"'.$ascii.'"'; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + $properties = array_map(array($this, 'name_value'), + array_keys($var), + array_values($var)); + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + } + + // treat it like a regular array + $elements = array_map(array($this, 'encode'), $var); + + foreach($elements as $element) { + if(Services_JSON::isError($element)) { + return $element; + } + } + + return '[' . join(',', $elements) . ']'; + + case 'object': + $vars = get_object_vars($var); + + $properties = array_map(array($this, 'name_value'), + array_keys($vars), + array_values($vars)); + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + + default: + return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) + ? 'null' + : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + function name_value($name, $value) + { + $encoded_value = $this->encode($value); + + if(Services_JSON::isError($encoded_value)) { + return $encoded_value; + } + + return $this->encode(strval($name)) . ':' . $encoded_value; + } + + /** + * reduce a string by removing leading and trailing comments and whitespace + * + * @param $str string string value to strip of comments and whitespace + * + * @return string string value stripped of comments and whitespace + * @access private + */ + function reduce_string($str) + { + $str = preg_replace(array( + + // eliminate single line comments in '// ...' form + '#^\s*//(.+)$#m', + + // eliminate multi-line comments in '/* ... */' form, at start of string + '#^\s*/\*(.+)\*/#Us', + + // eliminate multi-line comments in '/* ... */' form, at end of string + '#/\*(.+)\*/\s*$#Us' + + ), '', $str); + + // eliminate extraneous space + return trim($str); + } + + /** + * decodes a JSON string into appropriate variable + * + * @param string $str JSON-formatted string + * + * @return mixed number, boolean, string, array, or object + * corresponding to given JSON input string. + * See argument 1 to Services_JSON() above for object-output behavior. + * Note that decode() always returns strings + * in ASCII or UTF-8 format! + * @access public + */ + function decode($str) + { + $str = $this->reduce_string($str); + + switch (strtolower($str)) { + case 'true': + return true; + + case 'false': + return false; + + case 'null': + return null; + + default: + $m = array(); + + if (is_numeric($str)) { + // Lookie-loo, it's a number + + // This would work on its own, but I'm trying to be + // good about returning integers where appropriate: + // return (float)$str; + + // Return float or int, as appropriate + return ((float)$str == (integer)$str) + ? (integer)$str + : (float)$str; + + } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { + // STRINGS RETURNED IN UTF-8 FORMAT + $delim = substr($str, 0, 1); + $chrs = substr($str, 1, -1); + $utf8 = ''; + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c < $strlen_chrs; ++$c) { + + $substr_chrs_c_2 = substr($chrs, $c, 2); + $ord_chrs_c = ord($chrs{$c}); + + switch (true) { + case $substr_chrs_c_2 == '\b': + $utf8 .= chr(0x08); + ++$c; + break; + case $substr_chrs_c_2 == '\t': + $utf8 .= chr(0x09); + ++$c; + break; + case $substr_chrs_c_2 == '\n': + $utf8 .= chr(0x0A); + ++$c; + break; + case $substr_chrs_c_2 == '\f': + $utf8 .= chr(0x0C); + ++$c; + break; + case $substr_chrs_c_2 == '\r': + $utf8 .= chr(0x0D); + ++$c; + break; + + case $substr_chrs_c_2 == '\\"': + case $substr_chrs_c_2 == '\\\'': + case $substr_chrs_c_2 == '\\\\': + case $substr_chrs_c_2 == '\\/': + if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || + ($delim == "'" && $substr_chrs_c_2 != '\\"')) { + $utf8 .= $chrs{++$c}; + } + break; + + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 5; + break; + + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs{$c}; + break; + + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 2); + ++$c; + break; + + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 3); + $c += 2; + break; + + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 4); + $c += 3; + break; + + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 5); + $c += 4; + break; + + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 6); + $c += 5; + break; + + } + + } + + return $utf8; + + } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { + // array, or object notation + + if ($str{0} == '[') { + $stk = array(SERVICES_JSON_IN_ARR); + $arr = array(); + } else { + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = array(); + } else { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = new stdClass(); + } + } + + array_push($stk, array('what' => SERVICES_JSON_SLICE, + 'where' => 0, + 'delim' => false)); + + $chrs = substr($str, 1, -1); + $chrs = $this->reduce_string($chrs); + + if ($chrs == '') { + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } else { + return $obj; + + } + } + + //print("\nparsing {$chrs}\n"); + + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c <= $strlen_chrs; ++$c) { + + $top = end($stk); + $substr_chrs_c_2 = substr($chrs, $c, 2); + + if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { + // found a comma that is not inside a string, array, etc., + // OR we've reached the end of the character list + $slice = substr($chrs, $top['where'], ($c - $top['where'])); + array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); + //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + // we are in an array, so just push an element onto the stack + array_push($arr, $this->decode($slice)); + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + // we are in an object, so figure + // out the property name and set an + // element in an associative array, + // for now + $parts = array(); + + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // "name":value pair + $key = $this->decode($parts[1]); + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // name:value pair, where name is unquoted + $key = $parts[1]; + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } + + } + + } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { + // found a quote, and we are not inside a string + array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); + //print("Found start of string at {$c}\n"); + + } elseif (($chrs{$c} == $top['delim']) && + ($top['what'] == SERVICES_JSON_IN_STR) && + ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) { + // found a quote, we're in a string, and it's not escaped + // we know that it's not escaped becase there is _not_ an + // odd number of backslashes at the end of the string so far + array_pop($stk); + //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '[') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-bracket, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); + //print("Found start of array at {$c}\n"); + + } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { + // found a right-bracket, and we're in an array + array_pop($stk); + //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '{') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-brace, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); + //print("Found start of object at {$c}\n"); + + } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { + // found a right-brace, and we're in an object + array_pop($stk); + //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($substr_chrs_c_2 == '/*') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a comment start, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); + $c++; + //print("Found start of comment at {$c}\n"); + + } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { + // found a comment end, and we're in one now + array_pop($stk); + $c++; + + for ($i = $top['where']; $i <= $c; ++$i) + $chrs = substr_replace($chrs, ' ', $i, 1); + + //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } + + } + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + return $obj; + + } + + } + } + } + + /** + * @todo Ultimately, this should just call PEAR::isError() + */ + function isError($data, $code = null) + { + if (class_exists('pear')) { + return PEAR::isError($data, $code); + } elseif (is_object($data) && (get_class($data) == 'services_json_error' || + is_subclass_of($data, 'services_json_error'))) { + return true; + } + + return false; + } +} + +if (class_exists('PEAR_Error')) { + + class Services_JSON_Error extends PEAR_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + parent::PEAR_Error($message, $code, $mode, $options, $userinfo); + } + } + +} else { + + /** + * @todo Ultimately, this class shall be descended from PEAR_Error + */ + class Services_JSON_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + + } + } + +} + +?> \ No newline at end of file diff --git a/plugins/video_zencoder/includes/zencoder.inc b/plugins/video_zencoder/includes/zencoder.inc new file mode 100644 index 0000000..1269505 --- /dev/null +++ b/plugins/video_zencoder/includes/zencoder.inc @@ -0,0 +1,280 @@ +access_key = variable_get('zencoder_api_key', ''); + $this->limit = variable_get('amazon_s3_limit',5); + $this->bucket = variable_get('amazon_s3_bucket', ''); + } + + + /** + * create transcoding job on Zencoder.com + */ + public function create($file) { + // API Key + $api_key = variable_get('zencoder_api_key', ''); + // File details + $filename = $file->filename; + // Get varialbes + $lable = 'VIDEO_' . $file->fid; + $base_url = $folder; + $width = variable_get('zc_width', ''); + $height = variable_get('zc_height', ''); + $quality = variable_get('zc_quality', 3); + $speed = variable_get('zc_speed', 3); + $upscale = variable_get('zc_upscale', ''); + $stretch = variable_get('zc_stretch', ''); + $frame_rate = variable_get('zc_frame_rate', ''); + $max_frame_rate = variable_get('zc_max_frame_rate', ''); + $keyframe_interval = variable_get('zc_key_frame_interval', ''); + $video_bitrate = variable_get('zc_vid_bit_rate', ''); + $bitrate_cap = variable_get('zc_bit_rate_cap', ''); + $buffer_size = variable_get('zc_buffer_size', ''); + $h264_profile = variable_get('zc_h245_profile', 1); + $h264_level = variable_get('zc_h245_level', 0); + $skip_video = variable_get('zc_skip_video', ''); + $audio_codec = variable_get('zc_audio_codec', 'aac'); + $audio_bitrate = variable_get('zc_audio_bitrate', ''); + $audio_channels = variable_get('zc_audio_channels', 2); + $audio_sample_rate = variable_get('zc_audio_sample_rate', ''); + $skip_audio = variable_get('zc_skip_audio', ''); + $thumb_no = variable_get('no_of_video_thumbs', 5); + // TODO : + $thumb_size = '160x120'; + $thumb_base = $baseurl; + $thumb_prefix = $filename; + $notify_url = variable_get('zc_notify_url', ''); + $notify_email = variable_get('zc_notify_email', 'heshanmw@gmail.com'); + $start_clip = variable_get('zc_start_clip', ''); + $clip_length = variable_get('zc_clip_length', ''); + + $bucket = $this->bucket; + + // Job details + $input_name = $bucket . '/' . $filename; +// watchdog('zencoder', $input_name); + + $zc_output = array(); + if(!empty($lable)) + $zc_output['label'] = $lable; + if(!empty($bucket)) + $zc_output['url'] = 's3://' . $bucket . '/' . $filename . '.flv'; + if(!empty($width)) + $zc_output['width'] = $width; + if(!empty($height)) + $zc_output['height'] = $height; + if($quality != 3) + $zc_output['quality'] = $quality; + if($speed != 3) + $zc_output['speed'] = $speed; + if(!empty($upscale)) + $zc_output['upscale'] = $upscale; + if(!empty($frame_rate)) + $zc_output['frame_rate'] = $frame_rate; + if(!empty($max_frame_rate)) + $zc_output['max_frame_rate'] = $max_frame_rate; + if(!empty($keyframe_interval)) + $zc_output['keyframe_interval'] = $keyframe_interval; + if(!empty($video_bitrate)) + $zc_output['video_bitrate'] = $video_bitrate; + if(!empty($bitrate_cap)) + $zc_output['bitrate_cap'] = $bitrate_cap; + if(!empty($buffer_size)) + $zc_output['buffer_size'] = $buffer_size; + if(($h264_profile != 1)) + $zc_output['h264_profile'] = $h264_profile; + if(($h264_level != 0)) + $zc_output['h264_level'] = $h264_level; + if(!empty($skip_video)) + $zc_output['skip_video'] = $skip_video; + if(($audio_codec != 'aac')) + $zc_output['audio_codec'] = $audio_codec; + if(!empty($audio_bitrate)) + $zc_output['audio_bitrate'] = $audio_bitrate; + if(($audio_channels != 2)) + $zc_output['audio_channels'] = $audio_channels; + if(!empty($audio_sample_rate)) + $zc_output['audio_sample_rate'] = $audio_sample_rate; + if(!empty($skip_audio)) + $zc_output['skip_audio'] = $skip_audio; + if(!empty($start_clip)) + $zc_output['start_clip'] = $start_clip; + if(!empty($clip_length)) + $zc_output['clip_length'] = $clip_length; + +// thumbnails + $thumbnails['number'] = $thumb_no; + if(!empty($thumb_size)) + $thumbnails['thumb_size'] = $thumb_size; + if(!empty($bucket)) + $thumbnails['base_url'] = 's3://' . $bucket; + if(!empty($lable)) + $thumbnails['prefix'] = $lable; + $zc_output['thumbnails'] = $thumbnails; + + // Notifications + if(!empty($notify_url)) + $notifications[] = $notify_url; + if(!empty($notify_email)) + $notifications[] = $notify_email; + $zc_output['notifications'] = $notifications; + + + $encoding_job_json = array( + 'test' => 1, +// 'download_connections' => -1, + 'api_key' => $this->access_key, + 'input' => 's3://' . $input_name . '', + 'output' => $zc_output + ); + + + watchdog('zencoder', json_encode($encoding_job_json)); + + $encoding_job = new ZencoderJob(json_encode($encoding_job_json)); + + // Check if it worked + if ($encoding_job->created) { +// watchdog('zencoder', $encoding_job->id); + return $encoding_job; + } + else { + foreach($encoding_job->errors as $error) { + // echo $error."\n"; + watchdog('zencoder', 'Zencoder reports some errors. !error', array('!error' => $error), WATCHDOG_ERROR); + } + return false; + } + + } + + /* + * Inserts file object into the database. + */ + public function insert($fid, $jobid = 0) { + db_query("INSERT INTO {video_zencoder} (fid, jobid, status) VALUES (%d, %d, %d)", $fid, $jobid, VIDEO_ZC_PENDING); + } + + /* + * Updates the database after a successful transfer to amazon. + */ + public function update($video) { + $result = db_query("UPDATE {video_zencoder} SET jobid = %d, outputid = %d, bucket='%s', filename='%s', filepath='%s', filemime='%s', filesize='%s', status=%d, completed=%d WHERE vid=%d", + $video->jobid, $video->outputid, $video->bucket, $video->filename, $video->filepath, $video->filemime, $video->filesize, VIDEO_ZC_ACTIVE, time(), $video->vid); + return $result; + } + + public function working($vid) { + db_query("UPDATE {video_zencoder} SET status=%d WHERE vid=%d", VIDEO_ZC_WORKING, $vid); + } + + public function failed($vid) { + db_query("UPDATE {video_zencoder} SET status=%d WHERE vid=%d", VIDEO_ZC_FAILED, $vid); + } + /* + * Verifies the existence of a file id, returns the row or false if none found. + */ + public function verify($fid) { + $sql = db_query("SELECT * FROM {video_zencoder} WHERE fid=%d", $fid); + $row = db_fetch_object($sql); + return $row; + } + + public function delete($fid) { + // Lets get our file no matter the status and delete it. + if($video = $this->verify($fid)) { + // Lets delete our record from the database. + db_query("DELETE FROM {video_zencoder} WHERE vid=%d", $video->vid); + } + } + public function get_job_id() { + return $this->jobid; + } + /* + * Gets a video object from the database. + */ + public function get($fid) { + $sql = db_query("SELECT * FROM {video_zencoder} WHERE fid=%d AND status=%d", $fid, VIDEO_ZC_ACTIVE); + $row = db_fetch_object($sql); + return $row; + } + + /* + * Selects the pending queue to be transfered to amazon. + */ + public function queue() { + $video = false; + $sql = db_query("SELECT vid, fid FROM {video_zencoder} WHERE status=%d LIMIT %d", VIDEO_ZC_PENDING, $this->limit); + while($row = db_fetch_object($sql)) { + // We need to check if this file id exists in our S3 table to avoid filenot found error. + $sql_video = db_query("SELECT * FROM {video_s3} WHERE fid=%d", $row->fid); + if($sql_video_row = db_fetch_object($sql_video)) { + // This is a s3 file, lets verify it has been pushed and if so lets push Zencoder queue. + module_load_include('inc', 'video', '/includes/conversion'); + if($sql_video_row->status == VIDEO_ZC_ACTIVE) { + $video = $sql_video_row; + } + } + else { + // This is a regular video file, lets get our file object from the files table and push it to amazon. + $sql_files = db_query("SELECT * FROM {files} WHERE fid=%d", $row->fid); + if($sql_files_row = db_fetch_object($sql_files)) { + $video = $sql_files_row; + } + } + // If we have a video lets go ahead and send it. + if($video) { + // Get Output file info + // API Key + $api_key = variable_get('zencoder_api_key', ''); + + // Update our status to working. + $this->working($row->vid); + $filepath = $video->filepath; + $filename = basename($video->filepath); + if ($encoding_job = $this->create($video)) { + // Update our table. + $video->bucket = $this->bucket; + $video->filename = $filename . '.flv'; + $video->filemime = 'video/x-flv'; + $video->vid = $row->vid; + $video->jobid = $encoding_job->id; + $video->outputid = $encoding_job->outputs["VIDEO" . $video->fid]->id; + $prefix = $this->ssl ? 'https://' : 'http://'; + $video->filepath = $prefix . $video->bucket .'.s3.amazonaws.com/'. $filename . '.flv'; + if($this->update($video)) { + watchdog('zencoder', t('Successfully created trancoding job on file: !file into the bucket %bucket on Zencoder.', array('!file' => $filepath, '%bucket' => $this->bucket)), array(), WATCHDOG_INFO); + } + } + else { + watchdog('zencoder', 'Failed to queus our file to Zencoder.', array(), WATCHDOG_ERROR); + $this->failed($row->vid); + } + } + else { + watchdog('zencoder', 'We did not find the file id: '.$row->fid.' or it is still queued for ffmpeg processing or S3 push.', array(), WATCHDOG_DEBUG); + } + } + } +} \ No newline at end of file diff --git a/plugins/video_zencoder/video_zencoder.admin.inc b/plugins/video_zencoder/video_zencoder.admin.inc new file mode 100644 index 0000000..7dcfa7f --- /dev/null +++ b/plugins/video_zencoder/video_zencoder.admin.inc @@ -0,0 +1,302 @@ + 'fieldset', + '#title' => t('Zencoder API'), + '#collapsible' => FALSE, + '#collapsed' => FALSE, + ); +// $form['amazon_info']['zencoder_username'] = array( +// '#type' => 'textfield', +// '#title' => t('Your email address'), +// '#default_value' => variable_get('zencoder_username', $user->mail), +// '#size' => 50, +// '#description' => t('Make sure the email is accurate, since we will send all the password details and API key details to this.') +// ); +// $form['amazon_info']['agree_terms_zencoder'] = array( +// '#type' => 'checkbox', +// '#title' => t('Agree Zencoder Terms and Conditions.'), +// '#default_value' => variable_get('agree_terms_zencoder', TRUE), +// '#description' => t('Read terms and conditions on !link.', array('!link' => l(t('Zencoder.com'), 'http://zencoder.com'))), +// ); + $form['zencoder_info']['zencoder_api_key'] = array( + '#type' => 'textfield', + '#title' => t('Zencoder API Key'), + '#default_value' => variable_get('zencoder_api_key', ''), + '#size' => 50, + '#description' => t('Zencoder API Key if you don\'t have API key click here to generate one.') + ); + + $form['zencoder'] = array( + '#type' => 'fieldset', + '#title' => t("Zencoder Settings"), + '#collapsed' => false, + '#description' => t('All output settings are optional. We choose sensible defaults for web and iPhone playback.') + ); + + // Basic Video Settings + $form['zencoder']['basic_video'] = array( + '#type' => 'fieldset', + '#title' => t("Basic Video Settings"), + '#collapsed' => true, + '#collapsible' => true, + '#description' => t('') + ); + + $form['zencoder']['basic_video']['zc_width'] = array( + '#type' => 'textfield', + '#title' => t('Video width'), + '#default_value' => variable_get('zc_width', ''), + '#description' => t('Width of the converted video, of the format 600x400. This is the maximum width of the output video specified as a positive integer. In order for video compression to work properly the width should be divisible by 4 (or even better 16).'), + '#size' => 12, + ); + $form['zencoder']['basic_video']['zc_height'] = array( + '#type' => 'textfield', + '#title' => t('Video height'), + '#default_value' => variable_get('zc_height', ''), + '#description' => t('Width of the converted video, of the format 600x400. This is the maximum height of the output video specified as a positive integer. In order for video compression to work properly the height should be divisible by 4 (or even better 16).'), + '#size' => 12, + ); + $quality = array( + 1=>'1 - Poor Quality (Smaller file)', + 2=>2, + 3=>'3 - Default', + 4=>4, + 5=>'5 - High Quality (Larger file)' + ); + $form['zencoder']['basic_video']['zc_quality'] = array( + '#type' => 'select', + '#title' => t('Quality'), + '#options' => $quality, + '#default_value' => variable_get('zc_quality', 3), + '#description' => t('This is the desired output video quality. A higher quality setting will mean higher bitrates and higher file sizes. A quality setting of 5 will be nearly lossless and a setting of 1 will be quite compressed and may not look great. Higher quality encoding is also a bit slower than lower quality encoding. As a rule of thumb, lowering quality by a level will reduce file size by about 40%. A quality setting of 3-4 usually looks pretty good. Note that the actual bitrate will vary when using the quality setting, depending on the type of video. Even at the same quality setting, low-complexity video (like a screencast) will generally result in lower bitrates than high-complexity video (like a movie).'), + ); + $speed = array( + 1=>'1 - Slow (Better Compression)', + 2=>2, + 3=>'3 - Default', + 4=>4, + 5=>'5 - Fast (Worst Compression)' + ); + $form['zencoder']['basic_video']['zc_speed'] = array( + '#type' => 'select', + '#title' => t('Speed'), + '#options' => $speed, + '#default_value' => variable_get('zc_speed', 3), + '#description' => t('This is the desired speed of encoding. A lower speed setting will mean slower encode times, but lower file sizes and better quality video. A high speed setting will transcode quickly, but compression will be less efficient and result in larger files with lower quality output.'), + ); + + // Advaced Video Settings + $form['zencoder']['adv_video'] = array( + '#type' => 'fieldset', + '#title' => t("Advanced Video Settings"), + '#collapsed' => true, + '#collapsible' => true, + '#description' => t('') + ); + + $form['zencoder']['adv_video']['zc_upscale'] = array( + '#type' => 'checkbox', + '#title' => t('Upscale?'), + '#description' => t("If a video is received that is smaller than your maximum frame size (width and height settings), this setting will determine if we will increase the size of the input video to the supplied width and height. For example, if width is set to 640 and the input file is 320x240, the upscale option will force the output video to expand to 640x480. If false, the output video will match the input's original size."), + '#default_value' => variable_get('zc_upscale', ''), + ); + $form['zencoder']['adv_video']['zc_stretch'] = array( + '#type' => 'checkbox', + '#title' => t('Stretch?'), + '#description' => t("If true, the aspect ratio of the original file may be distorted to fit the aspect ratio of the supplied width and height. By default, aspect ratio will always be preserved."), + '#default_value' => variable_get('zc_stretch', ''), + ); + $form['zencoder']['adv_video']['zc_frame_rate'] = array( + '#type' => 'textfield', + '#title' => t('Frame Rate '), + '#default_value' => variable_get('zc_frame_rate', ''), + '#description' => t('The output frame rate to use specified as a decimal number (e.g. 15 or 24.98). Unless you need to target a specific frame rate, you might be better off using Maximum Frame Rate setting. By default, the original frame rate will be preserved.'), + '#size' => 12, + ); + $form['zencoder']['adv_video']['zc_max_frame_rate'] = array( + '#type' => 'textfield', + '#title' => t('Maximum Frame Rate'), + '#default_value' => variable_get('zc_max_frame_rate', ''), + '#description' => t('The maximum frame rate to allow specified as a decimal number (e.g. 15 or 24.98). If the original frame rate is lower than this value, it will not be increased. Otherwise, it will be lowered to the max frame rate. By default, the original frame rate will be preserved.'), + '#size' => 12, + ); + $form['zencoder']['adv_video']['zc_key_frame_interval'] = array( + '#type' => 'textfield', + '#title' => t('Keyframe Interval'), + '#default_value' => variable_get('zc_key_frame_interval', ''), + '#description' => t('By default, a keyframe will be created every 250 frames. Specifying a keyframe interval will allow you to create more or less keyframes in your video. Sometimes the number of keyframes can affect video scrubbing. A greater number of keyframes will increase the size of your output file. Keyframe interval should be specified as a positive integer. For example, a value of 100 will create a keyframe every 100 frames.'), + '#size' => 12, + ); + $form['zencoder']['adv_video']['zc_vid_bit_rate'] = array( + '#type' => 'textfield', + '#title' => t('Video Bitrate'), + '#default_value' => variable_get('zc_vid_bit_rate', ''), + '#description' => t('The target video bitrate specified as kilobits per second (Kbps, e.g. 300 or 500). If you set quality, you don\'t need to set bitrate, unless you want to target a specific bitrate.'), + '#size' => 12, + ); + $form['zencoder']['adv_video']['zc_bit_rate_cap'] = array( + '#type' => 'textfield', + '#title' => t('Bitrate Cap'), + '#default_value' => variable_get('zc_bit_rate_cap', ''), + '#description' => t('A bitrate cap specified as kilobits per second (Kbps, e.g. 300 or 500). Specifying a quality alone will vary the bitrate to match the content of the video potentially introducing high peak bitrates. This setting will prevent peaks in the video bitrate, resulting in a file that looks worse but plays more smoothly. We recommend only using this for streaming or for devices that require it.'), + '#size' => 12, + ); + $form['zencoder']['adv_video']['zc_buffer_size'] = array( + '#type' => 'textfield', + '#title' => t('Buffer Size'), + '#default_value' => variable_get('zc_buffer_size', ''), + '#description' => t('The buffer size specified as kilobits per second (Kbps, e.g. 300 or 500). This is another setting useful for playback on specific devices. Check your streaming server or device settings for tips, or set this to the same as the bitrate cap.'), + '#size' => 12, + ); + $profile = array( + 'baseline' => 'Baseline - Default', + 'main' => 'Main', + 'high' => 'High - Best for web playback' + ); + $form['zencoder']['adv_video']['zc_h245_profile'] = array( + '#type' => 'select', + '#title' => t('H.264 Profile'), + '#options' => $profile, + '#default_value' => variable_get('zc_h245_profile', 1), + '#description' => t('The H.264 standard defines various sets of capabilities, which are referred to as profiles, targeting specific classes of applications. The currently supported profiles are baseline, main, and high. The baseline profile will produce video that looks worse, but requires less CPU on playback and is required for playback on some devices (e.g. iPhone or iPod). The high profile will look best, but can be CPU intensive to playback. By default, the baseline profile will be used to maximize compatibility.'), + ); + $level = array( + '' => '', + '1' => '1', + '1.1' => '1.1', + '1.2' => '1.2', + '1.3' => '1.3', + '2' => '2', + '2.1' => '2.1', + '2.2' => '2.2', + '3' => '3', + '3.1' => '3.1', + '3.2' => '3.2', + '4' => '4', + '4.1' => '4.1', + '4.2' => '4.2', + '5' => '5', + '5.1' => '5.1' + ); + $form['zencoder']['adv_video']['zc_h245_level'] = array( + '#type' => 'select', + '#title' => t('H.264 Level'), + '#options' => $level, + '#default_value' => variable_get('zc_h245_level', 0), + '#description' => t('Similar to profiles, the H.264 standard defines various levels that will limit the size and complexity of the output file. We recommend leaving this blank unless your output device requires it. iPhone video can\'t exceed level 3, Blu-Ray is 4.1, and older iPods max out at 1.3. By default, level 3 will be used to ensure iPhone playback.'), + ); + $form['zencoder']['adv_video']['zc_skip_video'] = array( + '#type' => 'checkbox', + '#title' => t('Skip Video'), + '#description' => t("The video track will be omitted from the output. You can still specify a video format, however, no video track will be present in the resulting file."), + '#default_value' => variable_get('zc_skip_video', ''), + ); + + // Advanced Audio Settings + $form['zencoder']['adv_audio'] = array( + '#type' => 'fieldset', + '#title' => t("Advanced Audio Settings"), + '#collapsed' => true, + '#collapsible' => true, + '#description' => t('') + ); + $audio_codec = array( + 'aac' => 'AAC - Default', + 'mp3' => 'MP3' + ); + $form['zencoder']['adv_audio']['zc_audio_codec'] = array( + '#type' => 'select', + '#title' => t('Audio Codec'), + '#options' => $audio_codec, + '#default_value' => variable_get('zc_audio_codec', 'aac'), + '#description' => t('The audio codec used in the video file can affect the ability to play the video on certain devices. The default codec used is AAC and should only be changed if the playback device you are targeting requires something different.'), + ); + $form['zencoder']['adv_audio']['zc_audio_bitrate'] = array( + '#type' => 'textfield', + '#title' => t('Audio Bitrate'), + '#default_value' => variable_get('zc_audio_bitrate', ''), + '#description' => t('The overall audio bitrate specified as kilobits per second (Kbps, e.g. 96 or 160). This value can\'t exceed 160 Kbps per channel. 96-160 is usually a good range for stereo output.'), + '#size' => 12, + ); + $audio_channel = array( + 1 => 'Mono', + 2 => 'Stereo - Default' + ); + $form['zencoder']['adv_audio']['zc_audio_channels'] = array( + '#type' => 'select', + '#title' => t('Audio Channels'), + '#options' => $audio_channel, + '#default_value' => variable_get('zc_audio_channels', 2), + '#description' => t('By default we will choose the lesser of the number of audio channels in the input file or 2 (stereo).'), + ); + $form['zencoder']['adv_audio']['zc_audio_sample_rate'] = array( + '#type' => 'textfield', + '#title' => t('Audio Sample Rate'), + '#default_value' => variable_get('zc_audio_sample_rate', ''), + '#description' => t('The sample rate of the audio file specified in hertz (e.g. 44100 or 22050). A sample rate of 44100 is the best option for web playback. 22050 and 48000 are also valid options. Warning: the wrong setting may cause encoding problems. By default, 44100 will be used.'), + '#size' => 12, + ); + $form['zencoder']['adv_audio']['zc_skip_audio'] = array( + '#type' => 'checkbox', + '#title' => t('Skip Audio'), + '#description' => t("The audio track will be omitted from the output. You must specify a video format and no audio track will be present in the resulting file."), + '#default_value' => variable_get('zc_skip_audio', ''), + ); + + // Notification Settings + $form['zencoder']['notification'] = array( + '#type' => 'fieldset', + '#title' => t("Notification Settings"), + '#collapsed' => true, + '#collapsible' => true, + '#description' => t('') + ); + $form['zencoder']['notification']['zc_notify_url'] = array( + '#type' => 'textfield', + '#title' => t('URL'), + '#default_value' => variable_get('zc_notify_url', ''), + '#description' => t('We will POST a JSON notification to the URL provided. We will consider the notification successful if we receive a 200 OK response.'), + ); + $form['zencoder']['notification']['zc_notify_email'] = array( + '#type' => 'textfield', + '#title' => t('Email Address'), + '#default_value' => variable_get('zc_notify_email', ''), + '#description' => t('We will send a notification email to the URL provided. The notification will contain basic file status information.'), + ); + + + // Other Settings + $form['zencoder']['other'] = array( + '#type' => 'fieldset', + '#title' => t("Other Settings"), + '#collapsed' => true, + '#collapsible' => true, + '#description' => t('') + ); + $form['zencoder']['other']['zc_start_clip'] = array( + '#type' => 'textfield', + '#title' => t('Start Clip'), + '#default_value' => variable_get('zc_start_clip', ''), + '#description' => t('Creates a subclip from the input file, starting at either a timecode or a number of seconds. Format: 0:01:23.6 or 88.6 for 1 minute and 23.6 seconds.'), + '#size' => 12, + ); + $form['zencoder']['other']['zc_clip_length'] = array( + '#type' => 'textfield', + '#title' => t('Clip Length'), + '#default_value' => variable_get('zc_clip_length', ''), + '#description' => t('Creates a subclip from the input file of the specified length using either a timecode or a number of seconds. Format: 0:00:45.3 or 45.3 for 45.3 seconds.'), + '#size' => 12, + ); + + return system_settings_form($form); +} + diff --git a/plugins/video_zencoder/video_zencoder.info b/plugins/video_zencoder/video_zencoder.info new file mode 100644 index 0000000..7d3bd68 --- /dev/null +++ b/plugins/video_zencoder/video_zencoder.info @@ -0,0 +1,7 @@ +;$Id$ +name = Zencoder API on Video +description = Zencoder is a webservice to transcode videos. +package = "Video" +dependencies[] = video +dependencies[] = video_s3 +core = 6.x \ No newline at end of file diff --git a/plugins/video_zencoder/video_zencoder.install b/plugins/video_zencoder/video_zencoder.install new file mode 100644 index 0000000..31c54a1 --- /dev/null +++ b/plugins/video_zencoder/video_zencoder.install @@ -0,0 +1,116 @@ + t('Store video s3 cdn and convert with zencoder webservice'), + 'fields' => array( + 'vid' => array( + 'description' => t('Auto Increment id'), + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'fid' => array( + 'description' => t('Original file id'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'nid' => array( + 'description' => t('Node id'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'jobid' => array( + 'description' => t('Job id'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'outputid' => array( + 'description' => t('Job Output id'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'bucket' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The bucket the video is stored in.'), + ), + 'filename' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The filename of the video.'), + ), + 'filepath' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The filepath of the video.'), + ), + 'filemime' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The filemime of the video.'), + ), + 'filesize' => array( + 'description' => t('Filesize of the video.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'status' => array( + 'description' => t('Status of the cdn transfer'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'completed' => array( + 'description' => t('Time of successful completion to amazon.'), + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'status' => array('status'), + 'file' => array('fid'), + ), + 'primary key' => array('vid'), + ); + return $schema; +} + +/** + * Implementation of hook_install(). + */ +function video_zencoder_install() { + drupal_install_schema('video_zencoder'); +} + +/** + * Implementation of hook_uninstall(). + */ +function video_zencoder_uninstall() { + drupal_uninstall_schema('video_zencoder'); + // TODO : Delete our variables. +} \ No newline at end of file diff --git a/plugins/video_zencoder/video_zencoder.module b/plugins/video_zencoder/video_zencoder.module new file mode 100644 index 0000000..042b2c3 --- /dev/null +++ b/plugins/video_zencoder/video_zencoder.module @@ -0,0 +1,104 @@ + 'Zencoder', + 'description' => 'Configure your Zencoder API settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('video_zencoder_admin_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'video_zencoder.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 10, + ); + return $items; +} + +/* + * Implementation of hook_cron(). + */ +function video_zencoder_cron() { + module_load_include('inc', 'video_zencoder', '/includes/zencoder'); + $zc = new video_zencoder_api; +// $s3->connect(); +// // Lets run our queue. + $zc->queue(); +} + +/** + * Implementation of hook_file_delete(). + */ +function video_zencoder_file_delete($file) { + module_load_include('inc', 'video_zencoder', '/includes/zencoder'); + $zc = new video_zencoder_api; + // Lets run delete. + $zc->delete($file->fid); +} + +/* + * Implementation of hook_form_alter(). + */ +function video_zencoder_form_alter(&$form, &$form_state, $form_id) { + if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { + $form['buttons']['submit']['#submit'][] = 'video_zencoder_node_update_submit'; + } +} + +/* + * Submit hanlder to update our s3 table to include the node id. + */ +function video_zencoder_node_update_submit($form, &$form_state) { + //lets update our video rending table to include the node id created + if (isset($form_state['nid']) && isset($form_state['values']['video_id']) && is_array($form_state['values']['video_id'])) { + foreach($form_state['values']['video_id'] as $fid) { + //lets update our table to include the nid + db_query("UPDATE {video_zencoder} SET nid=%d WHERE fid=%d", $form_state['nid'], $fid); + } + } +} + +/** + * Implementing hook_video_submit + * @param $element + * @param $form_state + */ +function video_zencoder_video_submit(&$element, &$form_state) { + $file = $element['#value']; + //we need to check if this fid has already been added to the database AND that there is in fact a fid + if (is_array($file) && isset($file['fid']) && !empty($file['fid'])) { + module_load_include('inc', 'video_zencoder', '/includes/zencoder'); + $zc = new video_zencoder_api; + // Lets verify that we haven't added this video already. Multiple validation fails will cause this to be ran more than once + if(!$video = $zc->verify($file['fid'])) { + // Video has not been added to the queue yet so lets add it. + $zc->insert($file['fid'], $jobid); + drupal_set_message(t('Video submission queued for transfer to your Zencoder Transcoding server. Will be there shortly.')); + } + } +} \ No newline at end of file diff --git a/theme/video-play-dcr.tpl.php b/theme/video-play-dcr.tpl.php new file mode 100644 index 0000000..576e52c --- /dev/null +++ b/theme/video-play-dcr.tpl.php @@ -0,0 +1,17 @@ + + + + + l(t('Plugin'), 'http://www.macromedia.com/shockwave/download/'))); ?> + + \ No newline at end of file diff --git a/theme/video-play-divx.tpl.php b/theme/video-play-divx.tpl.php new file mode 100644 index 0000000..e5e7f35 --- /dev/null +++ b/theme/video-play-divx.tpl.php @@ -0,0 +1,20 @@ + + + + + + + l(t('Plugin'), 'http://go.divx.com/plugin/download/'))); ?> + + \ No newline at end of file diff --git a/theme/video-play-flash.tpl.php b/theme/video-play-flash.tpl.php new file mode 100644 index 0000000..a559976 --- /dev/null +++ b/theme/video-play-flash.tpl.php @@ -0,0 +1,20 @@ + + + + + + + l(t('Plugin'), 'http://get.adobe.com/flashplayer/'))); ?> + + \ No newline at end of file diff --git a/theme/video-play-flv.tpl.php b/theme/video-play-flv.tpl.php new file mode 100644 index 0000000..661ad04 --- /dev/null +++ b/theme/video-play-flv.tpl.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/theme/video-play-quicktime.tpl.php b/theme/video-play-quicktime.tpl.php new file mode 100644 index 0000000..7e4fb29 --- /dev/null +++ b/theme/video-play-quicktime.tpl.php @@ -0,0 +1,32 @@ + + * + */ +?> + + + + + + + + + \ No newline at end of file diff --git a/theme/video-play-realmedia.tpl.php b/theme/video-play-realmedia.tpl.php new file mode 100644 index 0000000..8048c5c --- /dev/null +++ b/theme/video-play-realmedia.tpl.php @@ -0,0 +1,22 @@ + + + + + + + + + l(t('Plugin'), 'http://www.real.com/realplayer'))); ?> + + \ No newline at end of file diff --git a/theme/video-play-theora.tpl.php b/theme/video-play-theora.tpl.php new file mode 100644 index 0000000..1b759ee --- /dev/null +++ b/theme/video-play-theora.tpl.php @@ -0,0 +1,23 @@ + + + + + + + + + + + l(t('Plugin'), 'http://www.theora.org/cortado/'))); ?> + \ No newline at end of file diff --git a/theme/video-play-windowsmedia.tpl.php b/theme/video-play-windowsmedia.tpl.php new file mode 100644 index 0000000..c7a1026 --- /dev/null +++ b/theme/video-play-windowsmedia.tpl.php @@ -0,0 +1,21 @@ + + + + + + + + + l(t('Plugin'), 'http://www.microsoft.com/windows/windowsmedia/player/download/'))); ?> + \ No newline at end of file diff --git a/transcoders/video_ffmpeg.inc b/transcoders/video_ffmpeg.inc new file mode 100644 index 0000000..7a52dad --- /dev/null +++ b/transcoders/video_ffmpeg.inc @@ -0,0 +1,312 @@ +params['audiobitrate'] = variable_get('video_ffmpeg_helper_auto_cvr_audio_bitrate', $this->audio_bitrate); + $this->params['videobitrate'] = variable_get('video_ffmpeg_helper_auto_cvr_video_bitrate', $this->video_bitrate); + //@todo: move this to the actual widget and save in video_files table. + $this->params['size'] = variable_get('video_ffmpeg_width', $this->video_width) . 'x' . variable_get('video_ffmpeg_height', $this->video_height); + $this->params['command'] = variable_get('video_ffmpeg_helper_auto_cvr_options', $this->command); + $this->params['cmd_path'] = variable_get('video_transcoder_path', $this->ffmpeg); + $this->params['thumb_command'] = variable_get('video_ffmpeg_thumbnailer_options', $this->thumb_command); + $this->nice = variable_get('video_ffmpeg_nice_enable', TRUE) ? 'nice -n 19' : ''; + $this->params['videoext'] = variable_get('video_ffmpeg_ext', $this->video_ext); + } + + public function run_command($options) { + $command = $this->nice .' '. $this->params['cmd_path'].' '.$options.' 2>&1'; + watchdog('video_ffmpeg', 'Executing command: '. $command, array(), WATCHDOG_DEBUG); + ob_start(); + passthru($command, $command_return); + $output = ob_get_contents(); + ob_end_clean(); + return $output; + } + + public function generate_thumbnails($video) { + global $user; + // Setup our thmbnail path. + $video_thumb_path = variable_get('video_thumb_path', 'video_thumbs'); + $final_thumb_path = file_directory_path(). '/' . $video_thumb_path . '/' . $video['fid']; + + // Ensure the destination directory exists and is writable. + $directories = explode('/', $final_thumb_path); + // Get the file system directory. + $file_system = file_directory_path(); + foreach ($directories as $directory) { + $full_path = isset($full_path) ? $full_path . '/' . $directory : $directory; + // Don't check directories outside the file system path. + if (strpos($full_path, $file_system) === 0) { + field_file_check_directory($full_path, FILE_CREATE_DIRECTORY); + } + } + + // Total thumbs to generate + $total_thumbs = variable_get('no_of_video_thumbs', 5); + $videofile = escapeshellarg($video['filepath']); + //get the playtime from the current transcoder + $duration = $this->get_playtime($video['filepath']); + + $files = NULL; + for($i = 1; $i <= $total_thumbs; $i++) { + $seek = ($duration/$total_thumbs) * $i -1; //adding minus one to prevent seek times equaling the last second of the video + $filename = "/video-thumb-for-$fid-$i.jpg"; + $thumbfile = $final_thumb_path . $filename; + //skip files already exists, this will save ffmpeg traffic + if (!is_file($thumbfile)) { + //setup the command to be passed to the transcoder. + $options = t($this->params['thumb_command'], array('!videofile' => $videofile, '!seek' => $seek, '!thumbfile' => $thumbfile)); + // Generate the thumbnail from the video. + $command_output = $this->run_command($options); + if (!file_exists($thumbfile)) { + $error_param = array('%file' => $thumbfile, '%cmd' => $options, '%out' => $command_output); + $error_msg = t("Error generating thumbnail for video: generated file %file does not exist.
Command Executed:
%cmd
Command Output:
%out", $error_param); + // Log the error message. + watchdog('video_transcoder',$error_msg, array(), WATCHDOG_ERROR); + continue; + } + } + // Begin building the file object. + // @TODO : use file_munge_filename() + $file = new stdClass(); + $file->uid = $user->uid; + $file->status = FILE_STATUS_TEMPORARY; + $file->filename = trim($filename); + $file->filepath = $thumbfile; + $file->filemime = file_get_mimetype($filename); + $file->filesize = filesize($thumbfile); + $file->timestamp = time(); + $files[] = $file; + } + return $files; + } + + public function convert_video($video, $converted, $dimensions) { + // Setup our default command to be run. + $command = t($this->params['command'], array( + '!videofile' => $video->filepath, + '!audiobitrate' => $this->params['audiobitrate'], + '!size' => $dimensions, + '!videobitrate' => $this->params['videobitrate'], + '!convertfile' => $converted + )); + // Process our video + return $this->run_command($command); + } + + public function video_converted_extension() { + return $this->params['videoext']; + } + + /** + * Get some information from the video file + */ + public function get_video_info($video) { + static $command_ouput; + if(!empty($command_output)) return $command_output; + + $file = escapeshellarg($video); + // Execute the command + $options = ' -i ' . $file; + $command_output = $this->run_command($options); + return $command_output; + } + + /** + * Return the playtime seconds of a video + */ + public function get_playtime($video) { + $ffmpeg_output = $this->get_video_info($video); + // Get playtime + $pattern = '/Duration: ([0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9])/'; + preg_match_all($pattern, $ffmpeg_output, $matches, PREG_PATTERN_ORDER); + $playtime = $matches[1][0]; + // ffmpeg return length as 00:00:31.1 Let's get playtime from that + $hmsmm = explode(":", $playtime); + $tmp = explode(".", $hmsmm[2]); + $seconds = $tmp[0]; + $hours = $hmsmm[0]; + $minutes = $hmsmm[1]; + return $seconds + ($hours * 3600) + ($minutes * 60); + } + + /* + * Return the dimensions of a video + */ + public function get_dimensions($video) { + $ffmpeg_output = $this->get_video_info($video); + $res = array('width' => 0,'height' => 0); + // Get dimensions + $regex = ereg ('[0-9]?[0-9][0-9][0-9]x[0-9][0-9][0-9][0-9]?', $ffmpeg_output, $regs ); + if(isset($regs[0])) { + $dimensions = explode("x", $regs[0]); + $res['width'] = $dimensions[0] ? $dimensions[0] : NULL; + $res['height'] = $dimensions[1] ? $dimensions[1] : NULL; + + } + return $res; + } + + /** + * Interface Implementations + * @see sites/all/modules/video/includes/transcoder_interface#get_name() + */ + public function get_name() { + return $this->name; + } + + /** + * Interface Implementations + * @see sites/all/modules/video/includes/transcoder_interface#get_value() + */ + public function get_value() { + return $this->value; + } + + /** + * Interface Implementations + * @see sites/all/modules/video/includes/transcoder_interface#get_help() + */ + public function get_help() { + return l(t('FFMPEG Online Manual'), 'http://www.ffmpeg.org/'); + } + + /** + * Interface Implementations + * @see sites/all/modules/video/includes/transcoder_interface#admin_settings() + */ + public function admin_settings() { + $form = array(); + $form['video_transcoder_path'] = array( + '#type' => 'textfield', + '#title' => t('Path to Video Transcoder'), + '#description' => t('Absolute path to ffmpeg.'), + '#default_value' => variable_get('video_transcoder_path', '/usr/bin/ffmpeg'), + ); + $form['no_of_video_thumbs'] = array( + '#type' => 'textfield', + '#title' => t('Number of thumbnails'), + '#description' => t('Number of thumbnails to display from video.'), + '#default_value' => variable_get('no_of_video_thumbs', 5), + ); + $form['video_ffmpeg_nice_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Enable the use of nice when calling the ffmpeg command.'), + '#default_value' => variable_get('video_ffmpeg_nice_enable', TRUE), + '#description' => t('The nice command Invokes a command with an altered scheduling priority. This option may not be available on windows machines, so disable it.') + ); + // Thumbnail Videos We need to put this stuff last. + $form['autothumb'] = array( + '#type' => 'fieldset', + '#title' => t('Video Thumbnails'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['autothumb']['video_thumb_path'] = array( + '#type' => 'textfield', + '#title' => t('Path to save thumbnails'), + '#description' => t('Path to save video thumbnails extracted from the videos.'), + '#default_value' => variable_get('video_thumb_path', 'video_thumbs'), + ); + $form['autothumb']['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced Settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE + ); + $form['autothumb']['advanced']['video_ffmpeg_thumbnailer_options'] = array( + '#type' => 'textarea', + '#title' => t('Video thumbnailer options'), + '#description' => t('Provide the options for the thumbnailer. Available argument values are: ').'
  1. '.t('!videofile (the video file to thumbnail)').'
  2. '.t('!thumbfile (a newly created temporary file to overwrite with the thumbnail)
'), + '#default_value' => variable_get('video_ffmpeg_thumbnailer_options', '-i !videofile -an -y -f mjpeg -ss !seek -vframes 1 !thumbfile'), + ); + + // Video conversion settings. + $form['autoconv'] = array( + '#type' => 'fieldset', + '#title' => t('Video Conversion'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#prefix' => '
', + '#suffix' => '
' + ); + $form['autoconv']['video_ffmpeg_ext'] = array( + '#type' => 'textfield', + '#title' => t('Video Extension'), + '#size' => 10, + '#maxlength' => 10, + '#default_value' => variable_get('video_ffmpeg_ext', 'flv'), + '#description' => t('The video extensions without the period you want the converted file to contain.') + ); + $form['autoconv']['video_ffmpeg_width'] = array( + '#type' => 'textfield', + '#title' => t('Video Width'), + '#size' => 10, + '#maxlength' => 10, + '#default_value' => variable_get('video_ffmpeg_width', 640), + ); + $form['autoconv']['video_ffmpeg_height'] = array( + '#type' => 'textfield', + '#title' => t('Video height'), + '#size' => 10, + '#maxlength' => 10, + '#default_value' => variable_get('video_ffmpeg_height', 480), + ); + $form['autoconv']['video_ffmpeg_helper_auto_cvr_video_bitrate'] = array( + '#type' => 'textfield', + '#title' => t('Video bitrate'), + '#description' => t('The video bitrate in bit/s of the converted video.'), + '#size' => 10, + '#maxlength' => 10, + '#default_value' => variable_get('video_ffmpeg_helper_auto_cvr_video_bitrate', 200000), + ); + $form['autoconv']['video_ffmpeg_helper_auto_cvr_audio_bitrate'] = array( + '#type' => 'textfield', + '#title' => t('Audio bitrate'), + '#description' => t('The audio bitrate in bit/s of the converted video.'), + '#size' => 10, + '#maxlength' => 10, + '#default_value' => variable_get('video_ffmpeg_helper_auto_cvr_audio_bitrate', 64000), + ); + $form['autoconv']['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced Settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE + ); + $form['autoconv']['advanced']['video_ffmpeg_helper_auto_cvr_options'] = array( + '#type' => 'textarea', + '#title' => t('Video converter options'), + '#description' => t('Provide the ffmpeg options to configure the video conversion. Available argument values are: ').'
    '. + '
  • '.t('!videofile (the video file to convert)'). + '
  • '.t('!convertfile (a newly created file to store the converted file)'). + '
  • '.t('!size (video resolution of the converted file)'). + '
'.t('For further informations refer to the !ffmpegdoc', array('!ffmpegdoc' => l(t('Official FFMpeg documentation.'), 'http://ffmpeg.mplayerhq.hu/ffmpeg-doc.html', array('fragment' => TRUE)))), + '#default_value' => variable_get('video_ffmpeg_helper_auto_cvr_options', '-y -i !videofile -f flv -ar 22050 -ab !audiobitrate -s !size -b !videobitrate -qscale 1 !convertfile'), + ); + return $form; + } +} +?> diff --git a/transcoders/video_ffmpeg_wrapper.inc b/transcoders/video_ffmpeg_wrapper.inc new file mode 100644 index 0000000..8d5861d --- /dev/null +++ b/transcoders/video_ffmpeg_wrapper.inc @@ -0,0 +1,243 @@ +params['thumb_command'] = variable_get('video_ffmpeg_thumbnailer_options', $this->thumb_command); + } + + public function run_command($options) { + watchdog('ffmpeg_wrapper', 'Sending options: '. $options, array(), WATCHDOG_DEBUG); + $output = ffmpeg_wrapper_run_command($options); + if(is_object($output) && ISSET($output->errors)) { + return implode("\n", $output->errors); + } + return $output; + } + + public function generate_thumbnails($video) { + global $user; + // Setup our thmbnail path. + $video_thumb_path = variable_get('video_thumb_path', 'video_thumbs'); + $final_thumb_path = file_directory_path(). '/' . $video_thumb_path . '/' . $video['fid']; + + // Ensure the destination directory exists and is writable. + $directories = explode('/', $final_thumb_path); + // Get the file system directory. + $file_system = file_directory_path(); + foreach ($directories as $directory) { + $full_path = isset($full_path) ? $full_path . '/' . $directory : $directory; + // Don't check directories outside the file system path. + if (strpos($full_path, $file_system) === 0) { + field_file_check_directory($full_path, FILE_CREATE_DIRECTORY); + } + } + + // Total thumbs to generate + $total_thumbs = variable_get('no_of_video_thumbs', 5); + $videofile = escapeshellarg($video['filepath']); + //get the playtime from the current transcoder + $duration = $this->get_playtime($video); + + $files = NULL; + for($i = 1; $i <= $total_thumbs; $i++) { + $seek = ($duration/$total_thumbs) * $i - 1; + $filename = "/video-thumb-for-$fid-$i.jpg"; + $thumbfile = $final_thumb_path . $filename; + //skip files already exists, this will save ffmpeg traffic + if (!is_file($thumbfile)) { + //setup the command to be passed to the transcoder. + $options = t($this->params['thumb_command'], array('!videofile' => $videofile, '!seek' => $seek, '!thumbfile' => $thumbfile)); + // Generate the thumbnail from the video. + $command_output = $this->run_command($options); + if (!file_exists($thumbfile)) { + $error_param = array('%file' => $thumbfile, '%cmd' => $command, '%out' => $command_output); + $error_msg = t("Error generating thumbnail for video: generated file %file does not exist.
Command Executed:
%cmd
Command Output:
%out", $error_param); + // Log the error message. + watchdog('video_transcoder',$error_msg, array(), WATCHDOG_ERROR); + continue; + } + } + // Begin building the file object. + // @TODO : use file_munge_filename() + $file = new stdClass(); + $file->uid = $user->uid; + $file->status = FILE_STATUS_TEMPORARY; + $file->filename = trim($filename); + $file->filepath = $thumbfile; + $file->filemime = file_get_mimetype($filename); + $file->filesize = filesize($thumbfile); + $file->timestamp = time(); + $files[] = $file; + } + return $files; + } + + public function video_converted_extension() { + global $conf; + if(isset($conf['ffmpeg_output_type']) && !empty($conf['ffmpeg_output_type'])) { + return $conf['ffmpeg_output_type']; + } + return $this->video_ext; + } + + public function convert_video($video, $converted, $dimensions) { + $ffmpeg_object = new stdClass(); + // check configuration are pass of then use global $conf + if (empty($params)) { + global $conf; + $params = $conf; + } + + // first error check, make sure that we can decode this kind of file + if (!ffmpeg_wrapper_can_decode($video->filepath)) { + $message = 'FFmpeg Wrapper can not decode this file: !file'; + $variables = array('!file' => l($video->filepath, file_create_url($video->filepath))); + watchdog('video_render', $message, $variables, WATCHDOG_ERROR); + return false; + } + + // did the admin define a specific FFmpeg comand to run? + // we only run what the admin specified + if ($params['ffmpeg_video_custom']) { + $options[] = str_replace(array('%in_file', '%out_file'), array($video->filepath, $converted), $params['ffmpeg_video_custom_command']); + } + // build a standard configuration + else { + // build the ffmpeg command structure out + $options = array(); + // input file + $options[] = "-i '". $video->filepath ."'"; + // build the watermark config + if ($params['ffmpeg_video_wm']) $options[] = "-vhook '". ffmpeg_wrapper_path_to_vhook('watermark.so') ." -f ". $params['ffmpeg_video_wm_file'] ."'"; + // build the audio config + if ($params['ffmpeg_audio_advanced']) { + // use a specifc codec? + if ($params['ffmpeg_audio_acodec']) $options[] = '-acodec '. $params['ffmpeg_audio_acodec']; + // use a specific sample rate? + if ($params['ffmpeg_audio_ar']) $options[] = '-ar '. $params['ffmpeg_audio_ar']; + // use a specific bit rate? + if ($params['ffmpeg_audio_ab']) $options[] = '-ab '. $params['ffmpeg_audio_ab']; + } + + //custom sizing per video + $options[] = '-s '. $dimensions; + + // build the video config + if ($params['ffmpeg_video_advanced']) { + // is codec set? + if ($params['ffmpeg_video_vcodec']) $options[] = '-vcodec '. $params['ffmpeg_video_vcodec']; + // is the bit rate set? + if ($params['ffmpeg_video_br']) $options[] = '-b '. $params['ffmpeg_video_br']; + // is frame rate set? + if ($params['ffmpeg_video_fps']) $options[] = '-r '. $params['ffmpeg_video_fps']; + } + // implement truncating + if ($params['ffmpeg_time_advanced']) $options[] = '-t '. $params['ffmpeg_time']; + // add the output file + $options[] = "'". $converted ."'"; + } + $ffmpeg_object->command = implode(" ", $options); + + // run our command and return the output. + return $this->run_command($ffmpeg_object->command); + } + + /** + * Return the playtime seconds of a video + */ + public function get_playtime($video) { + $ffmpeg_output = ffmpeg_wrapper_get_file_data($video['filepath']); + return $ffmpeg_output['duration']; + } + + /** + * Return the dimensions of a video + */ + public function get_dimensions($video) { + $ffmpeg_output = ffmpeg_wrapper_get_file_data($video); + $res = array('width' => 0,'height' => 0); + if($ffmpeg_output['video']['s']) { + $dimensions = explode("x", $ffmpeg_output['video']['s']); + $res['width'] = $dimensions[0] ? $dimensions[0] : NULL; + $res['height'] = $dimensions[1] ? $dimensions[1] : NULL; + } + return $res; + } + + /** + * Interface Implementations + * @see sites/all/modules/video/includes/transcoder_interface#get_name() + */ + public function get_name() { + return $this->name; + } + + /** + * Interface Implementations + * @see sites/all/modules/video/includes/transcoder_interface#get_value() + */ + public function get_value() { + return $this->value; + } + + /** + * Interface Implementations + * @see sites/all/modules/video/includes/transcoder_interface#get_help() + */ + public function get_help() { + $help = l(t('FFMPEG Wrapper'), 'http://drupal.org/project/ffmpeg_wrapper'); + // If the module isn't loaded, show an error next to the link. + if(!module_exists('ffmpeg_wrapper')) { + $help .= ' '.t('You currently do not have this installed.').''; + } + return $help; + } + + /** + * Interface Implementations + * @see sites/all/modules/video/includes/transcoder_interface#admin_settings() + */ + public function admin_settings() { + global $conf; + $form = array(); + if (!module_exists('ffmpeg_wrapper')) { + $markup = t('You must download and install the !ffmpeg_wrapper to use the FFmpeg Wrapper as a transcoder.', array('!ffmpeg_wrapper' => l(t('FFmpeg Wrapper Module'), 'http://www.drupal.org/project/ffmpeg_wrapper'))); + $form['video_wrapper_info'] = array( + '#type' => 'markup', + '#value' => $markup, + '#prefix' => '
', + '#suffix' => '
', + ); + } + else { + $form['video_ffmpeg_wrapper_start'] = array( + '#type' => 'markup', + '#value' => '
', + ); + if(module_exists('ffmpeg_wrapper_ui')) { + $ffmpeg_admin_form = ffmpeg_wrapper_ui_configuration_form($conf); + } else if (module_exists('ffmpeg_wrapper')) { + $ffmpeg_admin_form = ffmpeg_wrapper_configuration_form($conf); + } + $form = array_merge($form, $ffmpeg_admin_form['ffmpeg_wrapper']); + $form['video_ffmpeg_wrapper_end'] = array( + '#type' => 'markup', + '#value' => '
', + ); + } + return $form; + } +} \ No newline at end of file diff --git a/types/uploadfield/uploadfield.theme.inc b/types/uploadfield/uploadfield.theme.inc new file mode 100644 index 0000000..5a81efd --- /dev/null +++ b/types/uploadfield/uploadfield.theme.inc @@ -0,0 +1,18 @@ + 'videoftp_js', + 'page arguments' => array(2, 3, 4), + //'access callback' => 'videoftp_edit_access', + 'access arguments' => array(3), + 'type' => MENU_CALLBACK, + ); + $items['videoftp/progress'] = array( + 'page callback' => 'videoftp_progress', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Implementation of hook_theme(). + */ +function videoftp_theme() { + $theme = array(); + $theme['videoftp_widget'] = array( + 'arguments' => array('element' => NULL), + 'file' => 'videoftp.theme.inc', + ); + $theme['videoftp_widget_item'] = array( + 'arguments' => array('element' => NULL), + 'file' => 'videoftp.theme.inc', + ); + $theme['videoftp_widget_file'] = array( + 'arguments' => array('element' => NULL), + 'file' => 'videoftp.theme.inc', + ); + return $theme; +} + +/** + * Implementation of CCK's hook_widget_info(). + */ +function videoftp_widget_info() { + return array( + 'videoftp_widget' => array( + 'label' => t('Video FTP'), + 'field types' => array('filefield'), + 'multiple values' => CONTENT_HANDLE_CORE, + 'callbacks' => array('default value' => CONTENT_CALLBACK_CUSTOM), + 'description' => t('Widget allows you to select video files uploaded through FTP to be attached to the node.'), + ), + ); +} + +/** + * Implementation of hook_elements(). + */ +function videoftp_elements() { + return array( + 'videoftp_widget' => array( + '#input' => TRUE, + '#columns' => array('fid', 'list', 'data', 'filepath'), + '#process' => array('videoftp_widget_process'), + '#value_callback' => 'videoftp_widget_value', + ), + ); +} + +/** + * Implementation of CCK's hook_widget_settings(). + */ +function videoftp_widget_settings($op, $widget) { + //load up our include file for the widget + module_load_include('inc', 'videoftp', 'videoftp_widget'); + switch ($op) { + case 'form': + return videoftp_widget_settings_form($widget); + case 'validate': + return videoftp_widget_settings_validate($widget); + case 'save': + return videoftp_widget_settings_save($widget); + } +} + +/** + * Implementation of hook_widget(). + */ +function videoftp_widget(&$form, &$form_state, $field, $items, $delta = NULL) { + $item = array('fid' => 0, 'list' => $field['list_default'], 'data' => array('description' => '', 'video_thumb' => '')); + if (isset($items[$delta])) { + $item = array_merge($item, $items[$delta]); + } + $element = array( + '#title' => $field['widget']['label'], + '#type' => $field['widget']['type'], + '#default_value' => $item, + ); + return $element; +} + +/** + * Menu callback; Shared AHAH callback for ftp file attachment and deletions. + * + * This rebuilds the form element for a particular field item. As long as the + * form processing is properly encapsulated in the widget element the form + * should rebuild correctly using FAPI without the need for additional callbacks + * or processing. + */ +function videoftp_js($type_name, $field_name, $delta) { + module_load_include('inc', 'videoftp', 'videoftp_widget'); + $field = content_fields($field_name, $type_name); + + if (empty($field) || empty($_POST['form_build_id'])) { + // Invalid request. + drupal_set_message(t('An unrecoverable error occurred.'), 'error'); + print drupal_to_js(array('data' => theme('status_messages'))); + exit; + } + + // Build the new form. + $form_state = array('submitted' => FALSE); + $form_build_id = $_POST['form_build_id']; + $form = form_get_cache($form_build_id, $form_state); + + if (!$form) { + // Invalid form_build_id. + drupal_set_message(t('An unrecoverable error occurred. This form was missing from the server cache. Try reloading the page and submitting again.'), 'error'); + print drupal_to_js(array('data' => theme('status_messages'))); + exit; + } + + // Build the form. This calls the file field's #value_callback function and + // saves the uploaded file. Since this form is already marked as cached + // (the #cache property is TRUE), the cache is updated automatically and we + // don't need to call form_set_cache(). + $args = $form['#parameters']; + $form_id = array_shift($args); + $form['#post'] = $_POST; + $form = form_builder($form_id, $form, $form_state); + + // Update the cached form with the new element at the right place in the form. + if (module_exists('fieldgroup') && ($group_name = _fieldgroup_field_get_group($type_name, $field_name))) { + if (isset($form['#multigroups']) && isset($form['#multigroups'][$group_name][$field_name])) { + $form_element = $form[$group_name][$delta][$field_name]; + } + else { + $form_element = $form[$group_name][$field_name][$delta]; + } + } + else { + $form_element = $form[$field_name][$delta]; + } + + if (isset($form_element['_weight'])) { + unset($form_element['_weight']); + } + $output = drupal_render($form_element); + // AHAH is not being nice to us and doesn't know the "other" button (that is, + // either "Attach" or "Delete") yet. Which in turn causes it not to attach + // AHAH behaviours after replacing the element. So we need to tell it first. + + // Loop through the JS settings and find the settings needed for our buttons. + $javascript = drupal_add_js(NULL, NULL); + $videoftp_ahah_settings = array(); + if (isset($javascript['setting'])) { + foreach ($javascript['setting'] as $settings) { + if (isset($settings['ahah'])) { + foreach ($settings['ahah'] as $id => $ahah_settings) { + if (strpos($id, 'videoftp-attach') || strpos($id, 'videoftp-remove')) { + $videoftp_ahah_settings[$id] = $ahah_settings; + } + } + } + } + } + + // Add the AHAH settings needed for our new buttons. + if (!empty($videoftp_ahah_settings)) { + $output .= ''; + } + + $output = theme('status_messages') . $output; + + // For some reason, file uploads don't like drupal_json() with its manual + // setting of the text/javascript HTTP header. So use this one instead. + $GLOBALS['devel_shutdown'] = FALSE; + print drupal_to_js(array('status' => TRUE, 'data' => $output)); + exit; +} \ No newline at end of file diff --git a/types/videoftp/videoftp.theme.inc b/types/videoftp/videoftp.theme.inc new file mode 100644 index 0000000..42203cb --- /dev/null +++ b/types/videoftp/videoftp.theme.inc @@ -0,0 +1,57 @@ +'; + + if ($element['fid']['#value'] != 0) { + $output .= '
'; + $output .= drupal_render($element['preview']); + $output .= '
'; + } + + $output .= '
'; + $output .= drupal_render($element); + $output .= '
'; + $output .= '
'; + + return $output; +} + +/** + * Custom theme function for VideoFTP upload elements. + * + * This function allows us to put the "Attach" button immediately after the + * select field by respecting the #field_suffix property. + */ +function theme_videoftp_widget_file($element) { + $output .= '
'; + if (isset($element['#field_prefix'])) { + $output .= $element['#field_prefix']; + } + $size = $element['#size'] ? ' size="'. $element['#size'] .'"' : ''; + _form_set_class($element, array('form-select')); + $multiple = $element['#multiple']; + $output .= '\n"; + if (isset($element['#field_suffix'])) { + $output .= $element['#field_suffix']; + } + $output .= '
'; + + return theme('form_element', $element, $output); +} \ No newline at end of file diff --git a/types/videoftp/videoftp_widget.inc b/types/videoftp/videoftp_widget.inc new file mode 100644 index 0000000..d674660 --- /dev/null +++ b/types/videoftp/videoftp_widget.inc @@ -0,0 +1,313 @@ + 'textfield', + '#title' => t('Permitted upload file extensions.'), + '#default_value' => !empty($widget['file_extensions']) ? $widget['file_extensions'] : 'mp4 mpeg avi mpg wmv flv mov', + '#size' => 64, + '#description' => t('Extensions a user can attach to this field. Separate extensions with a space and do not include the leading dot. Leaving this blank will allow attachment of a file with any extension.'), + '#weight' => 1 + ); + $form['path_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Path settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 2, + ); + $form['path_settings']['file_path'] = array( + '#type' => 'textfield', + '#size' => 64, + '#title' => t('File path'), + '#default_value' => ltrim(ltrim($widget['file_path'], "videos"), "/"), + '#description' => t('Optional subdirectory within the "files/videos/" directory where files will be moved and stored. Do not include preceding or trailing slashes.'), + '#element_validate' => array('video_widget_settings_file_path_validate', '_filefield_widget_settings_file_path_validate'), + ); + $form['path_settings']['ftp_path'] = array( + '#type' => 'textfield', + '#title' => t('FTP Filepath'), + '#default_value' => !empty($widget['ftp_path']) ? $widget['ftp_path'] : 'ftpvideos', + '#description' => t('The subdirectory within the "files/" directory where you have upload the videos for attachment. Once the video is attached it will be moved from this directory to the main files directory.'), + '#required' => TRUE, + '#weight' => 3, + ); + //default settings + $default = video_default_widget_settings($widget); + $form = $form + $default; + return $form; +} + +/** + * Implementation of CCK's hook_widget_settings($op = 'validate'). + * + * //@todo Integrate this into a universal function to share with uploadfield_widget + */ +function videoftp_widget_settings_validate($widget) { + // Check that only web images are specified in the callback. + if(!video_web_extensions($widget['file_extensions'])) { + form_set_error('file_extensions', t('Only web-standard videos are supported through the videoftp widget. If needing to upload other types of files, change the widget to use a standard file upload.')); + } + // Check for our ftp filepath and try to create it, if not it will throw an error. + $ftp_path = file_directory_path().'/'.$widget['ftp_path']; + file_check_directory($ftp_path, true, 'ftp_path'); +} + +/** + * Implementation of CCK's hook_widget_settings($op = 'save'). + */ +function videoftp_widget_settings_save($widget) { + return array('file_extensions', 'file_path', 'ftp_path', 'autothumbnail','autoconversion', 'default_dimensions', 'default_player_dimensions', 'default_video_thumb'); +} + +/** + * The #value_callback for the videoftp_widget type element. + */ +function videoftp_widget_value($element, $edit = FALSE) { + if (!$edit) { + // Creating so we load up our empty values. + $file = field_file_load($element['#default_value']['fid']); + $item = $element['#default_value']; + } + else { + // Reset our item array for our data. + $item = array_merge($element['#default_value'], $edit); + $field = content_fields($element['#field_name'], $element['#type_name']); + + // Uploads take priority over value of fid text field. + if ($fid = videoftp_save_upload($element)) { + $item['fid'] = $fid; + $item['data'] = array(); //reset our thumbnail + } + // Check for #videoftp_value_callback values. + // Because FAPI does not allow multiple #value_callback values like it does + // for #element_validate and #process, this fills the missing functionality + // to allow VideoFtp to be extended purely through FAPI. + elseif (isset($element['#videoftp_value_callback'])) { + foreach ($element['#videoftp_value_callback'] as $callback) { + $callback($element, $item); + } + } + + // Load file if the FID has changed so that it can be saved by CCK. + $file = field_file_load($item['fid']); + + // If the file entry doesn't exist, don't save anything. + if (empty($file)) { + $item = array(); + } + + // Checkboxes loose their value when empty. + // If the list field is present make sure its unchecked value is saved. + if (!empty($field['list_field']) && empty($edit['list'])) { + $item['list'] = 0; + } + } + // Merge file and item data so it is available to all widgets. + $item = array_merge($item, $file); + return $item; +} + +/** + * Process an individual element. + */ +function videoftp_widget_process($element, $edit, &$form_state, $form) { + $item = $element['#value']; + $field_name = $element['#field_name']; + $delta = $element['#delta']; + $field = content_fields($element['#field_name'], $element['#type_name']); + $element['#theme'] = 'videoftp_widget_item'; + + if (isset($element['preview']) && $element['#value']['fid'] != 0) { + $element['preview']['#value'] = theme('videoftp_widget_preview', $element['#value']); + } + + // Title is not necessary for each individual field. + if ($field['multiple'] > 0) { + unset($element['#title']); + } + + // Set up the buttons first since we need to check if they were clicked. + $element['videoftp_attach'] = array( + '#type' => 'submit', + '#value' => t('Attach'), + '#submit' => array('node_form_submit_build_node'), + '#ahah' => array( // with JavaScript + 'path' => 'videoftp/ahah/'. $element['#type_name'] .'/'. $element['#field_name'] .'/'. $element['#delta'], + 'wrapper' => $element['#id'] .'-ahah-wrapper', + 'method' => 'replace', + 'effect' => 'fade', + ), + '#field_name' => $element['#field_name'], + '#delta' => $element['#delta'], + '#type_name' => $element['#type_name'], + '#upload_validators' => $element['#upload_validators'], + '#weight' => 100, + '#post' => $element['#post'], + ); + $element['videoftp_remove'] = array( + // With default CCK edit forms, $element['#parents'] is array($element['#field_name'], $element['#delta']). + // However, if some module (for example, flexifield) places our widget deeper in the tree, we want to + // use that information in constructing the button name. + '#name' => implode('_', $element['#parents']) .'_videoftp_remove', + '#type' => 'submit', + '#value' => t('Remove'), + '#submit' => array('node_form_submit_build_node'), + '#ahah' => array( // with JavaScript + 'path' => 'videoftp/ahah/'. $element['#type_name'] .'/'. $element['#field_name'] .'/'. $element['#delta'], + 'wrapper' => $element['#id'] .'-ahah-wrapper', + 'method' => 'replace', + 'effect' => 'fade', + ), + '#field_name' => $element['#field_name'], + '#delta' => $element['#delta'], + '#weight' => 101, + '#post' => $element['#post'], + ); + + // Because the output of this field changes depending on the button clicked, + // we need to ask FAPI immediately if the remove button was clicked. + // It's not good that we call this private function, but + // $form_state['clicked_button'] is only available after this #process + // callback is finished. + if (_form_button_was_clicked($element['videoftp_remove'])) { + // Delete the file if it is currently unused. Note that field_file_delete() + // does a reference check in addition to our basic status check. + if (isset($edit['fid'])) { + $removed_file = field_file_load($edit['fid']); + if ($removed_file['status'] == 0) { + field_file_delete($removed_file); + } + } + $item = array('fid' => 0, 'list' => $field['list_default'], 'data' => array('description' => '', 'video_thumb' => '')); + } + + // Set access on the buttons and select. + $element['videoftp_attach']['#access'] = empty($item['fid']); + $element['videoftp_remove']['#access'] = !empty($item['fid']); + + $options = videoftp_options($field); + $element['ftpselect'] = array( + '#type' => 'select', + '#required' => isset($element['#required']) ? $element['#required'] : $field['required'], + '#options' => $options, + '#access' => empty($item['fid']), + ); + + // Set the FID. + $element['fid'] = array( + '#type' => 'hidden', + '#value' => $item['fid'], + ); + + if ($item['fid'] != 0) { + $element['preview'] = array( + '#type' => 'markup', + '#value' => theme('video_widget_preview', $item), + ); + } + + // placeholder.. will be serialized into the data column. this is a place for widgets + // to put additional data. + $element['data'] = array( + '#tree' => 'true', + '#access' => !empty($item['fid']), + ); + + // Create our thumbnails + if($field['widget']['autothumbnail']) { + video_thumb_process($element); + } + + // Add our extra fields if in preview mode + if(!empty($item['fid'])) { + video_widget_element_settings($element); + } + + // Set #element_validate in a way that it will not wipe out other + // validation functions already set by other modules. + if (empty($element['#element_validate'])) { + $element['#element_validate'] = array(); + } + array_unshift($element['#element_validate'], 'videoftp_widget_validate'); + + // Make sure field info will be available to the validator which + // does not get the values in $form. + $form_state['#field_info'][$field['field_name']] = $field; + + $element['#attributes']['id'] = $element['#id'] .'-ahah-wrapper'; + $element['#prefix'] = '
'; + $element['#suffix'] = '
'; + + // Lets use the clicked_button #submit[0] value here instead and see how that works out for now... + if($form_state['submitted'] == 1) { + video_widget_process($element, $form_state); + } + return $element; +} + +function videoftp_save_upload($element) { + global $user; + $upload_name = $element['#field_name'] .'_'. $element['#delta']; + $delta = $element['#delta']; + $field = content_fields($element['#field_name'], $element['#type_name']); + $video = $element['#post'][$field['field_name']][$delta]['ftpselect']; + $ftp_path = file_directory_path().'/'.$field['widget']['ftp_path']; + + if (empty($video) || !file_exists($ftp_path.'/'.$video)) { + return 0; + } + + $dest = filefield_widget_file_path($field); + if (!field_file_check_directory($dest, FILE_CREATE_DIRECTORY)) { + watchdog('filefield', 'The upload directory %directory for the file field %field (content type %type) could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', array('%directory' => $dest, '%field' => $element['#field_name'], '%type' => $element['#type_name'])); + form_set_error($upload_name, t('The file could not be uploaded.')); + return 0; + } + + // Begin building the file object. + $file = new stdClass(); + $file->uid = $user->uid; + $file->filename = file_munge_filename(trim(basename($video), '.'), $field['widget']['file_extensions']); + $file->filepath = $ftp_path.'/'.$video; + $file->filemime = file_get_mimetype($file->filename); + $file->filesize = filesize($file->filepath); + $file->status = FILE_STATUS_TEMPORARY; + $file->timestamp = time(); + + //lets move our file from the ftp folder to the files directory + if(file_move($file, $dest)) { + // Insert new record to the database. + drupal_write_record('files', $file); + } + _field_file_cache($file); // cache the file in order to minimize load queries + return $file->fid; +} + +function videoftp_options($field) { + $options = array(); + $options[] = t('Select Video'); + // Lets setup our ftp_path. + $ftp_path = file_directory_path().'/'.$field['widget']['ftp_path']; + // We are going to scan the directory and pull out the available video types by extension. + $extensions = explode(" ",$field['widget']['file_extensions']); + $video_files = scandir($ftp_path); + foreach ($video_files as $file) { + $ext = pathinfo($file); + if (in_array($ext['extension'], $extensions)) { + //add the file to the options array for selection + $options["$file"] = $file; + } + } + return $options; +} \ No newline at end of file diff --git a/video.info b/video.info index 93c9aaa..b4a584b 100644 --- a/video.info +++ b/video.info @@ -1,6 +1,9 @@ ;$Id$ + name = Video -description = Allows video nodes. +description = Allows Creation of CCK Video Fields. package = "Video" +dependencies[] = content +dependencies[] = filefield core = 6.x - +version = 6.x-4.x-dev \ No newline at end of file diff --git a/video.install b/video.install index b52b78a..0432d47 100644 --- a/video.install +++ b/video.install @@ -2,7 +2,7 @@ // $Id$ /** * @file - * Provide installation functions for video.module . + * Provides installation functions for video.module. * * @author Heshan Wanigasooriya * @@ -13,136 +13,86 @@ * Implementation of hook_schema(). */ function video_schema() { - $schema['video'] = array( - 'description' => t('Store video files informations'), + $schema['video_files'] = array( + 'description' => t('Store video transcoding queue'), 'fields' => array( - 'vid' => array( - 'description' => t('Prmary Key: {video}.vid of the video node'), + 'vid' => array( + 'description' => t('Video id'), + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'fid' => array( + 'description' => t('Original file id'), 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, ), 'nid' => array( - 'description' => t('Node id, index to the {node}.nid'), + 'description' => t('Node id'), 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, - ), - 'vtype' => array( - 'description' => t('The type of the video'), + ), + 'filename' => array( 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, + 'length' => '255', 'default' => '', + 'description' => t('The filename of the video.'), ), - 'vidfile' => array( - 'description' => t('Video file name / path'), - 'type' => 'text', - 'not null' => FALSE, + 'filepath' => array( + 'type' => 'varchar', + 'length' => '255', 'default' => '', + 'description' => t('The filepath of the video.'), ), - 'videox' => array( - 'description' => t('resolution : x'), - 'type' => 'int', - 'size' => 'small', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, + 'filemime' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The filemime of the video.'), ), - 'videoy' => array( - 'description' => t('resolution : y'), + 'filesize' => array( + 'description' => t('Filesize of the video.'), 'type' => 'int', - 'size' => 'small', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, ), - 'size' => array( - 'description' => t('size of the video file'), - 'type' => 'int', - 'size' => 'big', - 'unsigned' => TRUE, - 'not null' => FALSE, - ), - 'download_counter' => array( - 'description' => t('No. of downloads of the video'), - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, + 'dimensions' => array( + 'type' => 'varchar', + 'length' => '255', + 'default' => '', + 'description' => t('The dimensions of the video.'), ), - 'play_counter' => array( - 'description' => t('No. of play times per video'), + 'status' => array( + 'description' => t('Status of the transcoding'), 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, ), - 'video_bitrate' => array( - 'description' => t('Bit rate of the video'), - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - ), - 'audio_bitrate' => array( - 'description' => t('Bit rate of the audio'), - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - ), - 'audio_sampling_rate' => array( - 'description' => t('Sampling rate of the video'), - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - ), - 'audio_channels' => array( - 'description' => t('Chenells of the audio'), - 'type' => 'text', - 'not null' => FALSE, - ), - 'playtime_seconds' => array( - 'description' => t('Play time of the video'), + 'started' => array( + 'description' => t('Started transcodings'), 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - ), - 'download_folder' => array( - 'description' => t('download folder'), - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - ), - 'disable_multidownload' => array( - 'description' => t('enable/disable multi download'), - 'type' => 'int', - 'size' => 'tiny', - 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, ), - 'use_play_folder' => array( - 'description' => t('use play folder'), + 'completed' => array( + 'description' => t('Transcoding completed'), 'type' => 'int', - 'size' => 'tiny', - 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, ), - 'serialized_data' => array( - 'description' => t('Informations related to the videos'), - 'type' => 'text', - 'not null' => FALSE, - ), ), - 'indexes' => array( - 'nid' => array('nid'), + 'indexes' => array( + 'status' => array('status'), + 'file' => array('fid'), ), 'primary key' => array('vid'), ); - return $schema; } @@ -150,13 +100,7 @@ function video_schema() { * Implementation of hook_install(). */ function video_install() { - drupal_install_schema('video'); - - // default values for some variables use for resolution stuff - variable_set('video_resolution_1_name', '16:9 - Widescreen'); - variable_set('video_resolution_1_value', '400x226'); - variable_set('video_resolution_2_name', '4:3 - Television'); - variable_set('video_resolution_2_value', '400x300'); + drupal_install_schema('video'); } /** @@ -164,8 +108,27 @@ function video_install() { */ function video_uninstall() { drupal_uninstall_schema('video'); - variable_del('video_resolution_1_name'); - variable_del('video_resolution_1_value'); - variable_del('video_resolution_2_name'); - variable_del('video_resolution_2_value'); + //@todo Need to drop all variables used in this module here. +} + +/** + * Update 6405 + * dropping video_rendering table and creating video_files + * @return + */ +function video_update_6405() { + $ret = array(); + $ret[] = update_sql("DROP TABLE IF EXISTS {video_rendering}"); + $ret[] = update_sql("DROP TABLE IF EXISTS {video_files}"); + $table = video_schema(); + db_create_table($ret, 'video_files', $table['video_files']); + return $ret; +} + +function video_update_6406() { + drupal_set_message('The system has reset your thumbnail and ffmpeg command settings to their original state. If you made adjustments to these commands, you will have to reset them up.'); + //lets reset our ffmpeg system command variables. + variable_set('video_ffmpeg_thumbnailer_options', '-i !videofile -an -y -f mjpeg -ss !seek -vframes 1 !thumbfile'); + variable_set('video_ffmpeg_helper_auto_cvr_options', '-y -i !videofile -f flv -ar 22050 -ab !audiobitrate -s !size -b !videobitrate -qscale 1 !convertfile'); + return array(); } \ No newline at end of file diff --git a/video.module b/video.module index 3dc7f0d..1576bee 100644 --- a/video.module +++ b/video.module @@ -1,1488 +1,823 @@ - * - * @author Glen Marianko Twitter@demoforum - * @todo - */ - -/** GMM: Views 2 - * Implementation of hook_views_api() - */ -function video_views_api() { - return array( - 'api' => 2, - 'path' => drupal_get_path('module', 'video').'/includes', - ); -} - -/** - * Implementation of hook_views_handlers() - */ -function video_views_handlers() { - return array( - 'info' => array( - 'path' => drupal_get_path('module', 'video').'/includes', - ), - 'handlers' => array( - 'video_views_handler_field_playtime_seconds' => array('parent' => 'views_handler_field'), - 'video_views_handler_field_download' => array('parent' => 'views_handler_field'), - 'video_views_handler_field_play' => array('parent' => 'views_handler_field'), - 'video_views_handler_field_image' => array('parent' => 'views_handler_field'), - - ), - ); -} -/******************************************************************** - * General Hooks - ********************************************************************/ - -/** - * Help hook - * Implementation of hook_help - * @param $section - * string of the area of Drupal where help was requested - * - * @return - * string of help information - */ - -function video_help($path, $arg) { - switch ($path) { - case 'admin/help#video': - $output = '

'. t('The Video Module is used to create and administrator Video nodes for Drupal') .'

'; - return $output; - } -} - -/** - * Implementation of hook_menu(). - * - * @param $may_cache - * boolean indicating whether cacheable menu items should be returned - * - * @return - * array of menu information - */ -function video_menu() { - global $user; - $items = array(); - - $items['video'] = array( - 'title' => 'videos', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('video_page'), - 'access arguments' => array('access video'), - 'type' => MENU_SUGGESTED_ITEM); - - $items['video/feed'] = array( - 'title' => 'videos feed', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('video_feed'), - 'access arguments' => array('access video'), - 'type' => MENU_CALLBACK); - - - $items["node/add/video"] = array( - 'title' => 'Video', - 'description' => 'Allow a variety of video formats to be posted as nodes in your site', - 'page callback' => 'video_add', - 'access arguments' => array('create video')); - - $items['admin/settings/video'] = array( - 'title' => 'Video', - 'description' => 'Configure different aspects of the video module and its plugins', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('video_settings_form'), - 'access arguments' => array('administer video'), - 'type' => MENU_NORMAL_ITEM, - ); - - if (arg(0) == 'node' && is_numeric(arg(1))) { - if ($node = node_load(arg(1)) and $node->type == 'video') { - - //enable the download tab only if it is supported - if (video_support_download($node)) { - - $menu_type = (variable_get('video_displaydownloadmenutab', 1)) ? MENU_LOCAL_TASK : MENU_CALLBACK; - $items['node/'.$node->nid.'/download'] = array( - 'title' => 'Download', - 'page callback' => 'video_download', - 'page arguments' => array($node), - 'access arguments' => array('access video') && node_access('view', $node, $user->uid), - 'weight' => 5, - 'type' => $menu_type); - } - } - } - return $items; -} - - -/** - * Internal Drupal links hook - * - * @param $type - * string type of link to show - * - * @param $node - * object of node information + * @file video.module * - * @return - * array of link information */ -function video_link($type, $node = NULL) { - $link = array(); - // Node links for a video - if ($type == 'node' && $node->type == 'video' && $node->vidfile && user_access('access video')) { - //If the video is of type youtube and multi-file downloads aren't turned on don't show the download link. - if (!video_support_download($node) || $node->disable_multidownload == 1) { - $display_download_link = 0; - } - else { - $display_download_link = variable_get('video_displaydownloadlink', 1); - } - - if ($display_download_link == 1) { - $link['video_download'] = array( - 'title' => t('download'), - 'href' => "node/$node->nid/download", - 'attributes' => array( - 'class' => 'outgoing', - 'title' => t('download @link', array('@link' => $node->title)), - ), - ); - } - if (variable_get('video_displayplaytime', 1) && $node->playtime_seconds > 0) { // hide the duration if the admin hided it or we don't have playtime informations - $link['playtime'] = array( - 'title' => format_interval($node->playtime_seconds), - ); - } - if (variable_get('video_displayfilesize', 1) && $node->size != 0) { - $link['size'] = array( - 'title' => format_size($node->size), - ); - } - if (variable_get('video_playcounter', 1) && user_access('view play counter')) { - $link['play_counter'] = array( - 'title' => format_plural($node->play_counter, '1 play', '@count plays'), - ); - } - if (variable_get('video_downloadcounter', 1) && user_access('view download counter') && video_support_download($node)) { - $link['download_counter'] = array( - 'title' => format_plural($node->download_counter, '1 download', '@count downloads'), - ); - } - - return $link; - } - return array(); -} -/** - * Displays a Drupal page containing recently added videos - * - * @return - * string HTML output - */ -function video_page() { - theme('video_page', NULL); +/* + * Implementation of hook_init(). +*/ +function video_init() { + drupal_add_css(drupal_get_path('module', 'video'). '/css/video.css'); + drupal_add_js(drupal_get_path('module', 'video'). '/js/video.js'); } /** - * Displays a Drupal page containing recently added videos + * Invokes hook_video_*action*() in every module. + * Eg : + * hook_video_submit() + * hook_video_insert() + * hook_video_preview() + * hook_video_delete() + * hook_video_load() + * hook_video_form() - to show values once upload is completed eg. Resolution, and Convert on Save etc * - * @return - * string HTML output + * We cannot use module_invoke() for this, because the arguments need to + * be passed by reference. */ -function theme_video_page() { - $output = ''; - if (arg(1) != 'help') { //We are not reading help so output a list of recent video nodes. - $result = pager_query(db_rewrite_sql("SELECT n.nid, n.created FROM {node} n WHERE n.type = 'video' AND n.status = 1 ORDER BY n.created DESC"), variable_get('default_nodes_main', 10)); - while ($node = db_fetch_object($result)) { - $output .= node_view(node_load($node->nid), 1); +function video_module_invoke($action, &$array, &$video, $other = NULL) { + foreach (module_list() as $module) { + $function = $module . '_video_' . $action; + if (function_exists($function)) { + $function($array, $video, $other); } - $output .= theme('pager', NULL, variable_get('default_nodes_main', 10)); - // adds feed icon and link - drupal_add_link(array('rel' => 'alternate', - 'type' => 'application/rss+xml', - 'title' => variable_get('site_name', 'drupal') . ' ' . t('videos'), - 'href' => url('video/feed/'))); - - $output .= '
' . theme('feed_icon', url('video/feed'), t('Syndicate')); } - return $output; } /** - * Generate an RSS feed for videos - * - * @return - * feed + * Implementation of hook_perm(). */ -function video_feed() { - $channel = array( - 'title' => variable_get('site_name', 'drupal') . ' ' . t('videos'), - 'description' => t('Latest videos on') . ' ' . variable_get('site_name', 'drupal'), - 'link' => url('video', array('absolute' => TRUE)) - ); - - $result = db_fetch_object(db_query('SELECT n.nid FROM {node} n WHERE n.type = "video" AND n.status = 1 ORDER BY n.created DESC')); - node_feed($result, $channel); -} -/** - * Permissions hook - * - * @return - * array of permissions - */ function video_perm() { - $array = array('create video', 'access video', 'administer video', 'play video', 'download video', 'view play counter', 'view download counter', 'edit own video', 'edit all video nodes'); - return $array; + return array('bypass conversion video', 'convert on submission', 'override player dimensions', 'use default thumb'); } /** - * Settings Hook - * - * @return - * string of form content or error message + * Implementation of hook_menu(). */ -function video_settings_form() { - global $base_url; - - $form = array(); - - $form['menu'] = array( - '#type' => 'fieldset', - '#title' => t('General behavior'), - '#collapsible' => TRUE, - '#collapsed' => TRUE +function video_menu() { + $items = array(); + $items['admin/settings/video'] = array( + 'title' => 'Video', + 'description' => 'Configure different aspects of the video module and its plugins', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('video_general_admin_settings'), + 'file' => 'video.admin.inc', + 'access arguments' => array('administer site configuration'), + 'type' => MENU_NORMAL_ITEM, ); - - $vtypes = video_get_types_infos(); - - if ($vtypes) { // no vtype available - $video_types = array(); - foreach ($vtypes as $vtype => $info) { - $video_types[$vtype] = $info['#name']; - } - $video_types[0] = t('No default type'); - $form['menu']['video_default_video_type'] = array( - '#type' => 'select', - '#title' => t('Choose default video type'), - '#default_value' => variable_get('video_default_video_type', 0), - '#description' => t('For installations that have more than one video type available, this sets the default video type when a user visits node/add/video'), - '#options' => $video_types, - ); - } - - $form['menu']['video_displaydownloadmenutab'] = array( - '#type' => 'checkbox', - '#title' => t('Display download menu tab'), - '#default_value' => variable_get('video_displaydownloadmenutab', 1), - '#description' => t('Toggle display of menu tab to download video from the node page.') + $items['admin/settings/video/general'] = array( + 'title' => 'General', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => 0, ); - $form['menu']['video_displaydownloadlink'] = array( - '#type' => 'checkbox', - '#title' => t('Display download link'), - '#default_value' => variable_get('video_displaydownloadlink', 1), - '#description' => t('Toggle display of "download" link (below the node content in most themes).') + $items['admin/settings/video/players'] = array( + 'title' => 'Players', + 'description' => 'Configure your player settings for each video extension.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('video_players_admin_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'video.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, ); - $form['menu']['video_displayplaytime'] = array( - '#type' => 'checkbox', - '#title' => t('Display playtime'), - '#default_value' => variable_get('video_displayplaytime', 1), - '#description' => t('Toggle the display of the playtime for a video.') + $items['admin/settings/video/transcoders'] = array( + 'title' => 'Transcoders', + 'description' => 'Configure your transcoder to convert your videos or extra thumbnails.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('video_transcoder_admin_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'video.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, ); - $form['menu']['video_displayfilesize'] = array( - '#type' => 'checkbox', - '#title' => t('Display filesize'), - '#default_value' => variable_get('video_displayfilesize', 1), - '#description' => t('Toggle the display of the filesize for a video.') + $items['admin/settings/video/metadata'] = array( + 'title' => 'Metadata', + 'description' => 'Configure your metadata settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('video_metadata_admin_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'video.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 3, ); - $form['menu']['video_autoplay'] = array( - '#type' => 'checkbox', - '#title' => t('Automatically start video on page load'), - '#default_value' => variable_get('video_autoplay', TRUE), - '#description' => t('Start the video when the page and video loads') + $items['admin/settings/video/cron'] = array( + 'title' => 'Cron Settings', + 'description' => 'Configure your cron settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('video_cron_admin_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'video.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 4, ); + return $items; +} - $form['resolutions'] = array( - '#type' => 'fieldset', - '#title' => t('Video resolutions'), - '#collapsible' => TRUE, - '#collapsed' => TRUE +/** + * Implementation of hook_theme(). + */ +function video_theme() { + $theme = array(); + $theme['video_thumbnails'] = array( + 'arguments' => array('file' => NULL, 'alt' => '', 'title' => '', 'attributes' => NULL, 'getsize' => TRUE), + 'file' => 'video.theme.inc', ); - - $form['resolutions']["video_resolution_width"] = array( - '#type' => 'textfield', - '#title' => t("Default width"), - '#default_value' => variable_get("video_resolution_width", 400), - '#description' => t('The width which will be used to scale video during playing. This let all videos on the website look the same') + $theme['video_widget_preview'] = array( + 'arguments' => array('item' => TRUE), + 'file' => 'video.theme.inc', ); - - $i = 1; - while($i <= 4) { - $form['resolutions']["video_resolution_{$i}_name"] = array( - '#type' => 'textfield', - '#title' => t("Resolution {$i} name"), - '#default_value' => variable_get("video_resolution_{$i}_name", ''), - ); - $form['resolutions']["video_resolution_{$i}_value"] = array( - '#type' => 'textfield', - '#title' => t("Resolution {$i} value"), - '#default_value' => variable_get("video_resolution_{$i}_value", ''), - '#description' => t('The resolution: two numbers representing width and height separated by an "x"'), - ); - $i++; - } - - // statistics stuff - $form['counters'] = array( - '#type' => 'fieldset', - '#title' => t('Statistics counters'), - '#description' => t('To allow users to view counters visit: ') . l(t('access control'), 'admin/user/permissions'), - '#collapsible' => TRUE, - '#collapsed' => TRUE + $theme['video_image'] = array( + 'arguments' => array('file' => NULL, 'alt' => '', 'title' => '', 'attributes' => NULL, 'getsize' => TRUE, 'imagecache' => NULL), + 'file' => 'video.theme.inc', ); - $form['counters']['video_playcounter'] = array( - '#type' => 'checkbox', - '#title' => t('Count play hits'), - '#default_value' => variable_get('video_playcounter', 1), - '#description' => t('Counts a hit everytime someone views the play page.') + $theme['video_widget_video_thumb'] = array( + 'arguments' => array('item' => TRUE), + 'file' => 'video.theme.inc', ); - $form['counters']['video_downloadcounter'] = array( - '#type' => 'checkbox', - '#title' => t('Count downloads'), - '#default_value' => variable_get('video_downloadcounter', 1), - '#description' => t('Counts a hit everytime someone downloads a video.') + $theme['video_formatter_video_plain'] = array( + 'arguments' => array('element' => NULL), + 'file' => 'video_formatter.inc', ); - - $form['flash'] = array( - '#type' => 'fieldset', - '#title' => t('Flash settings'), - '#collapsible' => TRUE, - '#collapsed' => TRUE + $theme['video_formatter_video_nodelink'] = array( + 'arguments' => array('element' => NULL, 'imagecache' => NULL), + 'file' => 'video_formatter.inc', ); - $form['flash']['video_flvplayerloader'] = array( - '#type' => 'textfield', - '#title' => t('Filename of Flash loader'), - '#default_value' => variable_get('video_flvplayerloader', 'FlowPlayer.swf'), - '#description' => t('The name of the Shockwave file that manages loading the FLV movie. This is relative to the website root.') + //$theme['video_formatter_video_colorbox'] = array( + // 'arguments' => array('element' => NULL, 'imagecache' => NULL), + // 'file' => 'video_formatter.inc', + //); + $theme['video_formatter_video_media_js'] = array( + 'arguments' => array('element' => NULL), + 'file' => 'video_formatter.inc', ); - $form['ogg'] = array( - '#type' => 'fieldset', - '#title' => t('Ogg Theora settings'), - '#collapsible' => TRUE, - '#collapsed' => TRUE + $theme['video_encoding_failed'] = array( + 'arguments' => array(), + 'file' => 'video_formatter.inc', ); - $form['ogg']['video_cortado'] = array( - '#type' => 'textfield', - '#title' => t('Filename of Cortado Java Applet'), - '#default_value' => variable_get('video_cortado', $base_url . '/cortado.jar'), - '#description' => t('The path to the Cortado Applet to play Ogg Theora Files.') + $theme['video_inprogress'] = array( + 'arguments' => array(), + 'file' => 'video_formatter.inc', ); - return system_settings_form($form); -} - - -/** - * Form API callback to validate the upload settings form. - * - * Keeps the use from showing the play tab or the play link - * if they have chosen to display the video in the node body. - * - * @param $form_id - * The identifier of the form - * - * @param $form_values - * form values from the settings page - * - */ -function video_settings_form_validate($form, &$form_state){ - //print_r($form_state['values']); die; - - // if admin set a name for a resolution he also have to set its value - while($i <= 4) { - if($form_state['values']["video_resolution_{$i}_name"] != '' && $form_state['values']["video_resolution_{$i}_value"] == '') { - form_set_error("video_resolution_{$i}_value", t('You have to set a value for resolution %res_num if you want to enable it.', array('%res_num' => $i))); - } - if($form_state['values']["video_resolution_{$i}_value"] != '' && !preg_match('/^[0-9]{2,4}x[0-9]{2,4}$/',$form_state['values']["video_resolution_{$i}_value"])) { // check valid resolution value - form_set_error("video_resolution_{$i}_value", t('You have to set valid value for resolution %res_num if you want to enable it. A valid value is 400x300', array('%res_num' => $i))); - } - - $i++; - } - ; // for future use -} - - -/****************************************************************************** - * Node Hooks - ******************************************************************************/ - -/** - * Implementation of _node_info(). - * - * @return - * array - */ -function video_node_info() { - return array('video' => array( - 'name' => t('Video'), - 'module' => 'video', - 'description' => t('Allow a variety of video formats to be posted as nodes in your site'), - ) + $path = drupal_get_path('module', 'video'). '/theme'; + //Lets setup our themes for our players + $players = video_video_players(); + foreach($players as $tpl => $value) { + $theme[$tpl] = array( + 'arguments' => array('video' => NULL, 'node' => NULL, 'themed_output' => NULL), + 'file' => 'video_formatter.inc', + 'template' => str_replace('_', '-', $tpl), + 'path' => $path, ); -} - -/** - * access hook - */ -function video_access($op, $node, $account) { - - - if ($op == 'create') { - return user_access('create video', $account); } + //We need to add an flv theme buffer to allow users to override in their own module to add in extra parameters before + //calling our flv template file. + $theme['video_flv'] = array( + 'arguments' => array('video' => NULL, 'node' => NULL), + 'file' => 'video_formatter.inc' + ); - if ($op == 'update' || $op == 'delete') { - return (user_access('edit own video', $account) && ($account->uid == $node->uid)) || user_access('edit all video nodes', $account); + //setup our imagecache presets + if(module_exists('imagecache')) { + //we need formatters for each of our thumbnails. + //@todo create a function to check for our colorbox module and only add theme elements that could be used. + $thumb_types = array('video_nodelink'); //array('video_colorbox', 'video_nodelink'); + foreach($thumb_types as $types) { + foreach (imagecache_presets() as $preset) { + $theme['video_formatter_'. $preset['presetname'] .'__'. $types] = array( + 'arguments' => array('element' => NULL), + 'function' => 'theme_video_formatter_imagecache', + 'file' => 'video_formatter.inc' + ); + } + } } + return $theme; } /** - * Implementation of hook_nodeapi(). - * We use this to append tags to the RSS feeds Drupal generates. + * Implementation of CCK's hook_field_formatter_info(). */ -function video_nodeapi($node, $op, $arg) { - switch ($op) { - case 'rss item': - if ($node->type == 'video') { - // RSS Enclosure http://cyber.law.harvard.edu/rss/rss.html#ltenclosuregtSubelementOfLtitemgt - $attributes['url'] = _video_get_fileurl($node->vidfile); - $attributes['length'] = $node->size; - $mime_type = _video_get_mime_type($node); - if ($mime_type) { - $attributes['type'] = $mime_type; - $enclosure = array('key' => 'enclosure', 'attributes' => $attributes); - } - - // MRSS media:content http://search.yahoo.com/mrss - $media['url'] = $attributes['url']; - if ($attributes['length'] > 1) { - $media['fileSize'] = $attributes['length']; - } - if ($mime_type) { - $media['type'] = $mime_type; - } - if (isset($node->playtime_seconds) && $node->playtime_seconds > 0) { - $media['duration'] = $node->playtime_seconds; - } - if (isset($node->video_bitrate) && $node->video_bitrate > 0) { - $media['bitrate'] = $node->video_bitrate; - } - if (isset($node->videox) && isset($node->videoy) && $node->videox > 0) { - $media['width'] = $node->videox; - $media['height'] = $node->videoy; - } - if (isset($node->audio_sampling_rate) && $node->audio_sampling_rate > 0) { - $media['samplingrate'] = $node->audio_sampling_rate; - } - $mrss = array('key' => 'media:content', 'attributes' => $media); - - // work around for http://drupal.org/node/157709 - static $been_here = FALSE; - if (! $been_here) { - $mrss['namespace'] = array('xmlns:media="http://search.yahoo.com/mrss/"'); - $been_here = TRUE; - } +function video_field_formatter_info() { + $formatters = array( + 'video_plain' => array( + 'label' => t('Video'), + 'field types' => array('filefield'), + 'description' => t('Displays video files with player embedded.'), + ), + 'video_nodelink' => array( + 'label' => t('Video Thumbnail linked to node'), + 'field types' => array('filefield'), + 'description' => t('Displays the video thumbnail and links to the node.'), + ), + //'video_colorbox' => array( + // 'label' => t('Video Thumbnail to Colorbox'), + // 'field types' => array('filefield'), + // 'description' => t('Displays the video thumbnail and adds colorbox support.'), + //), + 'video_media_js' => array( + 'label' => t('Video inject with jMedia'), + 'field types' => array('filefield'), + 'description' => t('Displays the video by using jmedia javascript.'), + ), + ); + //setup our imagecache presets + if(module_exists('imagecache')) { + //we need formatters for each of our thumbnails. + $thumb_types = array('video_nodelink'); //array('video_colorbox', 'video_nodelink'); + foreach($thumb_types as $types) { + foreach (imagecache_presets() as $preset) { + $formatters[$preset['presetname'] .'__'. $types] = array( + 'label' => t('@preset @label', array('@preset' => $preset['presetname'], '@label' => $formatters[$types]['label'])), + 'field types' => array('filefield'), + ); } - return array($enclosure, $mrss); - - case 'revision delete': - db_query('DELETE FROM {video} WHERE vid = %d', $node->vid); - break; + } } + return $formatters; } - -/** - * Create video submission page. Let the user select the actived video types - * or display the video form for the selected video type +/* + * Implmentation of hook_cron(). */ -function video_add() { - global $user; - - $edit = isset($_POST['edit']) ? $_POST['edit'] : ''; - - // If a video type has been specified, validate its existence. - - $vtypes = video_get_types(); - - if (arg(3) && in_array(arg(3), $vtypes)) { // is a valid video type - $type = arg(3); - - // Initialize settings: - module_load_include('inc', 'node', 'node.pages'); - $node = (object) array('uid' => $user->uid, 'name' => $user->name, 'type' => 'video', 'vtype' => $type); - $output = drupal_get_form('video_node_form', $node); - drupal_set_title(t('Submit %name video', array('%name' => $type))); - } - else if (count($vtypes) == 1) { // only one vtype active. redirect the user to the active type form - // Initialize settings: - $node = (object) array('uid' => $user->uid, 'name' => $user->name, 'type' => 'video', 'vtype' => $vtypes[0]); - module_load_include('inc', 'node', 'node.pages'); - $output = drupal_get_form('video_node_form', $node); - drupal_set_title(t('Submit %name video', array('%name' => $vtypes[0]))); - } - else if ($vtype = variable_get('video_default_video_type', 0)) { - // Initialize settings: - $node = (object) array('uid' => $user->uid, 'name' => $user->name, 'type' => 'video', 'vtype' => $vtype); - module_load_include('inc', 'node', 'node.pages'); - $output = drupal_get_form('video_node_form', $node); - drupal_set_title(t('Submit %name video', array('%name' => $vtype))); - } - else { - $output = video_types_page(); +function video_cron() { + if (variable_get('video_cron', FALSE)) { + module_load_include('inc', 'video', '/includes/conversion'); + $video_conversion = new video_conversion; + $video_conversion->run_queue(); } - return $output; } - -/** - * Display a video types selection page -*/ -function video_types_page() { - - drupal_set_title(t('Submit Video')); // we have to set a titl ebecause the node module will not do it for us as we are using a callback video_add() - - $vtypes = video_get_types_infos(); - if(!$vtypes) { // no vtype available - return t('There are no Video types enabled.'); - } - else { - $items = array(); - foreach ($vtypes as $vtype => $infos) { - $out = '
'. l($infos['#name'], "node/add/video/$vtype", array('HTML'=>TRUE, 'attributes' => array('title' => 'Add a '. $infos['#name']))) .'
'; - $out .= '
'. $infos['#description'] .'
'; - $items[$vtype] = $out; - } - // let's order by type name - ksort($items); - return t('Choose from the following available video types:') .'
'. implode('', $items) .'
'; +function video_form_alter(&$form, &$form_state, $form_id) { + if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { + $form['buttons']['submit']['#submit'][] = 'video_node_update_submit'; } } - -/** - * Return an array containing enabled Video Types - */ -function video_get_types(){ - return array_keys(video_get_types_infos()); -} - - -/** - * Return an array containing informations on enabled Video Types - */ -function video_get_types_infos(){ - static $infos = NULL; - - if(!is_array($infos)) { - $infos = module_invoke_all('v_info'); +function video_node_update_submit($form, &$form_state) { + //lets update our video rending table to include the node id created + if (isset($form_state['nid']) && isset($form_state['values']['video_id']) && is_array($form_state['values']['video_id'])) { + foreach($form_state['values']['video_id'] as $fid) { + //lets update our table to include the nid + db_query("UPDATE {video_files} SET nid=%d WHERE fid=%d", $form_state['nid'], $fid); + } } - return $infos; -} - - -/** - * Return the informations for a given video type -*/ -function video_get_type_info($type) { - return module_invoke('video_'.$type, 'v_info'); } - -/** - * Return true if a given video type is downloadable +/* + * Utility function that will add a preview of thumbnails for you to select when uploading videos. */ -function video_support_download($node) { - $info = video_get_type_info($node->vtype); - return $info[$node->vtype]['#downloadable']; -} - - - -/** - * Hook, displays the contents of the node form page for creating and editing nodes. - * - * @param $node - * object - * - * @return - * string value of form content - */ -function video_form($node) { - //We must unserialize the array for display in the forms. - if($node->serial_data) { - $node->serial_data = unserialize($node->serialized_data); - } - $form = array(); - - // default node stuff - $type = node_get_types('type', $node); - - $form['title'] = array( - '#type' => 'textfield', - '#title' => check_plain($type->title_label), - '#size' => 60, - '#maxlength' => 128, - '#required' => TRUE, - '#default_value' => $node->title, - '#weight' => -20 - ); - - if ($type->has_body) { - $form['body_filter']['body'] = array( - '#type' => 'textarea', - '#title' => check_plain($type->body_label), - '#required' => ($type->min_word_count > 0), - '#rows' => 10, - '#default_value' => $node->body, - ); - $form['body_filter']['format'] = filter_form($node->format); - } - - // set an hidden field to store video type - $form['vtype'] = array( - '#type' => 'hidden', - '#value' => $node->vtype - ); - - // kjh: set an hidden field to store encoded fid - if ($node->serial_data && isset($node->serial_data['video_encoded_fid'])) { - $form['video_encoded_fid'] = array( - '#type' => 'hidden', - '#value' => $node->serial_data['video_encoded_fid'] - ); - } +function video_thumb_process(&$element) { + // Developed for ffmpeg support + $file = $element['#value']; + $delta = $file['fid']; + if (isset($element['preview']) && $file['fid'] != 0) { + module_load_include('inc', 'video', '/includes/transcoder'); + $transcoder = new video_transcoder; + if($thumbs = $transcoder->generate_thumbnails($file)) { + $default_thumb = ''; + $rnd_img = rand(0, variable_get('no_of_video_thumbs', 5) - 1); + $default_thumb = $thumbs[$rnd_img]->filepath; + + if(is_array($thumbs)) { + foreach($thumbs as $fid => $img) { + $thumbss[$img->filepath] = theme('video_thumbnails', $img, '', '', array('width' => '50'), FALSE); + } + } + } - $form['video'] = array('#type' => 'fieldset', '#title' => t('Video Information'), '#weight' => -19); - - if(!video_support_autoresolution($node)) { // this vtype doesn't support autoresolution - // let's display the resolution selection - $form['video']['vresolution'] = array( - '#type' => 'select', - '#title' => t('Resolution'), - '#description' => t("Select the approriate resolution (aspect ratio) for your video.
If you don't know what to choose then the default value will probably be ok for you."), - '#options' => _video_get_resolution_options(), - '#default_value' => _video_get_resolution_selected_option($node), - '#required' => true, - ); - } - else { - // set an hidden field to store video resolution - $form['hvresolution'] = array( - '#type' => 'hidden', - '#value' => $node->videox . 'x' . $node->videoy + $element['data']['video_thumb'] = array( + '#type' => 'radios', + '#title' => t('Video Thumbnails'), + '#options' => $thumbss, + '#default_value' => !empty($file['data']['video_thumb']) ? $file['data']['video_thumb'] : $default_thumb, + '#weight' => 10, + '#attributes' => array('class' => 'video-thumbnails', 'onchange' => 'videoftp_thumbnail_change()', 'rel' => 'video_large_thumbnail-'.$delta), ); - } - - if(!video_support_autoplaytime($node)) { // this vtype doesn't support autoplaytime - $form['video']['playtime'] = array( - '#type' => 'fieldset', - '#title' => t('Playtime'), - '#collapsible' => true, - '#collapsed' => ($node->playtime_seconds) ? false : true, // display expanded if we have values inserted by the user - '#description' => t('Insert here the duration of the video.
Values may be entered in excess of their normal "clock maximum" (the seconds field may be 3600 to represent 1 hour), however each value will be summed for a total of all three.')); - $playtime = _video_sec2hms($node->playtime_seconds); - $form['video']['playtime']['playtime_hours'] = array( - '#type' => 'textfield', - '#title' => t('Hours'), - '#size' => 11, - '#maxlength' => 11, - '#default_value' => $playtime['hours'], - ); - $form['video']['playtime']['playtime_minutes'] = array( - '#type' => 'textfield', - '#title' => t('Minutes'), - '#size' => 11, - '#maxlength' => 11, - '#default_value' => $playtime['minutes'], - ); - $form['video']['playtime']['playtime_seconds'] = array( - '#type' => 'textfield', - '#title' => t('Seconds'), - '#required' => FALSE, - '#size' => 11, - '#maxlength' => 11, - '#default_value' => $playtime['seconds'], - ); - } - else { - // set an hidden field to store video length - $form['playtime_seconds'] = array( - '#type' => 'hidden', - '#value' => $node->playtime_seconds - ); - // we need to store file size too - $form['hsize'] = array( - '#type' => 'hidden', - '#value' => $node->size - ); - } - - // Get the video-type-specific bits. - $form = module_invoke('video_' . $node->vtype, 'v_form', $node, $form); - - return $form; -} - - -/** - * Implementation of hook_validate - */ -function video_validate($node, $form = array()) { - if(!video_support_autoresolution($node) || $node->vresolution) { // we have some resolution value - // form api checked for good values of vresolution - if(variable_get("video_{$node->vresolution}_value", '') == '') { + // Setup our large thumbnail that is on the left. + // @todo Add smaller video preview instead of thumbnail? + if (isset($file['data']['video_thumb']) && !empty($file['data']['video_thumb'])) { + $large_thumb = array('filepath' => $file['data']['video_thumb']); + } + elseif (!empty($field['widget']['default_video_thumb'])) { + $large_thumb = $field['widget']['default_video_thumb']; } + else { + $large_thumb = array('filepath' => $default_thumb); + } + // @todo Integrate the thumbnails with imagecache. + $element['preview']['#suffix'] = '
'. theme('video_thumbnails', $large_thumb, '', '', array('width' => '150'), FALSE) .'
'; } - - module_invoke('video_'.$node->vtype, 'v_validate', $node); - } - /** - * Implementation of hook submit + * Adds a video to the video rendering table. + * + * If auto converting, it will convert your video to flv right now. We are passing the element by reference + * just in case we ever want to add more to the element during this process. + * + * @param $element + * Form element to get the video file from. */ -function video_presave(&$node) { - if(video_support_autoresolution($node) && ($node->new_video_upload_file_fid)) { // vtype support autoresolution getting - $xy = module_invoke('video_' . $node->vtype, 'v_auto_resolution', $node); - if ($xy) { - $node->videox = $xy[0]; - $node->videoy = $xy[1]; - } +function video_convert_process(&$element) { + $file = $element['#value']; + // Add default dimensions from our default_value if needed + if(!isset($file['data']['dimensions'])) { + $file['data']['dimensions'] = $element['data']['dimensions']['#value']; } - else { - // if you have a existing value from hidden field - if($node->hvresolution) { - $res = explode('x', $node->hvresolution); - $node->videox = $res[0]; - $node->videoy = $res[1]; - $node->size = $node->hsize; + $convert = false; + //we need to check if this fid has already been added to the database AND that there is in fact a fid + if (is_array($file) && isset($file['fid']) && !empty($file['fid']) && !$file['data']['bypass_autoconversion']) { + $fid = $file['fid']; + //setup our conversion class and check for the fid existence. + module_load_include('inc', 'video', '/includes/conversion'); + $video_conversion = new video_conversion; + // Lets verify that we haven't added this video already. Multiple validation fails will cause this to be ran more than once + if(!$video = $video_conversion->load_video($fid)) { + // Video has not been added to the queue yet so lets add it. + db_query("INSERT INTO {video_files} (fid, status, dimensions) VALUES (%d, %d, '%s')", $fid, VIDEO_RENDERING_PENDING, $file['data']['dimensions']); + $convert = true; + //lets queue our node status to unpublished. + $element['#unpublish'] = true; } - // we should have a good value (checked by Form API) - else { - $res = explode('x', variable_get('video_resolution_' . $node->vresolution . '_value', '')); - $node->videox = $res[0]; - $node->videoy = $res[1]; + elseif($video->video_status != VIDEO_RENDERING_COMPLETE) { + //lets queue our node status to unpublished. + $element['#unpublish'] = true; } - } - if(video_support_autoplaytime($node) && ($node->new_video_upload_file_fid)) { // vtype support auto playtime - $node->playtime_seconds = module_invoke('video_' . $node->vtype, 'v_auto_playtime', $node); - } - else { // vtype does not support auto_playtime - $node->playtime_seconds += ($node->playtime_hours * 3600) + ($node->playtime_minutes * 60); + // Our video should be in the database pending, lets see if we need to convert it now. + // Check if we are going from unselected to selected or if this is a new video and we have checked the checkbox + if (((!isset($element['#default_value']['data']['convert_video_on_save']) || !$element['#default_value']['data']['convert_video_on_save']) && $file['data']['convert_video_on_save']) || ($convert && $file['data']['convert_video_on_save'])) { + $return = $video_conversion->process($fid); + if ($return === FALSE) { + drupal_set_message(t('Something went wrong with your video conversion. Please check your recent log entries for further debugging.'), 'error'); + } + elseif($return === TRUE) { + //we are always unpublished until we are converted. + unset($element['#unpublish']); + drupal_set_message(t('Successfully converted your video.')); + } + } + elseif ($convert) { + drupal_set_message(t('Video submission queued for processing. Please wait: our servers are preparing your video for display.')); + } } } /** - * Implementation of hook_insert. - * Create video record in the video table - * - * @return - * TRUE on success, FALSE on error + * Implementation of hook_file_delete(). */ -function video_insert($node) { - // set the required properties of the video node - video_presave($node); - $node->serialized_data = serialize($node->serial_data); //Serialize the data for insertion into the database. - - return db_query("INSERT INTO {video} (vid, nid, vtype, vidfile, size, videox, videoy, video_bitrate, audio_bitrate, audio_sampling_rate, audio_channels, playtime_seconds, disable_multidownload, download_folder, use_play_folder, serialized_data) VALUES (%d, %d, '%s', '%s', %d, %d, %d, %d, %d, %d, '%s', %d, %d, '%s', %d, '%s')", - $node->vid, $node->nid, $node->vtype, $node->vidfile, $node->size, $node->videox, $node->videoy, $node->video_bitrate, $node->audio_bitrate, $node->audio_sampling_rate, $node->audio_channels, $node->playtime_seconds, $node->disable_multidownload, $node->download_folder, $node->use_play_folder, $node->serialized_data); +function video_file_delete($file) { + //lets get all our videos and unlink them + $sql = db_query("SELECT filepath FROM {video_files} WHERE fid=%d", $file->fid); + //we loop here as future development will include multiple video types (HTML 5) + while($row = db_fetch_object($sql)) { + if(file_exists($row->filepath)) unlink($row->filepath); + } + //now delete our rows. + db_query('DELETE FROM {video_files} WHERE fid = %d', $file->fid); + //now lets delete our video thumbnails and folder. + $video_thumb_path = variable_get('video_thumb_path', 'video_thumbs'); + $thumb_folder = file_directory_path(). '/' . $video_thumb_path . '/' . $file->fid; + // Recursively delete our folder and files + rmdirr($thumb_folder); } - /** - * Hook - * - * @return - * TRUE on success, FALSE on error + * Compares passed extensions with normal video web extensions. */ -function video_update($node) { - if ($node->revision) { //If a new node revision is being added then insert a new row. - return video_insert($node); - } - else { - // set the required properties of the video node - video_presave($node); - - // GMM: make sure to save the encoded_fid - if (!isset($node->serial_data['video_encoded_fid']) && $node->video_encoded_fid) { - $node->serial_data['video_encoded_fid'] = $node->video_encoded_fid; - } - - $node->serialized_data = serialize($node->serial_data); //Serialize the data for insertion into the database. - - return db_query("UPDATE {video} SET vidfile='%s', size=%d, videox=%d, videoy=%d, video_bitrate=%d, audio_bitrate=%d, audio_sampling_rate=%d, audio_channels='%s', playtime_seconds=%d, disable_multidownload=%d, download_folder='%s', use_play_folder=%d, serialized_data='%s' WHERE vid = %d", - $node->vidfile, $node->size, $node->videox, $node->videoy, $node->video_bitrate, $node->audio_bitrate, $node->audio_sampling_rate, $node->audio_channels, $node->playtime_seconds, $node->disable_multidownload, $node->download_folder, $node->use_play_folder, $node->serialized_data, $node->vid); +function video_web_extensions($ext) { + $extensions = array_filter(explode(' ', $ext)); + $web_extensions = array( + 'mov', 'mp4', '3gp', '3g2', 'mpg', 'mpeg', // quicktime + 'divx', //divx + 'rm', // realplayer + 'flv', 'f4v', //flash player + 'swf', // swf player + 'dir', 'dcr', // dcr player + 'asf', 'wmv', 'avi', 'mpg', 'mpeg', // windows media + 'ogg', + 'ogv', // ogg/ogv theora + ); + if (count(array_diff($extensions, $web_extensions))) { + return FALSE; } + return TRUE; } - - /** - * Implementation of hook_delete + * Implementation of hook_views_api(). */ -function video_delete($node) { - db_query("DELETE FROM {video} WHERE nid = %d", $node->nid); +function video_views_api() { + return array( + 'api' => 2.0, + 'path' => drupal_get_path('module', 'video') . '/views', + ); } - - /** - * Implementation of hook_load() - * - * @param $node - * object or boolean FALSE on error - */ -function video_load($node) { - - if (is_numeric($node->vid)) { - $node = db_fetch_object(db_query("SELECT * FROM {video} WHERE vid = %d", $node->vid)); - - // load serialized data for plug-ins - $node->serial_data = unserialize($node->serialized_data); - - return $node; + * Process elements loads on settings + * @param $element + */ +function video_widget_element_settings(&$element) { + $file = $element['#value']; + $delta = $element['#delta']; + $field = content_fields($element['#field_name'], $element['#type_name']); + // Check if using the default width and replace tokens. + $default_dimensions = user_access('override player dimensions'); + $description = t('Set your video dimensions. This will create your player with these dimensions.'); + //setup our default dimensions. + $dimensions = $field['widget']['default_dimensions']; + $player_dimensions = $field['widget']['default_player_dimensions']; + // Lets figure out our dimensions for our video and add astericks next to our options. + $options = video_explode("\n", variable_get("video_metadata_dimensions", video_default_dimensions())); + if ($field['widget']['autoconversion'] && isset($element['preview']) && $file['fid'] != 0 && $default_dimensions) { + $video_info = _video_dimensions_options($options, $file['filepath']); + $description = t('Set your video dimensions. This will create your player and transcode your video with these dimensions. Your video size is !size, if you choose a higher resolution, this could cause video distortion. You are shown dimensions that match your aspect ratio, if you choose dimensions that do not match your ratio, we will pad your video by adding black bars on either the top or bottom while maintaining your videos original aspect ratio.', array('!size' => $video_info['width'] .'x'. $video_info['height'])); + //setup our default display of dimensions. + //lets go through our options looking for a matching resolution + foreach($options as $key => $value) { + if(stristr($value, t('(Matches Resolution)')) == TRUE) { + $dimensions = $key; + break; + } + } } - else { - return false; + // Override our dimensions to the user selected. + if (isset($file['data']['dimensions']) && !empty($file['data']['dimensions'])) { + $dimensions = $file['data']['dimensions']; } -} -/** - * Implementation of hook_view(). - */ -function video_view(&$node, $teaser = FALSE, $page = FALSE) { + // Override our player dimensions to the user selected. + if (isset($file['data']['player_dimensions']) && !empty($file['data']['player_dimensions'])) { + $player_dimensions = $file['data']['player_dimensions']; + } - // include the video css file - drupal_add_css(drupal_get_path('module', 'video').'/video.css'); + $element['data']['dimensions'] = array( + '#type' => 'select', + '#title' => t('Dimensions for Video Transcoding'), + '#default_value' => $dimensions, + '#description' => $description, + '#options' => $options, + ); + $element['data']['player_dimensions'] = array( + '#type' => 'select', + '#title' => t('Dimensions for Video Player'), + '#default_value' => $player_dimensions, + '#description' => t('WxH of your video player.'), + '#options' => $options, + ); + // If users cannot change the default dimensions, lets change this to a value. + if (!$default_dimensions) { + $element['data']['dimensions']['#type'] = 'value'; + $element['data']['dimensions']['#value'] = $dimensions; + $element['data']['player_dimensions']['#type'] = 'value'; + $element['data']['player_dimensions']['#value'] = $player_dimensions; + } - //Run the body through the standard filters. - $node = node_prepare($node, $teaser); - //print_r($node); - //exit; - // theme the teaser - $node->teaser = theme('video_teaser', $node, $teaser, $page); - - // if we are viewing the page, run the body through the theme - if ($page) { - $output = ''; - if (user_access('play video')) { - $node->content['video_player'] = array('#value' => theme('video_player', $node), '#weight' => -1); + // only in preview mode and then create thumbnails + if ($field['widget']['autoconversion']) { + if (user_access('bypass conversion video')) { + $element['data']['bypass_autoconversion'] = array( + '#type' => 'checkbox', + '#title' => t('Bypass auto conversion'), + '#default_value' => isset($file['data']['bypass_autoconversion']) ? $file['data']['bypass_autoconversion'] : variable_get('video_bypass_conversion', FALSE), + '#description' => t('This will bypass your auto conversion of videos.'), + '#attributes' => array('class' => 'video-bypass-auto-conversion'), + ); } - else { - $output .= l(t('login'), "user/login", array('class' => 'outgoing', 'title' => t('login to your account'))); - $output .= ' ' . t('or') . ' '; - $output .= l(t('register'), "user/register", array('class' => 'outgoing', 'title' => t('create a new account'))); - $output .= ' ' . t('to play video'); - - $node->content['video_player'] = array('#value' => $output, '#weight' => -1); + $convert_on_submission = user_access('convert on submission'); + $element['data']['convert_video_on_save'] = array( + '#type' => $convert_on_submission ? 'checkbox' : 'value', + '#title' => t('Convert video on save'), + '#default_value' => isset($file['data']['convert_video_on_save']) ? $file['data']['convert_video_on_save'] : variable_get('video_convert_on_save', FALSE), + '#description' => t('This will convert your video to flv format when you save, instead of scheduling it for cron.'), + '#attributes' => array('class' => 'video-convert-video-on-save'), + ); + $use_default_thumb = user_access('use default thumb'); + if ($use_default_thumb) { + $element['data']['use_default_video_thumb'] = array( + '#type' => $use_default_thumb ? 'checkbox' : 'value', + '#title' => t('Use the default thumbnail for this video?'), + '#default_value' => isset($file['data']['use_default_video_thumb']) ? $file['data']['use_default_video_thumb'] : variable_get('video_use_default_thumb', FALSE), + '#description' => t('This will set a flag for this video to use the default video thumbnail when outputed..'), + '#attributes' => array('class' => 'video-use-default-video-thumb'), + ); } } - - return $node; } -/******************************************************************** - * Block display functions - ********************************************************************/ /** - * Hook block. Does all the interaction with the drupal block system. Uses video_block_list() for DB queries. - * - * @param $op - * string type of block - * - * @param $delta - * integer 0 for latest, 1 for played+downloaded, 2 for most played, 3 for most downloaded. - * - * @param $edit - * array holds the data submitted by the configure forms. - * - * @return - * array - */ -function video_block($op = 'list', $delta = 0, $edit = array()) { - if ($op == 'list') { - $blocks[0]['info'] = t('Latest videos'); - $blocks[1]['info'] = t('Top videos'); - $blocks[2]['info'] = t('Most played videos'); - $blocks[3]['info'] = t('Most downloaded'); - $blocks[4]['info'] = t('Random video'); - return $blocks; - } - else if ($op == 'view') { - switch ($delta) { - case 0: - return array( - 'subject' => variable_get('video_block_title_0', t('Latest videos')), - 'content' => video_block_list($delta) - ); - case 1: - return array( - 'subject' => variable_get('video_block_title_1', t('Top videos')), - 'content' => video_block_list($delta) - ); - case 2: - return array( - 'subject' => variable_get('video_block_title_2', t('Most played videos')), - 'content' => video_block_list($delta) - ); - case 3: - return array( - 'subject' => variable_get('video_block_title_3', t('Most downloaded')), - 'content' => video_block_list($delta) - ); - case 4: - return array( - 'subject' => variable_get('video_block_title_4', t('Random video')), - 'content' => video_block_list($delta) - ); - } - } - else if ($op == 'configure') { - switch ($delta) { //Get the default title of the block incase the variable is not set yet. - case 0: - $default_title = t('Latest videos'); - break; - case 1: - $default_title = t('Top videos'); - break; - case 2: - $default_title = t('Most played videos'); - break; - case 3: - $default_title = t('Most downloaded'); - break; - case 4: - $default_title = t('Random video'); - } - $form['video_block_title'] = array( - '#type' => 'textfield', - '#title' => t('Block display title'), - '#default_value' => variable_get("video_block_title_$delta", $default_title)); - $form['video_block_limit'] = array( - '#type' => 'select', - '#title' => t('Number of videos to list in block'), - '#default_value' => variable_get("video_block_limit_$delta", 10), - '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15))); - return $form; - } - else if ($op == 'save') { - variable_set("video_block_title_$delta", $edit['video_block_title']); - variable_set("video_block_limit_$delta", $edit['video_block_limit']); - } -} - -/** - * Query DB for block content - * - * @param $delta - * int 0, 1, 2, or 3. Determines which type of block is being accessed. - * - * @return - * string HTML content for a block + * Video_widgi_process for API handlers for any video types. + * @param $element + * @param $form_state */ -function video_block_list($delta = 0) { - $count = variable_get("video_block_limit_$delta", 10); - switch ($delta) { - case 0: - $orderby = 'n.created'; - break; - case 1: - $orderby = 'v.download_counter + v.play_counter'; - break; - case 2: - $orderby = 'v.play_counter'; +function video_widget_process($element, &$form_state) { + $item = $element['#value']; + switch($form_state['clicked_button']['#submit'][0]) { + case 'node_form_submit': + // Auto convert our video file + if($field['widget']['autoconversion']) { + video_convert_process($element); + //lets set our node status to unpublished if our video is not converted. + if ($element['#unpublish']) { + //unpublish the node + $form_state['values']['status'] = 0; + } + } + // Call hook_video_submit API + video_module_invoke('submit', $element, $form_state); + // + //queue up the file id to update the node id in the video rendering / cdn tables. + $form_state['values']['video_id'][] = $item['fid']; break; - case 3: - $orderby = 'v.download_counter'; + case 'node_form_build_preview': + // Call hook_video_preview API + video_module_invoke('preview', &$element, &$form_state); break; - case 4: - $count = 1; - $orderby = 'RAND()'; + case 'node_form_delete_submit': + //moved to hook_file_delete in video module. break; } - return node_title_list(db_query_range(db_rewrite_sql("SELECT n.nid, n.title, $orderby FROM {node} n INNER JOIN {video} v ON n.vid = v.vid WHERE n.type = 'video' AND n.status = 1 AND n.moderate = 0 ORDER BY $orderby DESC"),0, $count)); } -/**************************************************** - * Menu callback functions - ****************************************************/ - -/** - * Redirects to download the video file. - */ -function video_download($node) { - // $node as been loaded by video_menu - print_r($node); - exit; - if ($node) { - if (video_support_download($node) && _video_allow_download($node)) { //Make sure the video type is not youtube before downloading. - _video_download_goto($node); +/* + * Function updates our options list to show matching aspect ratios and if we have a matching resolution. + * + * We will update the options array by reference and return the aspect ratio of the file. +*/ +function _video_dimensions_options(&$options, $video) { + $aspect_ratio = _video_aspect_ratio($video); + //loop through our options and find matching ratio's and also the exact width/height + foreach($options as $key => $value) { + $wxh = explode('x', $value); + //lets check our width and height first + if($aspect_ratio['width'] == $wxh[0] && $aspect_ratio['height'] == $wxh[1]) { + $options[$key] = $value .' '.t('(Matches Resolution)'); } - else { //If video is type youtube then it can't be downloaded. - drupal_set_message(t('There are no files to download for this video.'), 'error'); - print theme('page', ''); + else { + //now lets check our ratio's + $ratio = number_format($wxh[0] / $wxh[1], 4); + if($ratio == $aspect_ratio['ratio']) { + $options[$key] = $value .' '.t('(Matches Ratio)'); + } } } - else { - drupal_not_found(); - } + return $aspect_ratio; } - -/** - * Return true if the video is downloadable, false otherwise +/* + * Returns the width/height and aspect ratio of the video + * + * @todo: move this to the transcoder class instead? */ -function _video_allow_download($node) { - // TODO: now videos are downloadable by default. why not implementing a feature to let users choose if they want their videos to be downloadable? - - return true; +function _video_aspect_ratio($video) { + //lets get our video dimensions from the file + module_load_include('inc', 'video', '/includes/transcoder'); + $transcoder = new video_transcoder; + $wxh = $transcoder->get_dimensions($video); + $width = $wxh['width']; + $height = $wxh['height']; + + if(!$width || !$height) { + //no width and height found just return. + watchdog('video_conversion', 'We could not determine the height and width of the video: '.$video, array(), WATCHDOG_DEBUG); + drupal_set_message(t('The system counld not determine the width and height of your video: !video. If transcoding, the system could have problems.', array('!video' => $video))); + return; + } + + //now lets get aspect ratio and compare our options in the select dropdown then add an asterick if any to each option representing a matching aspect ratio. + $ratio = number_format($width / $height, 4); + $aspect_ratio = array( + 'width' => $width, + 'height' => $height, + 'ratio' => $ratio, + ); + return $aspect_ratio; } - -/** - * Theme the teaser - * - * This is just in place for site admins and theme developers - * who need to adjust how the teaser is themed. - * - * @param $node - * The node to be displayed. - * @param $teaser - * Whether we are to generate a "teaser" or summary of the node, rather than display the whole thing. - * @param $page - * Whether the node is being displayed as a standalone page. If this is TRUE, the node title should not be displayed, as it will be printed automatically by the theme system. Also, the module may choose to alter the default breadcrumb trail in this case. - * - * @return - * html - */ -function theme_video_teaser($node, $teaser = FALSE, $page = FALSE) { - return $node->teaser; +/* + * Return our list of video extensions and their associated player. +*/ +function video_video_extensions() { + $extensions = array( + 'divx' => 'video_play_divx', + 'mov' => 'video_play_quicktime', + '3gp' => 'video_play_quicktime', + '3g2' => 'video_play_quicktime', + 'mp4' => 'video_play_quicktime', + 'rm' => 'video_play_realmedia', + 'f4v' => 'video_play_flv', + 'flv' => 'video_play_flv', + 'swf' => 'video_play_flash', + 'dir' => 'video_play_dcr', + 'dcr' => 'video_play_dcr', + 'asf' => 'video_play_windowsmedia', + 'wmv' => 'video_play_windowsmedia', + 'avi' => 'video_play_windowsmedia', + 'mpg' => 'video_play_windowsmedia', + 'mpeg' => 'video_play_windowsmedia', + 'ogg' => 'video_play_theora', + 'ogv' => 'video_play_theora', + ); + return $extensions; } -/** - * theme the view of the page to include the video - * assumes that body was put through prepare in hook_view - * - * @param $node - * The node to be displayed. - * @param $teaser - * Whether we are to generate a "teaser" or summary of the node, rather than display the whole thing. - * @param $page - * Whether the node is being displayed as a standalone page. If this is TRUE, the node title should not be displayed, as it will be printed automatically by the theme system. Also, the module may choose to alter the default breadcrumb trail in this case. - * - * @return - * html - */ -function theme_video_view($node, $teaser = FALSE, $page = FALSE) { - return '
'. $node->body .'
'; +/* + * Return our supported video players. +*/ +function video_video_players() { + $players = array( + 'video_play_divx' => t('Divx Player'), + 'video_play_quicktime' => t('Quicktime'), + 'video_play_realmedia' => t('Real Media Player'), + 'video_play_flv' => t('FLV Flash Players'), + 'video_play_flash' => t('SWF Flash Player'), + 'video_play_dcr' => t('Director/Shockwave'), + 'video_play_windowsmedia' => t('Windows Media Player'), + 'video_play_theora' => t('Theora Player'), + ); + return $players; } -/** -* theme function to control which player is presented -* -* @param $node -* node object -* -* @return -* html +/* + * Return our possible flash players. */ -function theme_video_player($node) { - // include video.js file for Internet Explorer fixes - //theme('video_get_script'); - drupal_add_js(drupal_get_path('module', 'video') . '/video.js'); - if (variable_get('video_playcounter', 1)) { - db_query("UPDATE {video} SET play_counter = play_counter + 1 where vid = %d", $node->vid); //Increment play counter. +function video_video_flv_players() { + $options = array(); + if(module_exists('swftools')) { + $options['swftools'] = t('SWF Tools'); } - - _video_scale_video($node); - $output = module_invoke('video_'.$node->vtype, 'v_play', $node); - - return $output; -} - - -/** - * Cut down on redundant link text - * - * @param $url - * string URL to link to - * - * @param $title - * string title of link to show on mouseover - * - * @param $link_text - * string text of the link - * - * @return - * string HTML link - */ -function theme_video_format_play($output, $url, $title, $link_text) { - $output = "\n
\n" . $output; - $output .= "

\n". t('Problems viewing videos?'); - $output .= "
\n"; - $output .= l($link_text, $url, array('attributes' => array('title' => $title), 'absolute' => TRUE)); - return $output ."\n

\n
\n"; -} - - -/** - * Takes an associative array of $fields with 'title' and 'body' keys and outputs the HTML. - * This theme function allows the same HTML code to generate all the custom and metadata fields. - * - * @param $fields - * array with 'title' and 'body' keys - * - * @return - * string of content to display - */ -function theme_video_fields($fields) { - $output = ''; - $odd_even = 'odd'; - foreach ($fields as $field) { - $output .= "
" . check_plain($field['title']) . ' ' . check_plain($field['body']) . "
\n"; - $odd_even = ($odd_even == 'odd') ? 'even' : 'odd'; //Always switch its value. + if(module_exists('flowplayer')) { + $options['flowplayer'] = t('Flowplayer'); } - return $output; -} - - -/** - * Import the video.js script - */ -function theme_video_get_scripvt() { - drupal_add_js(drupal_get_path('module', 'video') . '/video.js'); + return $options; } -/****************************************************************************** - * End theme functions - ****************************************************************************** - * Start private functions created for this module. - ******************************************************************************/ - /** - * Pull the file extension from a filename - * - * @param $vidfile - * string filename to get the filetype from. - * - * @return - * string value of file type or boolean FALSE on error + * Get the object for the suitable player for the parameter resource */ -function _video_get_filetype($vidfile) { - //If the filename doesn't contain a ".", "/", or "\" and is exactly 11 characters then consider it a youtube video ID. - if (!strpos($vidfile, '.') and !strpos($vidfile, '/') and !strpos($vidfile, '\\') and strlen($vidfile) == 11) { - $file_type = 'youtube'; - } - else if (strpos($vidfile, 'google:') === 0) { - $file_type = 'googlevideo'; - } - else if (strstr($vidfile, '.')) { //If file contains a "." then get the file extension after the "." - - $file_type = end(explode('.', $vidfile)); +function video_get_player($element) { + // Setup our node object to be passed along with the player. + $node = $element['#node']; + // Setup our video object + module_load_include('inc', 'video', '/includes/video_helper'); + $video_helper = new video_helper; + $video = $video_helper->video_object($element); + // Lets spit out our theme based on the extension + $defaults = video_video_extensions(); + $theme_function = variable_get('video_extension_'. $video->extension, $defaults[$video->extension]); + // Lets do some special handling for our flv files to accomdate multiple players. + if($theme_function == 'video_play_flv') { + return theme('video_flv', $video, $node); } else { - $file_type = FALSE; + return theme($theme_function, $video, $node); } - - return strtolower($file_type); } -/** - * Forward user directly to the file for downloading - * - */ -function _video_download_goto($node) { - if (user_access('download video')) { - - if (variable_get('video_downloadcounter', 1)) { - db_query("UPDATE {video} SET download_counter = download_counter + 1 where vid = %d", $node->vid); //Increment download counter. - } - - // let the submodule handle the real download logic - module_invoke('video_'.$node->vtype, 'v_download', $node); - } - else { //If the user does not have access to download videos. - drupal_set_message(t('You do not have permission to download videos.'), 'error'); - drupal_goto("node/".$node->nid); //Use the nid we just loaded to go back to the node page. +function video_default_widget_settings($widget) { + $form = array(); + // Default video settings. + $form['plugins'] = array( + '#type' => 'fieldset', + '#title' => t('Video Advanced Settings'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#weight' => 10 + ); + $form['plugins']['default_dimensions'] = array( + '#type' => 'select', + '#title' => t('Default Video Resolution Dimensions'), + '#default_value' => !empty($widget['default_dimensions']) ? $widget['default_dimensions'] : '', + '#options' => video_explode("\n", variable_get("video_metadata_dimensions", video_default_dimensions())), + '#description' => t('Default transcoding resolution WIDTHxHEIGHT, in px, that FFMPEG will use to transcode your video files.') + ); + $form['plugins']['default_player_dimensions'] = array( + '#type' => 'select', + '#title' => t('Default Video Player Dimensions'), + '#default_value' => !empty($widget['default_player_dimensions']) ? $widget['default_player_dimensions'] : '', + '#options' => video_explode("\n", variable_get("video_metadata_dimensions", video_default_dimensions())), + '#description' => t('Default player WIDTHxHEIGHT in px. This is your actual player dimensions that your video will be playing in.') + ); + $form['plugins']['autoconversion'] = array( + '#type' => 'checkbox', + '#title' => t('Enable video conversion.'), + '#description' => t('Use ffmpeg(Default) to automatically convert videos to web compatible types eg. FLV, Please make sure to configure your transcoder settings.'), + '#default_value' => $widget['autoconversion'], + ); + $form['plugins']['autothumbnail'] = array( + '#type' => 'checkbox', + '#title' => t('Enable thumbnail creation.'), + '#description' => t('Use ffmpeg(Default) to create thumbnails, Please make sure to configure your transcoder settings.'), + '#default_value' => $widget['autothumbnail'], + ); + // Default thumbnail settings. + $form['default'] = array( + '#type' => 'fieldset', + '#title' => t('Video Thumbnail Settings'), + '#element_validate' => array('video_default_widget_settings_validate'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#weight' => 11 + ); + // @TODO: Move this to the actual upload/attach when creating a node to allow the user to upload their own thumbnail for each video. + // Present a video image of the current default image. + if (!empty($widget['default_video_thumb'])) { + $form['default']['default_video_thumbnail'] = array( + '#type' => 'markup', + '#value' => theme('video_image', $widget['default_video_thumb'], '', '', array('width' => '150'), FALSE), + '#prefix' => '
', + '#suffix' => '
' + ); } + $form['default']['default_video_thumb_upload'] = array( + '#type' => 'file', + '#title' => empty($widget['default_video_thumb']) ? t('Upload default video thumbnail') : t('Replace default video thumbnail with'), + '#description' => t('Choose a image that will be used as video thumbnail when you don\'t have video thumbnails for videos.'), + ); + // We set this value on 'validate' so we can get CCK to add it + // as a standard field setting. + $form['default_video_thumb'] = array( + '#type' => 'value', + '#value' => $widget['default_video_thumb'], + ); + return $form; } - - /** - * Convert filesize to bytes + * Element specific validation for video default value. * - * @return - * integer bytes */ -function _video_size2bytes($node) { - if (!empty($node->size)) { - switch ($node->size_format) { - case 'Kb': // KiloBits - return intval($node->size * 128); - break; - case 'KB': // KiloBytes - return intval($node->size * 1024); - break; - case 'Mb': // MegaBits - return intval($node->size * 131072); - break; - case 'MB': // MegaBytes - return intval($node->size * 1048576); - break; - case 'Gb': // GigaBits - return intval($node->size * 134217728); - break; - case 'GB': // GigaBytes - return intval($node->size * 1073741824); - break; - default: - return (int)$node->size; - break; - } - } - else { - return 0; +function video_default_widget_settings_validate($element, &$form_state) { + // Verify the destination exists + $destination = file_directory_path() .'/video_thumbs'; + if (!field_file_check_directory($destination, FILE_CREATE_DIRECTORY)) { + form_set_error('default_video_thumb', t('The default image could not be uploaded. The destination %destination does not exist or is not writable by the server.', array('%destination' => dirname($destination)))); + return; } -} - - -/** - * Convert seconds to hours, minutes, and seconds. - * Derived from h:m:s example by Jon Haworth - * - * @link - * http://www.laughing-buddha.net/jon/php/sec2hms/ - * - * @param $sec - * integer value of seconds. - * - * @return - * array associative with key values of hours, minutes, and seconds. - */ -function _video_sec2hms($sec = 0) { - $hms = array(); - // 3600 seconds in an hour and trash remainder - $hms['hours'] = intval(intval($sec) / 3600); - // dividing the total seconds by 60 will give us - // the number of minutes, but we're interested in - // minutes past the hour: to get that, we need to - // divide by 60 again and keep the remainder - $hms['minutes'] = intval(($sec / 60) % 60); - $hms['seconds'] = intval($sec % 60); //keep the remainder. - return $hms; -} - -/** - * Returns an absolute url which references - * to the video file - * - * @param $video_file - * string containing absolute or relative URL to video. - * - * @return - * string containing absolute URL path to video file. - */ -function _video_get_fileurl($video_file) { - global $base_url; - //creation of absolute url - if (preg_match("/^(http|ftp|mm|rstp)(s?):\/\//", $video_file)) { //If path is absolute - return check_plain($video_file); - } - else { // path is relative to drupal install - return check_plain($base_url . '/' . $video_file); - } -} + $validators = array ( + 'file_validate_is_image' => array(), + ); -/** - * Returns the correct mime-type for the video. Returns false if the - * mime-type cannot be detected. - */ -function _video_get_mime_type($node) { - switch (_video_get_filetype($node->vidfile)) { - case 'mov': - return 'video/quicktime'; - case 'avi' : // Added - return 'video/x-msvideo'; - case 'mpg' : // Added - case 'mpeg' : // Added - return 'video/mpeg'; // Added - case 'divx': - return 'video/vnd.divx'; - case 'rm': - return 'application/vnd.rn-realmedia'; - case 'flv': - return 'flv-application/octet-stream'; - case 'asf': - return 'video/x-ms-asf'; - case 'wmv': - return 'video/x-ms-wmv'; - case '3gp': - return 'video/3gpp'; - case 'mp4': - return 'video/mp4'; - case 'dir': - case 'dcr': - return 'application/x-director'; - // We can't support this sources properly, so return false. - case 'youtube': - case 'googlevideo': - return false; - case 'ogg': - return 'application/ogg'; - default: - // We couldn't detect the mime-type, so return false. - return false; + // We save the upload here because we can't know the correct path until the file is saved. + if (!$file = file_save_upload('default_video_thumb_upload', $validators, $destination)) { + // No upload to save we hope... or file_save_upload() reported an error on its own. + return; } -} -/** - * Generates the HTML for any object parameters in an embedded video. - * - * @param $node the node which is being played - * - * @return - * string with the parameters in HTML form. - */ -function _video_get_parameters(&$node) { - - // call hook_v_get_params - $param_value = module_invoke_all('v_get_params', $node); - - $output = ''; - foreach ($param_value as $param => $value) { - $output .= '\n'; + // Remove old image (if any) & clean up database. + $old_default = $form_state['values']['default_video_thumb']; + if (!empty($old_default['fid'])) { + if (file_delete(file_create_path($old_default['filepath']))) { + db_query('DELETE FROM {files} WHERE fid=%d', $old_default['fid']); + } } - return $output; -} - -/** - * Return true if the video support auto resolution -*/ -function video_support_autoresolution($node) { - $info = video_get_type_info($node->vtype); - $has_hook = module_hook('video_' . $node->vtype, 'v_auto_resolution'); - return $has_hook && isset($info[$node->vtype]['#autoresolution']) && $info[$node->vtype]['#autoresolution']; + // Make the file permanent and store it in the form. + file_set_status($file, FILE_STATUS_PERMANENT); + $file->timestamp = time(); + $form_state['values']['default_video_thumb'] = (array)$file; } - -/** - * Return true if the video support auto playtime -*/ -function video_support_autoplaytime($node) { - $info = video_get_type_info($node->vtype); - $has_hook = module_hook('video_' . $node->vtype, 'v_auto_playtime'); - return $has_hook && isset($info[$node->vtype]['#autoplaytime']) && $info[$node->vtype]['#autoplaytime']; +function video_widget_settings_file_path_validate($element, &$form_state) { + //lets prepend our video folder to the path settings. first truncate videos/ off the end if it exists. + $form_state['values']['file_path'] = 'videos/'. $form_state['values']['file_path']; } - -/** - * Get the resolution options array to use on the video form +/* + * #options helper function to set our key=value for the form api. */ -function _video_get_resolution_options() { - +function video_explode($delimeter, $dimensions) { $options = array(); - - $i = 1; - while($i <= 4) { - if(variable_get('video_resolution_'.$i.'_value', '') != '') { // only if we have a value - $options[$i] = variable_get('video_resolution_'.$i.'_name', ''); + $values = explode($delimeter, $dimensions); + foreach($values as $value) { + //lets check we have a value and its in the right format + if(!empty($value) && video_format_right($value)) { + $options[trim($value)] = trim($value); } - $i++; } return $options; } +function video_format_right($value) { + $format = explode("x", $value); + if(!isset($format[0]) || !is_numeric(trim($format[0]))) return false; + if(!isset($format[1]) || !is_numeric(trim($format[1]))) return false; + return true; +} -/** - * Get the selected resolution id from the videox and videoy fields +/* + * Default video dimensions. */ -function _video_get_resolution_selected_option($node) { - $value = $node->videox . "x" . $node->videoy; - - $i = 1; - while($i <= 4) { - if(variable_get('video_resolution_'.$i.'_value', '') == $value) { - return $i; - } - - $i++; - } +function video_default_dimensions() { + return "176x144\n352x288\n704x576\n1408x1152\n128x96\n160x120\n320x240\n640x480\n800x600\n1024x768\n1600x1200\n2048x1024\n1280x1024\n2560x2048\n5120x4096\n852x480\n1366x768\n1600x1024\n1920x1200\n2560x1600\n3200x2048\n3840x2400\n6400x4096\n7680x4800\n320x200\n640x350\n852x480\n1280x720\n1920x1080"; } -/** - * Scale a video to match the desired width +/* + * Utility function to remove all files and directories recursively. */ -function _video_scale_video(&$node) { - $def_width = (int) variable_get("video_resolution_width", 400); - - if (!$node->videox || !$node->videoy) { - $height = $def_width * 3 / 4; - } else { - $height = $def_width * ($node->videoy / $node->videox); // do you remember proportions?? :-) - } - - $height = round($height); - // add one if odd - if($height % 2) { - $height++; +function rmdirr($dir) { + if($objs = glob($dir."/*")) { + foreach($objs as $obj) { + is_dir($obj)? rmdirr($obj) : unlink($obj); + } } - $node->video_scaled_x = $def_width; - $node->video_scaled_y = $height; -} - -/** - * Implementation of hook_theme(). - */ -function video_theme() { - return array( - 'video_page' => array( - 'arguments' => array(), - ), - 'video_fields' => array( - 'arguments' => array('fields' => NULL), - ), - 'video_format_play' => array( - 'arguments' => array('output' => NULL,'url' => NULL,'title' => NULL,'link_text' => NULL), - ), - 'video_get_scripvt' => array( - 'arguments' => array(), - ), - 'video_player' => array( - 'arguments' => array('node' => NULL), - ), - 'video_teaser' => array( - 'arguments' => array('node' => NULL,'teaser' => NULL,'page' => NULL), - ), - 'video_view' => array( - 'arguments' => array('node' => NULL,'teaser' => NULL,'page' => NULL), - ), - ); -} + @rmdir($dir); +} \ No newline at end of file diff --git a/video.theme.inc b/video.theme.inc new file mode 100644 index 0000000..dfa36e5 --- /dev/null +++ b/video.theme.inc @@ -0,0 +1,105 @@ +'; + } + + if ($getsize) { + // Use cached width and height if available. + if (!empty($file['data']['width']) && !empty($file['data']['height'])) { + $attributes['width'] = $file['data']['width']; + $attributes['height'] = $file['data']['height']; + } + // Otherwise pull the width and height from the file. + elseif (list($width, $height, $type, $image_attributes) = @getimagesize($file['filepath'])) { + $attributes['width'] = $width; + $attributes['height'] = $height; + } + } + + if (!empty($title)) { + $attributes['title'] = $title; + } + + // Alt text should be added even if it is an empty string. + $attributes['alt'] = $alt; + + // Add a timestamp to the URL to ensure it is immediately updated after editing. + $query_string = ''; + if (isset($file['timestamp'])) { + $query_character = (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE && variable_get('clean_url', '0') == '0') ? '&' : '?'; + $query_string = $query_character . $file['timestamp']; + } + + $url = file_create_url($file['filepath']) . $query_string; + $attributes['src'] = $url; + $attributes = drupal_attributes($attributes); + return ''; +} + +function theme_video_widget_preview($item) { + return theme('filefield_widget_preview', $item); +} + +function theme_video_widget_video_thumb($item = NULL) { + return '
' . theme('video_image', $item, '', '', '', FALSE) . '
'; +} + +/** + * @defgroup "Theme Callbacks" + * @{ + * @see uploadfield_theme(). + */ +function theme_video_image($file, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE, $imagecache = FALSE) { + $file = (array)$file; + //if this is imagecache skip this as the file might not be created yet + if (!$imagecache && !is_file($file['filepath'])) { + return ''; + } + + if ($getsize && $imagecache && ($image = image_get_info($file['filepath']))) { + $attributes['width'] = $image['width']; + $attributes['height'] = $image['height']; + } + elseif ($getsize) { + // Use cached width and height if available. + if (!empty($file['data']['width']) && !empty($file['data']['height'])) { + $attributes['width'] = $file['data']['width']; + $attributes['height'] = $file['data']['height']; + } + // Otherwise pull the width and height from the file. + elseif (list($width, $height, $type, $image_attributes) = @getimagesize($file['filepath'])) { + $attributes['width'] = $width; + $attributes['height'] = $height; + } + } + + if (!empty($title)) { + $attributes['title'] = $title; + } + + // Alt text should be added even if it is an empty string. + $attributes['alt'] = $alt; + + // Add a timestamp to the URL to ensure it is immediately updated after editing. + $query_string = ''; + if (isset($file['timestamp'])) { + $query_character = (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE && variable_get('clean_url', '0') == '0') ? '&' : '?'; + $query_string = $query_character . $file['timestamp']; + } + + $url = file_create_url($file['filepath']) . $query_string; + $attributes['src'] = $url; + $attributes = drupal_attributes($attributes); + return ''; +} \ No newline at end of file diff --git a/video_formatter.inc b/video_formatter.inc new file mode 100644 index 0000000..0707284 --- /dev/null +++ b/video_formatter.inc @@ -0,0 +1,177 @@ +load_video($element['#item']['fid']); + if($video->video_status == VIDEO_RENDERING_ACTIVE || $video->video_status == VIDEO_RENDERING_PENDING) { + return theme('video_inprogress'); + } + else if ($video->video_status == VIDEO_RENDERING_FAILED) { + return theme('video_encoding_failed'); + } + } + return video_get_player($element); +} + +/* + * Renders the video thumbnail as a link to the node page. + */ +function theme_video_formatter_video_nodelink($element, $imagecache = FALSE) { + // Inside a view $element may contain null data. In that case, just return. + if (empty($element['#item']['fid'])) return ''; + //setup our thumbnail object + module_load_include('inc', 'video', '/includes/video_helper'); + $video_helper = new video_helper; + $thumbnail = $video_helper->thumbnail_object($element); + //get our themed image + $image = theme('video_image', $thumbnail, $thumbnail->alt, $thumbnail->title, '', TRUE, $imagecache); + $class = 'popups video video-nodelink video-'. $element['#field_name']; + return l($image, 'node/'. $element['#node']->nid, array('attributes' => array('class' => $class), 'html' => TRUE)); +} + +/* + * Renders the video thumbnail linked to the absolute filepath of the video. Colorbox is introduced to handle + * the video in an overlay. + * + * @todo Very unstable, need better methods of integration. Seems hackish right now to find out what flv player is being + * used. We are also using jmedia for all other filetypes. + */ +function theme_video_formatter_video_colorbox($element, $imagecache = FALSE) { + global $base_path; + if (!module_exists('colorbox')) { + drupal_set_message(t('You must download and enable !colorbox for this formatter.', array('!colorbox' => l(t('Colorbox'), 'http://www.drupal.org/project/colorbox'))), 'error'); + return theme('video_formatter_video_nodelink', $element); + } + + // Inside a view $element may contain null data. In that case, just return. + if (empty($element['#item']['fid'])) return ''; + + //load up our media plugins + drupal_add_js(drupal_get_path('module', 'video') .'/js/jquery.media.js'); + drupal_add_js(drupal_get_path('module', 'video') .'/js/jquery.metadata.js'); + drupal_add_js(drupal_get_path('module', 'video') .'/js/flowplayer-3.2.0.min.js'); + + //setup our video object + module_load_include('inc', 'video', '/includes/video_helper'); + $video_helper = new video_helper; + $video = $video_helper->video_object($element); + + $action = swftools_get_action($video->filepath); + $player = swftools_get_player($action); + $path = explode("_", $player); + $player_url = $base_path . swftools_get_player_path() .'/'. $path[0] .'/'. variable_get($player .'_file', ''); + if(stristr($player, 'flowplayer')) { + $player = 'flowplayer'; + } + //add our default settings to the Drupal.settings object + $settings = array('video' => array( + 'flvplayer' => $player_url, + 'autoplay' => $video->autoplay, + 'autobuffer' => $video->autobuffering, + 'player' => $player, + )); + drupal_add_js($settings, 'setting'); + $image = theme('video_image', $video->thumbnail, $video->thumbnail->alt, $video->thumbnail->title, '', TRUE, $imagecache); + $class = 'video-box video-'. $element['#field_name'] .'{width:\''.$video->player_width.'px\', height:\''.$video->player_height.'px\', player:\''.$player.'\'}'; + return l($image, $video->url, array('attributes' => array('class' => $class), 'html' => TRUE)); +} + +/* + * We are using the jMedia library to output our video files. + * + * @todo Does not work with flv files as this requires an actual flv player. Need to figure out how to best integrate the player + * into this function. + * + * We are outputing an anchor to the videofile. The jMedia functions will overtake this anchor and setup our object/embed tags. + */ +function theme_video_formatter_video_media_js($element) { + drupal_add_js(drupal_get_path('module', 'video') .'/js/jquery.media.js'); + drupal_add_js(drupal_get_path('module', 'video') .'/js/jquery.metadata.js'); + //setup our video object + module_load_include('inc', 'video', '/includes/video_helper'); + $video_helper = new video_helper; + $video = $video_helper->video_object($element); + //lets output the link to be overtaken by jmedia + $link = l($video->filename, $video->url, array('attributes' => array('class' => 'jmedia {width: '. $video->player_width .', height: '. $video->player_height .', autoplay: '. $video->autoplay .'}'))); + return $link; +} + +/** + * Displays a "encoding in progress message" + */ +function theme_video_inprogress() { + return '
'. t('This video is currently being processed. Please wait.') . '
'; +} + +/** + * Display an "encoding failed" message" + */ +function theme_video_encoding_failed() { + return '
'. t('The video conversion process has failed. You might want to submit a simpler video format like mpeg or divx avi.
If the problem persists please contact website administrators.') . '
'; +} + +function theme_video_flv($video, $node) { + if ($video->flash_player == 'swftools') { + $options = array( + 'params' => array( + 'width' => $video->player_width, + 'height' => $video->player_height, + ), + 'othervars' => array( + //@todo: swftools bug, can't enable this until they fix their pathing for the images. + //'image' => $video->thumbnail->swfthumb, + ), + ); + $themed_output = swf($video->url, $options); + } + elseif ($video->flash_player == 'flowplayer') { + $options = array( + 'clip' => array( + 'url' => $video->url, + 'autoPlay' => $video->autoplay, + 'autoBuffering' => $video->autobuffering, + ), + ); + $themed_output = theme( + 'flowplayer', + $options, + $video->formatter, + array('style' => 'width:' . $video->player_width . 'px;height:' . $video->player_height . 'px;') + ); + } + else { + $themed_output = t('No flash player has been setup. ' . l(t('Please select a player to play Flash videos.'), 'admin/settings/video/players')); + } + return theme('video_play_flv', $video, $node, $themed_output); +} + +function theme_video_formatter_imagecache($element) { + // Inside a view $element may contain NULL data. In that case, just return. + if (empty($element['#item']['fid'])) { + return ''; + } + + // Extract the preset name from the formatter name. + list($namespace, $theme_function) = explode('__', $element['#formatter'], 2); + if ($preset = imagecache_preset_by_name($namespace)) { + $element['imagecache_preset'] = $namespace; + } + //return $theme_function; + return theme('video_formatter_'. $theme_function, $element, TRUE); +} \ No newline at end of file diff --git a/views/video.views.inc b/views/video.views.inc new file mode 100644 index 0000000..5f2c46f --- /dev/null +++ b/views/video.views.inc @@ -0,0 +1,77 @@ + array( + 'path' => drupal_get_path('module', 'video') . '/views', + ), + 'handlers' => array( + // field handlers + 'video_views_handler_field_data' => array( + 'parent' => 'content_handler_field', + ), + ), + ); +} + +/** + * Implementation of hook_views_data() + */ +function video_views_data() { + $data = array(); + $data['video']['table']['group'] = t('Video'); + $widgets = array('videoftp_widget','uploadfield_widget'); + foreach (content_fields() as $field) { + if($field['module'] == 'filefield' && isset($field['widget']['type']) && in_array($field['widget']['type'], $widgets)) { + $views_data = content_views_field_views_data($field); + $table_alias = content_views_tablename($field); + $db_info = content_database_info($field); + + $title = t('@label (!name) thumbnail', array('@label' => t($field['widget']['label']), '!name' => $field['field_name'])); + $types = array(); + foreach (content_types() as $type) { + if (isset($type['fields'][$field['field_name']])) { + // TODO : run check_plain here instead of on the imploded string below ? + $types[] = $type['name']; + } + } + + $additional_fields = array(); + foreach ($db_info['columns'] as $column => $attributes) { + // Select explicitly enabled field columns. + if (!empty($attributes['views'])) { + $db_columns[$column] = $attributes; + } + // Ensure all columns are retrieved. + $additional_fields[$attributes['column']] = $attributes['column']; + } + + $data[$table_alias][$field['field_name'].'_thumbnail'] = array( + 'group' => t('Video'), + 'title' => $title, + 'help' => t($field_types[$field['type']]['label']) .' - '. t('Appears in: @types', array('@types' => implode(', ', $types))), + ); + $data[$table_alias][$field['field_name'].'_thumbnail']['field'] = array( + 'title' => $title, + 'field' => $db_info['columns']['data']['column'], + 'table' => $db_info['table'], + 'handler' => 'video_views_handler_field_data', + 'click sortable' => FALSE, + 'content_field_name' => $field['field_name'], + 'additional fields' => $additional_fields, + 'access callback' => 'content_access', + 'access arguments' => array('view', $field), + ); + } + } + return $data; +} diff --git a/views/video_views_handler_field_data.inc b/views/video_views_handler_field_data.inc new file mode 100644 index 0000000..d082e46 --- /dev/null +++ b/views/video_views_handler_field_data.inc @@ -0,0 +1,60 @@ +{$this->field_alias}); + $filepath = $data['video_thumb']; + + // We're down to a single node here, so we can retrieve the actual field + // definition for the node type being considered. + $field = content_fields($this->content_field['field_name'], $values->{$this->aliases['type']}); + $options = $this->options; + $db_info = content_database_info($field); + + // Build a pseudo-node from the retrieved values. + $node = drupal_clone($values); + $node->type = $values->{$this->aliases['type']}; + $node->nid = $values->{$this->aliases['nid']}; + $node->vid = $values->{$this->aliases['vid']}; + // Some formatters need to behave differently depending on the build_mode + // (for instance: preview), so we provide one. + $node->build_mode = NODE_BUILD_NORMAL; + + $item = array(); + foreach ($db_info['columns'] as $column => $attributes) { + $item[$column] = $values->{$this->aliases[$attributes['column']]}; + } + + $item['#delta'] = $field['multiple'] ? $values->{$this->aliases['delta']} : 0; + //added for thumbnails this should work. + $file = pathinfo($filepath); + $item['filepath'] = $filepath; + $item['filename'] = $file['basename']; + $item['filemime'] = file_get_mimetype($file['basename']); + $item['filesize'] = filesize($filepath); + + // Render items. + $formatter_name = $options['format']; + if ($formatter = _content_get_formatter($formatter_name, $field['type'])) { + if (content_handle('formatter', 'multiple values', $formatter) == CONTENT_HANDLE_CORE) { + // Single-value formatter. + $output = content_format($field, $item, $formatter_name, $node); + } + else { + // Multiple values formatter - we actually have only one value to display. + $output = content_format($field, array($item), $formatter_name, $node); + } + return $this->render_link($output, $values); + } + return ''; + } +} \ No newline at end of file -- cgit v1.2.3