Skip to content

Instantly share code, notes, and snippets.

@pbuyle
Created December 20, 2011 12:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pbuyle/1501391 to your computer and use it in GitHub Desktop.
Save pbuyle/1501391 to your computer and use it in GitHub Desktop.
<?php
/**
* Implements hook_field_info().
*/
function MODULE_field_info() {
return array(
'video_thumbnail_settings' => array(
'label' => t('Video Thumbnail Settings'),
'description' => t('Configure Video Thumbnail display.'),
'settings' => array(),
'instance_settings' => array(),
'default_widget' => 'video_thumbnail_settings_widget',
'default_formatter' => 'video_thumbnail_settings_formatter',
'no_ui' => FALSE // TODO: Set to TRUE and automatically create a field instance when needed
),
);
}
/**
* Implements hook_field_widget_info().
*/
function MODULE_field_widget_info() {
return array(
'video_thumbnail_settings_widget' => array(
'label' => t('Default'),
'field types' => array('video_thumbnail_settings'),
),
);
}
/**
* Implements hook_field_widget_form().
*/
function MODULE_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$file =& $form_state['file'];
if (!empty($file->uri)) {
$scheme = file_uri_scheme($file->uri);
$local_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
if (isset($local_wrappers[$scheme]) && ($movie = MODULE_ffmpeg_movie($file)) && $movie->hasVideo()) {
if (isset($form_state['values'])) {
$path = array_merge($element['#field_parents'], array($field['field_name'], $langcode));
$path_exists = FALSE;
$values = drupal_array_get_nested_value($form_state['values'], $path, $path_exists);
if ($path_exists) {
$items = $values;
drupal_array_set_nested_value($form_state['values'], $path, NULL);
}
}
else {
foreach ($items as &$item) {
switch ($item['mode']) {
case 'select':
$item['thumbnail']['framenumber'] = $item['value'];
break;
case 'media':
$item['thumbnail']['fid'] = $item['value'];
break;
}
}
}
$item = !empty($items[$delta]) ? $items[$delta] : array();
$mode = !empty($item['mode']) ? $item['mode'] : 'random';
$element += array(
'#type' => 'fieldset',
'#title' => 'Thumbnail',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#element_validate' => array('MODULE_field_widget_validate'),
'mode' => array(
'#type' => 'select',
'#default_value' => $mode,
'#options' => array(
'random' => t('Extract random thumbnail'),
'select' => t('Select extracted thumbnail'),
'media' => t('Select image as thumbnail')
),
'#return_value' => 1,
'#ajax' => array(
'callback' => 'MODULE_field_widget_form_thumbnail_callback',
'wrapper' => 'thumbnail-wrapper',
)
),
'thumbnail' => array(
'#prefix' => '<div id="thumbnail-wrapper">',
'#suffix' => '</div>',
)
);
switch ($mode) {
case 'random':
$element['thumbnail'] += array(
'#type' => 'container',
);
break;
case 'select':
$framerate = $movie->getFrameRate();
$framecount = $movie->getFrameCount();
$interval = 10;
$options = array();
$framenumbers = array();
$framenumber = 1;
while ($framenumber < $framecount) {
$framenumbers[] = (int)$framenumber;
$framenumber += $interval * $framerate;
}
$framenumbers[] = (int)($framecount - ($framerate * 0.25));
foreach(MODULE_video_extract_frames($file, $framenumbers) as $framenumber => $path) {
$options[$framenumber] = theme('image_style', array(
'style_name' => 'thumbnail',
'path' => $path,
'title' => format_interval($framenumber / $framerate)
));
}
$element['thumbnail'] += array(
'#type' => 'container',
);
$element['thumbnail']['framenumber'] = array(
'#type' => 'radios',
'#options' => $options,
'#default_value' => $item['thumbnail']['framenumber'],
'#required' => FALSE,
);
break;
case 'media':
$element['thumbnail'] += array(
'#type' => 'media',
'#mutliple' => FALSE,
'#title' => FALSE,
'#default_value' => array('fid' => $item['thumbnail']['fid']),
'#description' => t('Select an existing image or upload a new one to be used as thumbnail for this video.'),
'#media_options' => array(
'global' => array(
'types' => array('image'),
)
)
);
break;
}
}
}
$element['#attached']['css'][] = drupal_get_path('module', 'MODULE') . '/css/MODULE.theme.css';
$element['#attached']['js'][] = drupal_get_path('module', 'MODULE') . '/js/MODULE.widget.js';
return $element;
}
/**
* Implements hook_field_widget_validate().
*/
function MODULE_field_widget_validate($element, &$form_state) {
$item = array('mode' => $element['mode']['#value']);
switch($item['mode']) {
case 'random':
$item['value'] = 0;
break;
case 'select':
$item['value'] = !empty($element['thumbnail']['framenumber']) ? $element['thumbnail']['framenumber']['#value'] : 0;
break;
case 'media':
$item['value'] = !empty($element['thumbnail']['fid']) ? $element['thumbnail']['fid']['#value'] : 0;
break;
}
form_set_value($element, $item, $form_state);
}
/**
* Ajax callback to update the 'thumbnail' element.
*
* @see MODULE_field_widget_form().
*/
function MODULE_field_widget_form_thumbnail_callback($form, $form_state) {
$triggering_element = $form_state['triggering_element'];
$form_state['triggering_element']['#array_parents'];
if ($triggering_element['#type'] == 'select') {
$parents = array_slice($triggering_element['#array_parents'], 0, -1);
}
else {
$parents = array_slice($triggering_element['#array_parents'], 0, -2);
}
$parents[] = 'thumbnail';
return drupal_array_get_nested_value($form, $parents);
}
/**
* Implements hook_field_formatter_info().
*/
function MODULE_field_formatter_info() {
return array(
'video_thumbnail_settings_formatter' => array(
'label' => t('Default'),
'field types' => array('video_thumbnail_settings'),
),
);
}
/**
* Formatter for the video_thumbnail_settings_formatter.
*/
function MODULE_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
//Don't do anything for video_thumbnail_settings_formatter
}
/**
* Implements hook_field_formatter_settings_form().
*/
function MODULE_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$element = array();
//Don't do anything for video_thumbnail_settings_formatter
return $element;
}
/**
* Implements hook_field_is_empty().
*/
function MODULE_field_is_empty($item, $field) {
if (!empty($item['mode'])) {
switch ($item['mode']) {
case 'random':
return FALSE;
break;
case 'select':
case 'media':
return empty($item['value']);
break;
}
}
else {
return TRUE;
}
}
/**
* Implements hook_field_validate().
*/
function MODULE_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
$function = __FUNCTION__ . '_' . $field['type'];
if (function_exists($function)) {
return $function($entity_type, $entity, $field, $instance, $langcode, $items, $errors);
}
}
/**
* Implements hook_field_validate() for the video_thumbnail_settings filed type.
*/
function MODULE_field_validate_video_thumbnail_settings($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
if ($entity_type == 'file') {
foreach ($items as $delta => $item) {
switch ($item['mode']) {
case 'random':
break;
case 'select':
$file = file_load($entity->fid);
$scheme = file_uri_scheme($file->uri);
$local_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
if (isset($local_wrappers[$scheme]) && ($movie = MODULE_ffmpeg_movie($file)) && $movie->hasVideo()) {
$framecount = $movie->getFrameCount();
if ($item['value'] < 1 || $item['value'] >= $framecount) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'video_thumbnail_settings_framenumber',
'message' => t('Invalid thumbnail frame number.'),
);
}
}
break;
case 'media':
$thumbnail = file_load($item['value']);
if (!$thumbnail || $thumbnail->type !== 'image') {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'video_thumbnail_settings_media',
'message' => t('Invalid thumbnail media.'),
);
}
break;
default:
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'video_thumbnail_settings_mode',
'message' => t('Invalid thumbnail mode.'),
);
break;
}
}
}
}
<?php
// @todo Remove when http://drupal.org/node/977052 is fixed.
require_once dirname(__FILE__) . '/MODULE.field.inc';
/**
* Implement hook_file_formatter_info().
*/
function MODULE_file_formatter_info() {
$formatters = array();
if (extension_loaded('ffmpeg')) {
$formatters['video_thumbnail'] = array(
'label' => t('Video Thumbnail'),
'default settings' => array(
'image_style' => '',
'field' => '',
),
'view callback' => 'MODULE_file_formatter_video_thumbnail_view',
'settings callback' => 'MODULE_file_formatter_video_thumbnail_settings',
);
}
return $formatters;
}
/**
* Helper function to retrieve the path to an extracted video frame as a JPG
* file.
*
* @param string $uri
* The URI of the video file.
* @param int $framenumber
* The extracted frame number.
*
* @return
* The path for an extracted video frame as a JPG file. The actual file may not
* exist.
*/
function MODULE_video_frame_path($uri, $framenumber = 0) {
$scheme = file_uri_scheme($uri);
if ($scheme) {
$path = file_uri_target($uri);
}
else {
$path = $uri;
$scheme = file_default_scheme();
}
return $scheme . '://video_frames/' . $scheme . '/' . preg_replace('/\.[^.]+$/', "-$framenumber.jpg", $path);
}
/**
* Build the poster frame for an video file.
*
* @param object $file
* The (fully loaded) file entity for which to build a poster frame.
* @param array $settings
* The build settings, the following properties are supported
* - 'mode': one of 'select', 'random' and 'media'.
* - 'value': if 'mode' is 'media', the fid of (image) file entity to use as
* poster frame. If 'mode' is 'select', the frame number of the
* frame to extract as poster frame. Not used if 'mode' is 'random'
*
* @return
* The path to an image file to use as poster frame. Or NULL if no poster frame
* could be build (invalid file or settings).
*/
function MODULE_build_video_poster_frame($file, $settings) {
if (is_object($file) && !empty($file->type) && $file->type === 'video') {
switch ($settings['mode']) {
case 'media':
$thumbnail_file = file_load($settings['value']);
if ($thumbnail_file && ($thumbnail_file->type == 'image')) {
$path = $thumbnail_file->uri;
}
break;
case 'random':
$settings['value'] = 0;
case 'select':
$path = MODULE_video_extract_frame($file, $settings['value']);
break;
}
if (!empty($path) && file_exists($path)) {
return $path;
}
}
return NULL;
}
/**
* Helper function to extract a single frame from a video file. The extracted
* frame file name is build by appending the frame number to the video file
* name. If the given frame bnumber is 0 (or not a number), then a random frame
* is extracted but 0 is used as suffix when building the frame file name.
*
* @param object $file
* A file entity.
* @param int $framenumber
* (optional) The frame from the movie to extract.
*
* @return
* The path to the extracted frame as a unmanaged JPG file. Or NULL if no frame
* could be extracted.
*
*/
function MODULE_video_extract_frame($file, $framenumber = 0) {
if (!is_numeric($framenumber)) {
$framenumber = 0;
}
$paths = MODULE_video_extract_frames($file, array($framenumber));
if (empty($paths)) {
return NULL;
}
else {
return reset($paths);
}
}
/**
* Helper function to extract multiples frame sfrom a video file. The extracted
* frames file names are build by appending the frame numbers to the video file
* name. If a given frame number is 0 (or not a number), then a random frame is
* extracted but 0 is used as suffix when building the frame file name.
*
* If for some reason a frame couldn't get extracted (ie. it frame number is
* lower than the frame count but ffmpeg_movie::getFrame() returns null). Then
* the first found frame before the requested one is used.
*
* @param object $file
* A file entity.
* @param int $framenumbers
* (optional) The frames from the movie to extract.
*
* @return
* An array of paths for the extracted frame as a unmanaged JPG files, indexed
* by the (requested) frame number.
*
*/
function MODULE_video_extract_frames($file, $framenumbers, $suffix = TRUE) {
set_time_limit(0);
$scheme = file_uri_scheme($file->uri);
$local_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
$paths = array();
if (isset($local_wrappers[$scheme])) {
$suffix = $suffix || (count($framenumbers) > 1);
if (($movie = MODULE_ffmpeg_movie($file)) && $movie->hasVideo()) {
sort($framenumbers);
$framecount = $movie->getFrameCount();
$framerate = $movie->getFrameRate();
foreach ($framenumbers as $framenumber) {
$path = MODULE_video_frame_path($file->uri, $framenumber);
if (!file_exists($path)) {
$requested_framenumber = $framenumber;
if ($framenumber < 1 || !is_numeric($framenumber)) {
$framenumber = rand(1, $framecount - 1);
}
$frame = NULL;
// Loop until we get a frame.
while (!$frame && $framenumber > 0) {
$frame = $movie->getFrame($framenumber--);
}
if ($frame) {
$resource = $frame->toGDImage();
file_prepare_directory(drupal_dirname($path), FILE_CREATE_DIRECTORY);
if (imagejpeg($resource, drupal_realpath($path))) {
$paths[$requested_framenumber] = $path;
}
imagedestroy($resource);
}
}
else {
$paths[$framenumber] = $path;
}
}
}
}
return $paths;
}
/**
* Return a ffmpeg_movie object for a file. ffmpeg_movie objects are statically
* cached.
*
* @param object $file
* A file object.
*
* @return
* A ffmpeg_movie instance. Or null if $file is not a local file or if an
* instance could not be created.
*
* @see ffmpeg_movie::__construct().
*/
function MODULE_ffmpeg_movie($file) {
$cache =& drupal_static(__FUNCTION__, array());
$scheme = file_uri_scheme($file->uri);
$local_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
if (isset($local_wrappers[$scheme])) {
if (!isset($cache[$file->uri])) {
return $cache[$file->uri] = @(new ffmpeg_movie(drupal_realpath($file->uri)));
}
else {
return $cache[$file->uri];
}
}
return NULL;
}
/**
* View callback for the Video Thumbnail file formatter.
*/
function MODULE_file_formatter_video_thumbnail_view($file, $display, $langcode) {
$settings = !empty($display['settings']['field']) ? field_get_items('file', $file, $display['settings']['field']) : FALSE;
if ($settings === FALSE) {
$settings = array('mode' => 'random', 'value' => 0);
}
else {
$settings = reset($settings);
}
$path = MODULE_build_video_poster_frame($file, $settings);
if (!empty($path)) {
if (!empty($display['settings']['image_style'])) {
$element = array(
'#theme' => 'image_style',
'#style_name' => $display['settings']['image_style'],
'#path' => $path,
);
}
else {
$element = array(
'#theme' => 'image',
'#path' => $path,
);
}
return $element;
}
}
/**
* Settings callback for the Video Thumbnail file formatter.
*/
function MODULE_file_formatter_video_thumbnail_settings($form, &$form_state, $settings) {
$element = array();
$element['image_style'] = array(
'#title' => t('Image style'),
'#type' => 'select',
'#options' => image_style_options(FALSE),
'#default_value' => $settings['image_style'],
'#empty_option' => t('None (original image)'),
);
// TODO: Remove this setting and use a single automatically create field instance
$fields = array();
foreach (field_info_instances('file', $form['#file_type']) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
if ($field['type'] == 'video_thumbnail_settings') {
$fields[$field['field_name']] = $instance['label'];
}
}
if (!empty($fields)) {
$element['field'] = array(
'#title' => t('Thumbnail frame from field:'),
'#type' => 'select',
'#options' => $fields,
'#default_value' => $settings['field'],
'#empty_option' => t('None (random frame)'),
);
}
return $element;
}
/**
* Implements hook_file_formatter_info_alter().
*/
function MODULE_file_formatter_info_alter(&$info) {
// Orverride the MediaElement File Field formatter wrapped by File entity to
// use our own callbacks and extend the available settings.
$info['file_field_mediaelement_video']['view callback'] = 'MODULE_file_formatter_mediaelement_view';
$info['file_field_mediaelement_video']['settings callback'] = 'MODULE_file_formatter_mediaelement_settings';
$info['file_field_mediaelement_video']['default settings']['poster'] = '';
$info['file_field_mediaelement_video']['default settings']['detect_size'] = FALSE;
}
/**
* Settings form callback for the mediaelement video file formatter (not file
* field).
*
* @see file_entity_file_formatter_file_field_settings()
*/
function MODULE_file_formatter_mediaelement_settings($form, &$form_state, $settings, $formatter_type, $file_type, $view_mode) {
// Get the settign form as provided by the File Entity module.
$form = file_entity_file_formatter_file_field_settings($form, $form_state, $settings, $formatter_type, $file_type, $view_mode);
// Additional settings
$poster_options = array(
NULL => t('None'),
'random' => t('Select a random frame'),
);
$fields = array();
foreach (field_info_instances('file', $file_type) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
if ($field['type'] == 'video_thumbnail_settings') {
$poster_options['field_' . $field['field_name']] = $instance['label'];
}
if (in_array($field['type'], array('file', 'link_field', 'media'))) {
$tracks_options[$field['field_name']] = $instance['label'];
}
}
$form['detect_size'] = array(
'#type' => 'checkbox',
'#title' => t('Detect video size'),
'#description' => t('If checked, the height and width of the video will be detected and the Height and Witdh settings will be used as maximum values.'),
'#default_value' => $settings['detect_size']
);
$form['poster'] = array(
'#type' => 'select',
'#title' => t('Poster frame'),
'#options' => $poster_options,
'#default_value' => $settings['poster'],
);
if (!empty($tracks_options)) {
$form['tracks'] = array(
'#type' => 'fieldset',
'#title' => t('Text tracks'),
'#description' => t('Select the fields used to provide <a href="@url">Text tracks</a> for the video element.', array(
'@url' => 'http://www.w3.org/TR/html5/video.html#text-track',
)),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
'#tree' => TRUE,
);
$kinds = array(
'' => t("Don't use as tracks"),
'subtitles' => t('Subtitles'),
'captions' => t('Caption'),
'descriptions' => t('Descriptions'),
'chapters' => t('Chapters'),
'metadata' => t('Metadata'),
);
foreach($tracks_options as $field_name => $title) {
$form['tracks'][$field_name] = array(
'#type' => 'select',
'#title' => $title,
'#options' => $kinds,
'#default_value' => !empty($settings['tracks'][$field_name]) ? $settings['tracks'][$field_name] : NULL,
);
}
}
return $form;
}
/**
* View callback for the mediaelement video file formatter (not file field).
*
* @see file_entity_file_formatter_file_field_view();
*/
function MODULE_file_formatter_mediaelement_view($file, $display, $langcode) {
$element = file_entity_file_formatter_file_field_view($file, $display, $langcode);
if ($element) {
// Apply poster frame settings
if (!empty($display['settings']['poster'])) {
$poster_settings = ($display['settings']['poster'] !== 'random') ? field_get_items('file', $file, substr($display['settings']['poster'], 6)) : FALSE;
if ($poster_settings === FALSE) {
$poster_settings = array('mode' => 'random', 'value' => 0);
}
else {
$poster_settings = reset($poster_settings);
}
$path = MODULE_build_video_poster_frame($file, $poster_settings);
if (!empty($path)) {
$element['#attributes']['poster'] = file_create_url($path);
$element['#attributes']['poster'] = file_create_url($path);
}
}
// Apply size detection settings
if (!empty($display['settings']['detect_size']) && $movie = MODULE_ffmpeg_movie($file)) {
$height = $movie->getFrameHeight();
$width = $movie->getFrameWidth();
$ratio = $width / $height;
if ($display['settings']['width'] > 0 && $width > $display['settings']['width']) {
$height = $display['settings']['width'] / $ratio;
$width = $display['settings']['width'];
}
if ($display['settings']['height'] > 0 && $height > $display['settings']['height']) {
$width = $ratio * $height;
$height = $display['settings']['height'];
}
$element['#settings']['height'] = $element['#attributes']['height'] = $height;
$element['#attached']['js'][0]['data']['mediaelement']['.' . $element['#attributes']['class']]['opts']['videoHeight'] = $height;
$element['#settings']['width'] = $element['#attributes']['width'] = $width;
$element['#attached']['js'][0]['data']['mediaelement']['.' . $element['#attributes']['class']]['opts']['videoWidth'] = $width;
}
// Apply tracks settings
if (!empty($display['settings']['tracks'])) {
foreach (array_filter($display['settings']['tracks']) as $field_name => $kind) {
foreach(field_available_languages('file', $field_name) as $langcode) {
if ($values = field_get_items('file', $file, $field_name, $langcode)) {
$field = field_info_field($field_name);
foreach($values as $value) {
$track = array();
switch($field['type']) {
case 'file':
$value = (object)$value;
$track += array(
'src' => file_create_url($value->uri),
'label' => entity_label('file', $value),
);
break;
case 'media':
$value = $value['file'];
$track += array(
'src' => file_create_url($value->uri),
'label' => entity_label('file', $value),
);
break;
case 'link_field':
// FIXME Does it work ?
$track += array(
'src' => $value['url'],
'label' => check_plain($value['title']),
);
break;
}
if ($track) {
$track += array(
'srclang' => $langcode,
'kind' => $kind,
);
$element['#tracks'][] = $track;
}
}
}
}
}
}
return $element;
}
}
#thumbnail-wrapper .form-radios .form-type-radio {
float: left;
}
#thumbnail-wrapper .form-radios .form-type-radio input {
display: none;
}
#thumbnail-wrapper .form-radios .form-type-radio label {
display: block;
overflow: hidden;
/*border: 1px solid transparent;*/
}
#thumbnail-wrapper .form-radios .form-type-radio label img {
border: 2px dashed transparent;
padding: 1px;
}
#thumbnail-wrapper .form-radios .form-type-radio.selected label img {
border-color: #0074BD;
}
(function($){
Drupal.behaviors.videoThumbnailSettingsField = {
attach: function(context, setting) {
jQuery('#thumbnail-wrapper .form-radios', context)
.once('video-thumbnail-settings-field')
.delegate('input:radio', 'change', function(e){
jQuery(this)
.parents('.form-radios')
.find('.form-type-radio')
.removeClass('selected')
.end()
.end()
.parents('.form-type-radio')
.addClass('selected')
.end()
})
.find('input:checked')
.parents('.form-type-radio')
.addClass('selected');
}
};
}(jQuery));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment