Using oEmbed to deliver video thumbnails in Drupal

We've gone to great lengths over the past few years to create a smooth workflow for allowing clients to insert video on their Drupal sites. We're currently working on a Drupal distribution which needs a really straightforward way for embedding video, one that can be achieved by any type of user.

When we first meet with clients we give them two options for video:

  1. Upload a video file, and an image to use for a thumbnail in listings
  2. Use a third party service such as YouTube and upload a thumbnail image for listings

1. Upload a video file and a thumbnail image

There are many modules available to allow this to happen.

All of these will allow the user to upload a video file and spit it out in a player. However these solutions are not always HTML5 or Mobile friendly. There are solutions using FFMPEG that will allow transcoding/encoding of the file to all the necessary file types, but on shared hosting or even sometimes on a VPS, getting FFMPEG to run can be a headache. One other hurdle to consider is the user entering the content. Not all clients are sophisticated enough to know how to encode their videos to ensure the best quality. They have no idea about selecting the correct codecs and audio compression to allow for an acceptable file size. Then, inevitably, the client will request all of the functionality that YouTube or Vimeo has in it's player, for their site. In this situation we usually advise the client to go with using YouTube, as long as they feel comfortable with that. Lauren's post on Why use YouTube for video hosting? gives a wonderful outline on the reasons to use YouTube. Of course some organizations simply can't or won't use these services and in those situations we build them a custom solution.

2. Use a third party service and upload a thumbnail

So we've convinced the client to use YouTube. What now? We need a way to get their YouTube videos on their site. As with the file upload route, there are many, many ways to do this. The simplest way for most users is to copy and paste the embed code that YouTube provides directly into their page. However this is not very helpful if the video is to appear in listings/views. To allow videos to appear in views we use a CCK field. There are also a plethora of ways to implement that:

I've recently found the oEmbed module, and it's proven to be a wonderful way to embed all kinds of media. oEmbed is a defined format/specification that allows a website to display embedded content (such as photos or videos) when a user posts a link to that resource. That means you can embed a YouTube video simply by pasting the URL of that video, from the YouTube site, into your page content and it will magically load. This is possible because YouTube chooses to deliver an oEmbed version of all of it's content. The oEmbed module for Drupal comes with a formatter for Link fields, so that you can create a link field, which when displayed can show as an embedded video. It works very well, and can be used in views. At this point I was pretty happy. But then realized that I still needed thumbnail images to appear in listings pages. My first instinct was to create an image field so that the user could upload a thumbnail. However I remembered that the oEmbed JSON data that YouTube returned provided a path to a thumbnail.

{
	"provider_url": "http://www.youtube.com/",
	"title": "BBC Formula 1 2012 Intro",
	"html": "<iframe width=\"480\" height=\"270\" src=\"http://www.youtube.com/embed/RN4tRKdolg4?fs=1&feature=oembed\" frameborder=\"0\" allowfullscreen></iframe>",
	"author_name": "Prezes367",
	"height": 270,
	"thumbnail_width": 480,
	"width": 480,
	"version": "1.0",
	"author_url": "http://www.youtube.com/user/Prezes367",
	"provider_name": "YouTube",
	"thumbnail_url": "http://i3.ytimg.com/vi/RN4tRKdolg4/hqdefault.jpg",
	"type": "video",
	"thumbnail_height": 360
}

Why not let YouTube, possibly the largest online video delivery site, handle the thumbnail for me, rather than making more work for the user. Then next step was figuring out how to get that thumbnail to display in place of the video. A quick read through the oEmbed module and I found out that the field is simply using a field formatter to output the embedded video. At this point it became clear that I could create my own thumbnail formatter. I needed to ensure that the thumbnail used image styles if it was to be useful. Site builders will require the ability to resize the thumbnail based on the design. YouTube provides defaults but they may not give the desired result. To allow the thumbnail to use an image style I need to treat it somewhat like a local file. However because the path to the thumbnail image is a remote file, the Remote Stream Wrapper module by Dave Reid was the only way to allow this to happen. This module provides the ability to use external files with file fields without saving the files to your local files directory. Dave Reid was a great help with this and he talked me thorough the process of using remote stream wrapper with image fields. The steps to create this display formatter were relatively straightforward and you can download the module from Drupal.org or get it from Github.

1. Declare the formatter for use on the "link" field

/**
 * Implements hook_field_formatter_info().
 * Create a new field formatter for link fields.
 */
function oembedthumbnail_field_formatter_info() {
  $formatters = array(
    'oembedthumbnail' => array(
      'label' => t('oEmbed Thumbnail'),
      'field types' => array('link_field'),
      'description' => t('Embeds links as thumbnails if possible - otherwise just links them.'),
      'settings' => array('imagestyle' => ''),
    ),
  );
 
  return $formatters;
}

2. Allow the use of image styles on the formatter

/**
 * Implements hook_field_formatter_settings_form().
 * Adds a dropdown to choose image styles on the field settings form.
 */
function oembedthumbnail_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
 
  $element = array();
  $element['imagestyle'] = array(
    '#title' => t('Image Style'),
    '#type' => 'select',
    '#options' => image_style_options(),
  );
 
  return $element;
}

3. Add the code to handle viewing the field

/**
 * Implements hook_field_formatter_view().
 */
function oembedthumbnail_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();
 
  foreach ($items as $delta => $item) {
    $attributes = array();
    $url = url($item['url'], $item + array('external' => TRUE));
    $attributes = array_filter($display['settings']);
 
    $element[$delta] = oembedcore_render_cache('oembedthumbnail', $url, $attributes);
  }
 
  return $element;
}
 
 
/**
 * Implements hook_element_info_alter().
 * We need to make sure our pre-render function is called when generating the thumbnail.
 */
function oembedthumbnail_element_info_alter(&$types) {
  $types['oembedthumbnail']['#pre_render'][] = 'oembedthumbnail_pre_render_thumbnail';
}

4. Do all of the remote file handling

/**
 * #pre_render callback for 'oembedthumbnail' elements.
 *
 * For oEmbed responses, process the thumbnail image according to the chosen image style
 */
function oembedthumbnail_pre_render_thumbnail($element) {
  // Generate a remote url http://youtube.com/oembed/images/....
  $remote_uri = file_create_url($element['#path']);
  // Check if a remote file object exists in the {file_managed} table for this file...
  $remote_file = remote_stream_wrapper_file_load_by_uri($remote_uri);
  // If there is no file object, create one with the remote url  'http://youtube.com/oembed/images/.....
  if (empty($remote_file)) {
    $remote_file = remote_stream_wrapper_file_create_by_uri($remote_uri);
    file_save($remote_file);  // From Dave Reid... Save the file as file_save will be deprecated in remote_stream_wrapper_file_create_by_uri...
  }
  // Now generate the derivative uri, public://styles/{style_name}/etc........
  $derivative_uri = remote_stream_wrapper_image_style_path($element['#display_options']['imagestyle'], $remote_file->uri);
  // From that internal uri generate an internal derative url, http://mysite.com/files/styles/{style_name}/etc.....
  $element['#path'] = file_create_url($derivative_uri);
  return $element;
}