Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add an option to render a thumbnail when hovering the progress bar #8542

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
113 changes: 113 additions & 0 deletions sandbox/thumbnail-preview.html.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Video.js Sandbox</title>
<link href="../dist/video-js.css" rel="stylesheet" type="text/css" />
<script src="../dist/video.js"></script>
</head>
<body>
<div>
<h2>Thumbnail Preview enabled on progress bar</h2>
<video-js
id="vid1"
controls
preload="auto"
width="640"
height="264"
poster="https://vjs.zencdn.net/v/oceans.png"
>
<source src="https://vjs.zencdn.net/v/oceans.mp4" type="video/mp4" />
<source src="https://vjs.zencdn.net/v/oceans.webm" type="video/webm" />
<source src="https://vjs.zencdn.net/v/oceans.ogv" type="video/ogg" />
<track
kind="captions"
src="../docs/examples/shared/example-captions.vtt"
srclang="en"
label="English"
/>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to
a web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank"
>supports HTML5 video</a
>
</p>
</video-js>
</div>

<div>
<h2>Thumbnail Preview disabled on progress bar</h2>
<video-js
id="vid2"
controls
preload="auto"
width="640"
height="264"
poster="https://vjs.zencdn.net/v/oceans.png"
>
<source src="https://vjs.zencdn.net/v/oceans.mp4" type="video/mp4" />
<source src="https://vjs.zencdn.net/v/oceans.webm" type="video/webm" />
<source src="https://vjs.zencdn.net/v/oceans.ogv" type="video/ogg" />
<track
kind="captions"
src="../docs/examples/shared/example-captions.vtt"
srclang="en"
label="English"
/>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to
a web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank"
>supports HTML5 video</a
>
</p>
</video-js>
</div>

<div>
<h2>Thumbnail Preview enabled on progress bar</h2>
<video-js
id="vid3"
controls
preload="auto"
width="640"
height="264"
poster="https://vjs.zencdn.net/v/oceans.png"
>
<source src="https://vjs.zencdn.net/v/oceans.mp4" type="video/mp4" />
<source src="https://vjs.zencdn.net/v/oceans.webm" type="video/webm" />
<source src="https://vjs.zencdn.net/v/oceans.ogv" type="video/ogg" />
<track
kind="captions"
src="../docs/examples/shared/example-captions.vtt"
srclang="en"
label="English"
/>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to
a web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank"
>supports HTML5 video</a
>
</p>
</video-js>
</div>

<script>
var vid1 = document.getElementById("vid1");
var options = {
controlBar: { thumbnailPreview: true },
};
videojs(vid1, options);

var vid2 = document.getElementById("vid2");
options.controlBar.thumbnailPreview = false;
videojs(vid2, options);

var vid3 = document.getElementById("vid3");
options.controlBar.thumbnailPreview = false;
videojs(vid3, options);
</script>
</body>
</html>
11 changes: 10 additions & 1 deletion src/js/control-bar/progress-control/mouse-time-display.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Component from '../../component.js';
import * as Fn from '../../utils/fn.js';

import './time-tooltip';
import './thumbnail-tooltip';

/**
* The {@link MouseTimeDisplay} component tracks mouse movement over the
Expand Down Expand Up @@ -45,6 +46,7 @@ class MouseTimeDisplay extends Component {
/**
* Enqueues updates to its own DOM as well as the DOM of its
* {@link TimeTooltip} child.
* {@link ThumbnailTooltip} child.
*
* @param {Object} seekBarRect
* The `ClientRect` for the {@link SeekBar} element.
Expand All @@ -59,6 +61,12 @@ class MouseTimeDisplay extends Component {
this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => {
this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
});

if (this.player_.options_.controlBar.thumbnailPreview) {
this.getChild('thumbnailTooltip').updateThumbnail(seekBarRect, seekBarPoint, time, () => {
this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
});
}
}
}

Expand All @@ -70,7 +78,8 @@ class MouseTimeDisplay extends Component {
*/
MouseTimeDisplay.prototype.options_ = {
children: [
'timeTooltip'
'timeTooltip',
'thumbnailTooltip'
]
};

Expand Down
174 changes: 174 additions & 0 deletions src/js/control-bar/progress-control/thumbnail-tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* @file thumbnail-tooltip.js
*/
import Component from '../../component';
import * as Dom from '../../utils/dom.js';
import * as Fn from '../../utils/fn.js';

