├── .gitignore ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── bower.json ├── lib ├── mep_feature_time_rail_thumbnails.rb └── mep_feature_time_rail_thumbnails │ ├── engine.rb │ └── version.rb ├── mep_feature_time_rail_thumbnails.gemspec └── vendor └── assets └── javascripts └── mep-feature-time-rail-thumbnails.js /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | test/dummy/.sass-cache 9 | *.gem 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Declare your gem's dependencies in mep_feature_time_rail_thumbnails.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use debugger 14 | # gem 'debugger' 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | mep_feature_time_rail_thumbnails (0.0.3) 5 | rails (>= 3.2.17) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actionmailer (4.0.5) 11 | actionpack (= 4.0.5) 12 | mail (~> 2.5.4) 13 | actionpack (4.0.5) 14 | activesupport (= 4.0.5) 15 | builder (~> 3.1.0) 16 | erubis (~> 2.7.0) 17 | rack (~> 1.5.2) 18 | rack-test (~> 0.6.2) 19 | activemodel (4.0.5) 20 | activesupport (= 4.0.5) 21 | builder (~> 3.1.0) 22 | activerecord (4.0.5) 23 | activemodel (= 4.0.5) 24 | activerecord-deprecated_finders (~> 1.0.2) 25 | activesupport (= 4.0.5) 26 | arel (~> 4.0.0) 27 | activerecord-deprecated_finders (1.0.3) 28 | activesupport (4.0.5) 29 | i18n (~> 0.6, >= 0.6.9) 30 | minitest (~> 4.2) 31 | multi_json (~> 1.3) 32 | thread_safe (~> 0.1) 33 | tzinfo (~> 0.3.37) 34 | arel (4.0.2) 35 | builder (3.1.4) 36 | erubis (2.7.0) 37 | hike (1.2.3) 38 | i18n (0.6.9) 39 | mail (2.5.4) 40 | mime-types (~> 1.16) 41 | treetop (~> 1.4.8) 42 | mime-types (1.25.1) 43 | minitest (4.7.5) 44 | multi_json (1.10.0) 45 | polyglot (0.3.4) 46 | rack (1.5.2) 47 | rack-test (0.6.2) 48 | rack (>= 1.0) 49 | rails (4.0.5) 50 | actionmailer (= 4.0.5) 51 | actionpack (= 4.0.5) 52 | activerecord (= 4.0.5) 53 | activesupport (= 4.0.5) 54 | bundler (>= 1.3.0, < 2.0) 55 | railties (= 4.0.5) 56 | sprockets-rails (~> 2.0.0) 57 | railties (4.0.5) 58 | actionpack (= 4.0.5) 59 | activesupport (= 4.0.5) 60 | rake (>= 0.8.7) 61 | thor (>= 0.18.1, < 2.0) 62 | rake (10.3.1) 63 | sprockets (2.12.1) 64 | hike (~> 1.2) 65 | multi_json (~> 1.0) 66 | rack (~> 1.0) 67 | tilt (~> 1.1, != 1.3.0) 68 | sprockets-rails (2.0.1) 69 | actionpack (>= 3.0) 70 | activesupport (>= 3.0) 71 | sprockets (~> 2.8) 72 | thor (0.19.1) 73 | thread_safe (0.3.3) 74 | tilt (1.4.1) 75 | treetop (1.4.15) 76 | polyglot 77 | polyglot (>= 0.3.1) 78 | tzinfo (0.3.39) 79 | 80 | PLATFORMS 81 | ruby 82 | 83 | DEPENDENCIES 84 | mep_feature_time_rail_thumbnails! 85 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 North Carolina State Univesity 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediaElement.js Plugin for Preview Thumbnails 2 | 3 | Hover over the time rail on a [MediaElement.js](http://mediaelementjs.com/) player and see video thumbnails. 4 | 5 | [See a video of this in action](http://jronallo.github.io/mep-feature-time-rail-thumbnails/) 6 | 7 | See it in action on the [NCSU Libraries Rare & Unique Digital Collection site](http://d.lib.ncsu.edu/collections/catalog?f%5Bformat%5D%5B%5D=Video). 8 | 9 | ## Use 10 | 11 | To use this feature you will first need to create an image sprite and a metadata WebVTT file. 12 | 13 | ### Create an Image Sprite 14 | 15 | The idea is to take a snapshot of your video every N seconds and then stitch the images together into a sprite. This means that only one image file needs to be requested from the server and switching between thumbnails can be very quick. 16 | 17 | Here's one way you could create the image sprite using ffmpeg and montage (from ImageMagick): 18 | 19 | ``` 20 | ffmpeg -i "video-name.mp4" -f image2 -vf fps=fps=1/5 video-name-%05d.jpg 21 | montage video-name*jpg -tile 5x -geometry 150x video-name-sprite.jpg 22 | ``` 23 | 24 | First ffmpeg takes a snapshot of your video every 5 seconds. Then montage reduces each image to 150px across and tiles them from left to right 5 across. 25 | 26 | ### Create a WebVTT metadata file 27 | 28 | Once you have the image sprite you'll also need to create a [WebVTT](http://docs.webplatform.org/wiki/concepts/VTT_Captioning) metadata file that contains the URL to your image sprite. For each time range of 5 seconds the URL is given including a [spatial Media Fragment](http://www.w3.org/TR/media-frags/) hash. This gives information about where in your sprite to look for the thumbnail for that time range. Here's an example of what one looks like: 29 | 30 | ``` 31 | WEBVTT 32 | 33 | 00:00:00.000 --> 00:00:05.000 34 | http://example.com/video-name-sprite.jpg#xywh=0,0,150,100 35 | 36 | 00:00:05.000 --> 00:00:10.000 37 | http://example.com/video-name-sprite.jpg#xywh=150,0,150,100 38 | 39 | 00:00:10.000 --> 00:00:15.000 40 | http://example.com/video-name-sprite.jpg#xywh=300,0,150,100 41 | 42 | 00:00:15.000 --> 00:00:20.000 43 | http://example.com/video-name-sprite.jpg#xywh=450,0,150,100 44 | 45 | 00:00:20.000 --> 00:00:25.000 46 | http://example.com/video-name-sprite.jpg#xywh=600,0,150,100 47 | 48 | 00:00:25.000 --> 00:00:30.000 49 | http://example.com/video-name-sprite.jpg#xywh=0,100,150,100 50 | ``` 51 | 52 | #### Current Sprite Limitations 53 | 54 | All the images embedded in the sprite should have exactly the same dimensions. The styling for the thumbnail hover area is calculated based on the dimensions of the first URL in the WebVTT file. 55 | 56 | The durations that each thumbnail ought to show should be consistent. The default is 5 seconds, though this is configurable when initializing the player. 57 | 58 | ### Markup 59 | 60 | Add a new track element to your video element like the following: 61 | 62 | ```html 63 | 64 | ``` 65 | 66 | The kind attribute must be "metadata" and the class must be "time-rail-thumbnails". The WebVTT file should also be accessible via an AJAX request, so either have it on the same domain or allow cross-origin requests. 67 | 68 | ### Initialization 69 | 70 | Add 'timerailsthumbnails' as the last feature when initializing the mediaelement player: 71 | 72 | ```javascript 73 | $('video').mediaelementplayer({ 74 | features: ['playpause','progress','current','duration','tracks','volume', 'timerailthumbnails'], 75 | timeRailThumbnailsSeconds: 5 76 | }); 77 | ``` 78 | 79 | Also, either use the default of 5 second intervals for each thumbnail or set `timeRailThumbnailsSeconds` to the appropriate value. 80 | 81 | ## Installation 82 | 83 | This JavaScript plugin is made available as both a Rails Engine gem for the asset pipeline (unreleased) and a bower package. Choose your poison. 84 | 85 | ### vtt.js 86 | 87 | This plugin relies on the [vtt.js](https://github.com/mozilla/vtt.js/tree/master) library for WebVTT parsing. You'll need to include this before including mep-feature-time-rail-thumbnails.js. 88 | 89 | ### Rails 90 | 91 | Include it in your Gemfile: 92 | ```ruby 93 | gem 'mep_feature_time_rail_thumbnails' 94 | ``` 95 | 96 | Add it to your application.js: 97 | ```javascript 98 | //= require mep-feature-time-rail-thumbnails 99 | ``` 100 | 101 | ### Bower 102 | 103 | Install with bower: 104 | ``` 105 | bower i mep_feature_time_rail_thumbnails 106 | ``` 107 | 108 | This will install [vtt.js](https://github.com/mozilla/vtt.js) as well. 109 | 110 | Include them both in HTML: 111 | ```html 112 | 113 | 114 | ``` 115 | 116 | ## Browser Support 117 | 118 | The plugin currently uses [MutationObserver](http://caniuse.com/mutationobserver) and is disabled for browsers without support. You may be able to use a polyfill, but I have not tried that yet. 119 | 120 | ## TODO 121 | 122 | - Make the interval of thumbnails really use the timestamps in the WebVTT file rather than relying on configuration and regular intervals. 123 | 124 | ## Author 125 | 126 | Jason Ronallo 127 | 128 | ## License 129 | 130 | This project rocks and uses MIT-LICENSE. 131 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mep_feature_time_rail_thumbnails", 3 | "version": "0.0.3", 4 | "homepage": "https://github.com/jronallo/mep-feature-time-rail-thumbnails", 5 | "authors": [ 6 | "Jason Ronallo " 7 | ], 8 | "description": "MediaElement.js Plugin for Preview Thumbnails", 9 | "main": "vendor/assets/javascripts/mep-feature-time-rail-thumbnails.js", 10 | "keywords": [ 11 | "html5 video", 12 | "mediaelement.js" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests", 21 | "lib", 22 | "Gemfile", 23 | "Gemfile.lock", 24 | "mep_feature_time_rail_thumbnails.gemspec" 25 | ], 26 | "dependencies": { 27 | "vtt.js": "0.11.7" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/mep_feature_time_rail_thumbnails.rb: -------------------------------------------------------------------------------- 1 | require "mep_feature_time_rail_thumbnails/engine" 2 | 3 | module MepFeatureTimeRailThumbnails 4 | end 5 | -------------------------------------------------------------------------------- /lib/mep_feature_time_rail_thumbnails/engine.rb: -------------------------------------------------------------------------------- 1 | module MepFeatureTimeRailThumbnails 2 | class Engine < ::Rails::Engine 3 | isolate_namespace MepFeatureTimeRailThumbnails 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/mep_feature_time_rail_thumbnails/version.rb: -------------------------------------------------------------------------------- 1 | module MepFeatureTimeRailThumbnails 2 | VERSION = "0.0.3" 3 | end 4 | -------------------------------------------------------------------------------- /mep_feature_time_rail_thumbnails.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "mep_feature_time_rail_thumbnails/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "mep_feature_time_rail_thumbnails" 9 | s.version = MepFeatureTimeRailThumbnails::VERSION 10 | s.authors = ["Jason Ronallo"] 11 | s.email = ["jronallo@gmail.com"] 12 | s.homepage = "https://github.com/jronallo/mep-feature-time-rail-thumbnails" 13 | s.summary = "MediaElement.js Plugin for Preview Thumbnails" 14 | s.description = "Add a thumbnail preview to the MediaElement.js player." 15 | 16 | s.files = Dir["{lib,vendor}/**/*", "MIT-LICENSE", "README.md"] 17 | 18 | s.add_dependency "rails", ">= 3.2.17" 19 | end 20 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/mep-feature-time-rail-thumbnails.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | $.extend(MediaElementPlayer.prototype, { 4 | buildtimerailthumbnails : function(player, controls, layers, media) { 5 | if (!player.isVideo) 6 | return; 7 | 8 | // This relies on mutation observers right now, so if those aren't available just 9 | // abandon it. 10 | if (!window.MutationObserver) 11 | return; 12 | 13 | // Check for presence of WebVTT 14 | if (!WebVTT) { 15 | console.log('mep-feature-time-rail-thumbnails.js requires vtt.js'); 16 | return; 17 | } 18 | 19 | function getVttCues(url) { 20 | var vtt, 21 | parser = new WebVTT.Parser(window, WebVTT.StringDecoder()), 22 | cues = []; 23 | 24 | // FIXME: Is there a way to do this with promises? 25 | $.ajax({ 26 | url: url, 27 | async: false, 28 | success: function(data){ 29 | vtt = data; 30 | }, 31 | error:function (xhr, ajaxOptions, thrownError){ 32 | if(xhr.status==404) { 33 | vtt = null; 34 | } 35 | } 36 | }); 37 | if (vtt) { 38 | parser.oncue = function(cue) { 39 | cues.push(cue); 40 | }; 41 | parser.parse(vtt); 42 | parser.flush(); 43 | } 44 | return cues; 45 | } 46 | 47 | function parseMediaFragmentHash(url) { 48 | var hash = url.substring(url.indexOf('#')+1); 49 | return hash.split('=')[1].split(','); 50 | } 51 | 52 | function setThumbnailImage(url) { 53 | // Make sure the url is protocol/scheme relative 54 | var protocol_relative_url = url.substr(url.indexOf('://')+1); 55 | $('.mejs-plugin-time-float-thumbnail').css('background-image','url(' + protocol_relative_url.split('#')[0] + ')'); 56 | } 57 | 58 | var 59 | mediaContainer = player.container.find('.mejs-mediaelement').parent(), 60 | element_to_observe = player.container.find('.mejs-time-float-current')[0], 61 | video_thumbnail_vtt_url, 62 | cues, 63 | preview_thumbnails_track = mediaContainer.find("track[kind='metadata'].time-rail-thumbnails"), 64 | time_rail_thumbnails_seconds; 65 | 66 | if (player.options.timeRailThumbnailsSeconds) { 67 | time_rail_thumbnails_seconds = player.options.timeRailThumbnailsSeconds; 68 | } else { 69 | time_rail_thumbnails_seconds = 5; 70 | } 71 | 72 | if (preview_thumbnails_track.length > 0) { 73 | video_thumbnail_vtt_url = preview_thumbnails_track.attr('src'); 74 | } else { 75 | return; 76 | } 77 | cues = getVttCues(video_thumbnail_vtt_url); 78 | 79 | // If there is only one cue then there's no need to show thumbnails at all so don't do anything. 80 | if (cues.length > 1) { 81 | // Set up the container to hold the thumbnail. 82 | var time_float = mediaContainer.find('.mejs-time-float'); 83 | time_float.prepend(''); 84 | 85 | // Set necessary styles. 86 | var xywh = parseMediaFragmentHash(cues[0].text); 87 | var x = xywh[0]; 88 | var y = xywh[1]; 89 | var w = xywh[2]; 90 | var h = xywh[3]; 91 | var new_height = parseInt(h) + time_float.height(); 92 | time_float.css('top', '-' + new_height + 'px'); 93 | time_float.find('.mejs-time-float-corner').css('top', new_height - 3 + 'px'); 94 | time_float.css('height', 'auto'); 95 | time_float.css('width', w + 'px'); 96 | time_float.find('.mejs-time-float-current').css('position', 'static'); 97 | time_float.find('.mejs-plugin-time-float-thumbnail').css('position', 'static'); 98 | 99 | time_float.css('-webkit-border-radius', '0').css('border-radius', '0'); 100 | time_float.find('span').css('-webkit-border-radius', '0').css('border-radius', '0'); 101 | 102 | setThumbnailImage(cues[0].text); 103 | 104 | // Add an observer to the .mejs-time-float-current and change the thumbnail 105 | // when the observer is triggered 106 | var observer = new MutationObserver(function(mutations){ 107 | var time_code_current = $('.mejs-time-float-current').text(); 108 | 109 | var sections = time_code_current.split(':'); 110 | if (sections.length < 3) { 111 | time_code_current = "00:" + time_code_current; 112 | } 113 | 114 | // If the mouse is hovering over the 0 seconds mark, then show the first frame. 115 | // Otherwise show something deeper into the video. 116 | var seconds = mejs.Utility.timeCodeToSeconds(time_code_current), 117 | cue; 118 | if (seconds == 0) { 119 | cue = cues[0]; 120 | } else { 121 | var tile = Math.floor(seconds / time_rail_thumbnails_seconds); 122 | cue = cues[tile + 1]; 123 | } 124 | 125 | // The text of the cue will be the background image of the thumbnail container. 126 | setThumbnailImage(cue.text); 127 | 128 | // Use the spatial media fragment hash of the url to determine the coordinates and size 129 | // of the image to be displayed. 130 | var xywh = parseMediaFragmentHash(cue.text); 131 | var x = xywh[0]; 132 | var y = xywh[1]; 133 | var w = xywh[2]; 134 | var h = xywh[3]; 135 | 136 | // Set the background position and height and width. 137 | $('.mejs-plugin-time-float-thumbnail').css('background-position', '-' + x + 'px -' + y + 'px' ); 138 | $('.mejs-plugin-time-float-thumbnail').css('height', h); 139 | $('.mejs-plugin-time-float-thumbnail').css('width', w); 140 | 141 | }); 142 | 143 | observer.observe(element_to_observe, {attributes: true, childList: true, characterData: true, subtree:true}); 144 | } 145 | } 146 | }); 147 | })(mejs.$); --------------------------------------------------------------------------------