Skip to content
This repository has been archived by the owner on Dec 17, 2018. It is now read-only.

Change plugin logic to work with VTT files #27

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
*.log
60 changes: 43 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ Video.js Thumbnails
===================
A plugin that allows you to configure thumbnails to display when the user is hovering over the progress bar or dragging it to seek.

[![Build Status](https://travis-ci.org/brightcove/videojs-thumbnails.svg?branch=master)](https://travis-ci.org/brightcove/videojs-thummbnails)


Using the Plugin
----------------
The plugin automatically registers itself when you include video.thumbnails.js in your page:
Expand All @@ -19,20 +16,49 @@ You probably want to include the default stylesheet, too. It handles showing and
<link href="videojs.thumbnails.css" rel="stylesheet">
```

Once you have your video created, you can activate the thumbnails plugin. In the first argument to the plugin, you should pass an object whose properties are the time in seconds you wish to display your thumbnails. At minimum, you'll need a prerty `0` with a `src`: the thumbnail to display if the user were to hover over the beginning of the progress bar. If you add additional times, they'll partition the progress bar and change the image that is displayed when the user hovers over that area. If you wanted to display one thumbnail for the first five seconds of a video and then another for the rest of the time, you could do it like this:

```js
video.thumbnails({
0: {
src: 'http://example.com/thumbnail1.png',
width: '120px'
},
5: {
src: 'http://example.com/thumbnail2.png'
}
});
To activate the plugin, add it to your videojs settings object:

```html
<script>
// initialize video.js
var video = videojs('video',{plugins:{thumbnails:{}}});
</script>
```

For each thumbnail time period, you can specify any other style changes you'd like to change when the user enters that region of the progress bar. Check out example.html to see how that technique can be used to create multiple thumbnails out of a single, sprited image.
The thumbnails need to be added with a VTT file. For this file, the [specification used by JW Player](http://support.jwplayer.com/customer/portal/articles/1407439-adding-preview-thumbnails) applies.
The VTT file is added as a metadata track to the video object, for example:

The `width` property on each time period lets us know what the visible portion of the thumbnail should be. This is so that thumbnails won't reach beyond the player and perhaps get cut off. It can be specified on each time period or on the `0` time period.
```html
<track kind="metadata" src="oceans.vtt"></track>
```

Full object example:

```html
<video id='video'
class='video-js vjs-default-skin'
width='640'
height='264'
poster='http://video-js.zencoder.com/oceans-clip.jpg'
controls>
<source src='http://video-js.zencoder.com/oceans-clip.mp4' type='video/mp4' />
<track kind="metadata" src="oceans.vtt"></track>
</video>
```

If your thumbnails do not include specified width and height in the VTT file (via the Media Fragment hash), you have to specify the default width and height in pixels the plugin settings:

```html
<script>
// initialize video.js
var video = videojs('video',{plugins:{thumbnails:{width:120,height:90}}});
</script>
```

You can add an optional basePath if you want to use images hosted on another domain
```html
<script>
// initialize video.js
var video = videojs('video',{plugins:{thumbnails:{width:120,height:90, basePath : "//external.url/basepath/"}}});
</script>
```
Binary file removed example-thumbnail.png
Binary file not shown.
49 changes: 2 additions & 47 deletions example.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,56 +26,11 @@
poster='http://video-js.zencoder.com/oceans-clip.jpg'
controls>
<source src='http://video-js.zencoder.com/oceans-clip.mp4' type='video/mp4' />
<track kind="metadata" src="oceans.vtt"></track>
</video>
<script>
// initialize video.js
var video = videojs('video');

// here's an example of generating thumbnails from a sprited image:
video.thumbnails({
0: {
src: 'thumbnails.png',
style: {
left: '-60px',
width: '600px',
height: '68px',
clip: 'rect(0, 120px, 68px, 0)'
}
},
10: {
style: {
left: '-180px',
clip: 'rect(0, 240px, 68px, 120px)'
}
},
20: {
style: {
left: '-300px',
clip: 'rect(0, 360px, 68px, 240px)'
}
},
30: {
style: {
left: '-420px',
clip: 'rect(0, 480px, 68px, 360px)'
}
},
40: {
style: {
left: '-540px',
clip: 'rect(0, 600px, 68px, 480px)'
}
}
});

// and here's an example of the bare-minimum plugin configuration:
/*
video.thumbnails({
0: {
src: 'example-thumbnail.png'
}
});
*/
var video = videojs('video',{plugins:{thumbnails:{}}});
</script>
</body>
</html>
Binary file added oceans.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions oceans.vtt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
WEBVTT

00:00:00.000 --> 00:00:05.000
oceans.jpg#xywh=0,0,120,68

00:00:05.000 --> 00:00:10.000
oceans.jpg#xywh=120,0,120,68

00:00:10.000 --> 00:00:15.000
oceans.jpg#xywh=240,0,120,68

00:00:15.000 --> 00:00:20.000
oceans.jpg#xywh=360,0,120,68

00:00:20.000 --> 00:00:25.000
oceans.jpg#xywh=480,0,120,68

00:00:25.000 --> 00:00:30.000
oceans.jpg#xywh=600,0,120,68

00:00:30.000 --> 00:00:35.000
oceans.jpg#xywh=720,0,120,68

00:00:35.000 --> 00:00:40.000
oceans.jpg#xywh=840,0,120,68

00:00:40.000 --> 00:00:45.000
oceans.jpg#xywh=960,0,120,68

00:00:45.000 --> 00:00:50.000
oceans.jpg#xywh=1080,0,120,68

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "videojs-thumbnails",
"description": "progress bar thumbnails plugin for video.js",
"version": "0.1.1",
"version": "1.0.0",
"peerDependencies": {
"video.js": "^4.0"
},
"devDependencies": {
"grunt": "^0.4.4",
"grunt-contrib-connect": "^0.9.0",
"grunt-contrib-jshint": "^0.10.0",
"video.js": "4.10.1"
"video.js": "4.12.4"
}
}
Binary file removed thumbnails.png
Binary file not shown.
3 changes: 2 additions & 1 deletion videojs.thumbnails.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/* a wrapper element that tracks the mouse vertically */
.vjs-thumbnail-holder {
position: absolute;
overflow: hidden;
left: -1000px;
bottom: 1.3em;
}

/* the thumbnail image itself */
.vjs-thumbnail {
position: absolute;
left: 0;
bottom: 1.3em;
opacity: 0;
transition: opacity .2s ease;
-webkit-transition: opacity .2s ease;
Expand Down
109 changes: 70 additions & 39 deletions videojs.thumbnails.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
(function() {
var defaults = {
0: {
src: 'example-thumbnail.png'
}
width:0, height:0, basePath : ""
},
extend = function() {
var args, target, i, object, property;
Expand Down Expand Up @@ -37,22 +35,6 @@
}
return el;
},
getVisibleWidth = function(el, width) {
var clip;

if (width) {
return parseFloat(width);
}

clip = getComputedStyle(el)('clip');
if (clip !== 'auto' && clip !== 'inherit') {
clip = clip.split(/(?:\(|\))/)[1].split(/(?:,| )/);
if (clip.length === 4) {
return (parseFloat(clip[1]) - parseFloat(clip[3]));
}
}
return 0;
},
getScrollOffset = function() {
if (window.pageXOffset) {
return {
Expand All @@ -64,16 +46,45 @@
x: document.documentElement.scrollLeft,
y: document.documentElement.scrollTop
};
},
parseImageLink = function(imglocation) {
var lsrc, clip, hashindex, hashstring;
hashindex = imglocation.indexOf('#');
if (hashindex === -1) {
return {src:imglocation,w:0,h:0,x:0,y:0};
}
lsrc = imglocation.substring(0,hashindex);
hashstring = imglocation.substring(hashindex+1);
if (hashstring.substring(0,5) !== 'xywh=') {
return {src:defaults.basePath + lsrc,w:0,h:0,x:0,y:0};
}
var data = hashstring.substring(5).split(',');
return {src:defaults.basePath + lsrc,w:parseInt(data[2]),h:parseInt(data[3]),x:parseInt(data[0]),y:parseInt(data[1])};
};

/**
* register the thubmnails plugin
*/
videojs.plugin('thumbnails', function(options) {
var div, settings, img, player, progressControl, duration, moveListener, moveCancel;
var div, settings, img, player, progressControl, duration, moveListener, moveCancel, thumbTrack;
defaults.basePath = options.basePath || defaults.basePath;
settings = extend({}, defaults, options);
player = this;

//detect which track we use. For now we just use the first metadata track
var numtracks = player.textTracks().length;
if (numtracks === 0) {
return;
}
i = 0;
while (i<numtracks) {
if (player.textTracks()[i].kind==='metadata') {
thumbTrack = player.textTracks()[i];
//Chrome needs this
thumbTrack.mode = 'hidden';
break;
}
i++;
}
(function() {
var progressControl, addFakeActive, removeFakeActive;
// Android doesn't support :active and :hover on non-anchor and non-button elements
Expand All @@ -99,16 +110,7 @@
div.className = 'vjs-thumbnail-holder';
img = document.createElement('img');
div.appendChild(img);
img.src = settings['0'].src;
img.className = 'vjs-thumbnail';
extend(img.style, settings['0'].style);

// center the thumbnail over the cursor if an offset wasn't provided
if (!img.style.left && !img.style.right) {
img.onload = function() {
img.style.left = -(img.naturalWidth / 2) + 'px';
};
}

// keep track of the duration to calculate correct thumbnail to display
duration = player.duration();
Expand Down Expand Up @@ -150,27 +152,56 @@
// to remove the progress control's left offset to know the mouse position
// relative to the progress control
mouseTime = Math.floor((left - progressControl.el().offsetLeft) / progressControl.width() * duration);
for (time in settings) {
if (mouseTime > time) {
active = Math.max(active, time);

//Now check which of the cues applies
var cnum = thumbTrack&&thumbTrack.cues.length;
i = 0;
while (i<cnum) {
var ccue = thumbTrack.cues[i];
if (ccue.startTime <= mouseTime && ccue.endTime >= mouseTime) {
setting = parseImageLink(ccue.text);
break;
}
i++;
}
setting = settings[active];
//None found, so show nothing
if (typeof setting === 'undefined') {
return;
}

//Changed image?
if (setting.src && img.src != setting.src) {
img.src = setting.src;
}
if (setting.style && img.style != setting.style) {
extend(img.style, setting.style);

//Fall back to plugin defaults in case no height/width is specified
if (setting.w === 0) {
setting.w = settings.width;
}
if (setting.h === 0) {
setting.h = settings.height;
}

width = getVisibleWidth(img, setting.width || settings[0].width);
//Set the container width/height if it changed
if (div.style.width != setting.w || div.style.height != setting.h) {
div.style.width = setting.w + 'px';
div.style.height = setting.h + 'px';
}
//Set the image cropping
img.style.left = -(setting.x) + 'px';
img.style.top = -(setting.y) + 'px';
img.style.clip = 'rect('+setting.y+'px,'+(setting.w+setting.x)+'px,'+(setting.y+setting.h)+'px,'+setting.x+'px)';

width = setting.w;
halfWidth = width / 2;

// make sure that the thumbnail doesn't fall off the right side of the left side of the player
if ( (left + halfWidth) > right ) {
left -= (left + halfWidth) - right;
left = right - width;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of the edge cases are much cleaner. Nice!

} else if (left < halfWidth) {
left = halfWidth;
left = 0;
} else {
left = left - halfWidth;
}

div.style.left = left + 'px';
Expand Down