/**
* thumbnail tooltips display a thumbnail under the progress bar.
*
* @extends Component
*/
class ThumbnailTooltip extends Component {

/**
* Creates an instance of this class.
*
* @param { import('../../player').default } player
* The {@link Player} that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of player options.
*/
constructor(player, options) {
super(player, options);
this.update = Fn.throttle(Fn.bind_(this, this.update), Fn.UPDATE_REFRESH_INTERVAL);
}

/**
* Create the thumbnail tooltip DOM element
*
* @return {Element}
* The element that was created.
*/
createEl() {
return super.createEl('div', {
className: 'vjs-thumbnail-tooltip',
id: 'thumbnailtooltip'
}, {
'aria-hidden': 'true'
});
}

/**
* Updates the position of the thumbnail tooltip relative to the `SeekBar`.
*
* @param {Object} seekBarRect
* The `ClientRect` for the {@link SeekBar} element.
*
* @param {number} seekBarPoint
* A number from 0 to 1, representing a horizontal reference point
* from the left edge of the {@link SeekBar}
*
* @param {number} time
* The time to update the tooltip to, not used during live playback
*
*/
update(seekBarRect, seekBarPoint, time) {
const tooltipRect = Dom.findPosition(this.el_);
const playerRect = Dom.getBoundingClientRect(this.player_.el());
const seekBarPointPx = seekBarRect.width * seekBarPoint;

// do nothing if either rect isn't available
// for example, if the player isn't in the DOM for testing
if (!playerRect || !tooltipRect) {
return;
}

// This is the space left of the `seekBarPoint` available within the bounds
// of the player. We calculate any gap between the left edge of the player
// and the left edge of the `SeekBar` and add the number of pixels in the
// `SeekBar` before hitting the `seekBarPoint`
const spaceLeftOfPoint = (seekBarRect.left - playerRect.left) + seekBarPointPx;

// This is the space right of the `seekBarPoint` available within the bounds
// of the player. We calculate the number of pixels from the `seekBarPoint`
// to the right edge of the `SeekBar` and add to that any gap between the
// right edge of the `SeekBar` and the player.
const spaceRightOfPoint = (seekBarRect.width - seekBarPointPx) +
(playerRect.right - seekBarRect.right);

// This is the number of pixels by which the tooltip will need to be pulled
// further to the right to center it over the `seekBarPoint`.
let pullTooltipBy = tooltipRect.width / 2;

// Adjust the `pullTooltipBy` distance to the left or right depending on
// the results of the space calculations above.
if (spaceLeftOfPoint < pullTooltipBy) {
pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
} else if (spaceRightOfPoint < pullTooltipBy) {
pullTooltipBy = spaceRightOfPoint;
}

// Due to the imprecision of decimal/ratio based calculations and varying
// rounding behaviors, there are cases where the spacing adjustment is off
// by a pixel or two. This adds insurance to these calculations.
if (pullTooltipBy < 0) {
pullTooltipBy = 0;
} else if (pullTooltipBy > tooltipRect.width) {
pullTooltipBy = tooltipRect.width;
}

// prevent small width fluctuations within 0.4px from
// changing the value below.
// This really helps for live to prevent the play
// progress thumbnail tooltip from jittering
pullTooltipBy = Math.round(pullTooltipBy);

this.el_.style.right = `-${pullTooltipBy}px`;
this.draw(time);
}

/**
* Draw the thumbnail at specific time to the tooltip DOM element.
*
* @param {string} content
* The formatted time we want the thumbnail for the tooltip.
*/
draw(content) {
const videoElement = this.player().children()[0];
// clone the original video element to navigate in the video at the
// time we want without altering the original element

const clonedVideo = videoElement.cloneNode(true);

clonedVideo.currentTime = parseInt(content, 10);

const canvas = Dom.createEl('canvas');

const context = canvas.getContext('2d');

// wait for the metadata to load before drawing the thumbnail canvas
clonedVideo.addEventListener('loadedmetadata', function() {
clonedVideo.addEventListener('seeked', function() {
context.drawImage(clonedVideo, 0, 0, canvas.width, canvas.height);
});
clonedVideo.removeEventListener('loadedmetadata', null);
});
// delete the previous thumbnail loaded to render the new one
if (this.el_.firstChild) {
this.el_.removeChild(this.el_.firstChild);
}
this.el_.appendChild(canvas);
}

/**
* Updates the position of the thumbnail tooltip relative to the `SeekBar`.
*
* @param {Object} seekBarRect
* The `ClientRect` for the {@link SeekBar} element.
*
* @param {number} seekBarPoint
* A number from 0 to 1, representing a horizontal reference point
* from the left edge of the {@link SeekBar}
*
* @param {number} time
* The time to update the tooltip to, not used during live playback
*
* @param {Function} cb
* A function that will be called during the request animation frame
* for tooltips that need to do additional animations from the default
*/
updateThumbnail(seekBarRect, seekBarPoint, time, cb) {
this.requestNamedAnimationFrame('ThumbnailTooltip#updateThumbnail', () => {
this.update(seekBarRect, seekBarPoint, time);
if (cb) {
cb();
}
});
}
}

Component.registerComponent('ThumbnailTooltip', ThumbnailTooltip);
export default ThumbnailTooltip;