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

RFC: Relive Timelens support #83

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions assets/clapprio/timelens.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* Common styles */

.timelens {
position: relative;
}

.timelens img {
width: 100%;
height: 100%;
display: block;

-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;

position: relative;
}

.timelens-thumbnail {
background: black;
border: solid rgba(28, 28, 28, 0.9) 3px !important;
border-radius: 3px;
background-clip: content-box;
width: 0px;
height: 0px;
position: absolute;
top: 0;
z-index: -1;
opacity: 0;
transition: opacity 0.2s, z-index 0.2s;
transform: translateY(-100%) translateY(-7px);
}

.timelens-time {
font-size: 15px !important;
font-family: Arial, sans-serif !important;
color: #eee;
background: rgba(28, 28, 28, 0.9);
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
display: inline-block !important;
padding: 0 10px !important;
border-radius: 2px 2px 0 0;
height: 22px;
vertical-align: middle !important;
line-height: 25px !important;
}

.timelens img:hover + .timelens-thumbnail {
z-index: 1;
opacity: 1;
}

.timelens-marker {
width: 0px;
height: 0px;
border-top: 15px solid #eee !important;
border-left: 10px solid transparent !important;
border-right: 10px solid transparent !important;
position: absolute;
top: -20px;
z-index: 999;
transform: translateX(-50%);
}

.timelens-marker-border {
width: 0px;
height: 0px;
border-top: 23px solid rgba(28, 28, 28, 0.9) !important;
border-left: 15px solid transparent !important;
border-right: 15px solid transparent !important;
position: absolute;
top: -10px;
z-index: 999;
transform: translateX(-50%);
}

/* MediaElement.js */

.mejs__time-slider {
height: 40px;
margin-top: -10px;
}

.mejs__time-buffering,
.mejs__time-loaded,
.mejs__time-current,
.mejs__time-hovered,
.mejs__time-handle,
.mejs__time-float {
display: none !important;
}

/* Clappr */

.media-control .bar-background {
height: 40px !important;
margin-top: -40px;
}

.media-control .seek-time,
.media-control .bar-scrubber,
.media-control .bar-hover {
display: none !important;
}

.media-control-hide {
bottom: -40px !important;
}
268 changes: 268 additions & 0 deletions assets/clapprio/timelens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/* Common functionality */

function timelens(container, options) {
// Load VTT file asynchronously, then continue with the initialization.
let vtt_url;
if (options.thumbnails) {
vtt_url = options.thumbnails;
}

const request = new XMLHttpRequest();
request.open("GET", vtt_url, true);
request.send(null);
request.onreadystatechange = function() {
if (request.readyState === 4 && request.status === 200) {
const type = request.getResponseHeader("Content-Type");
if (type.indexOf("text") !== 1) {
timelens2(container, request.responseText, options);
}
}
};
}

// Actually initialize Timelens.
function timelens2(container, vtt, options) {
const thumbnails = parseVTT(vtt);
const duration = thumbnails[thumbnails.length - 1].to;

// Use querySelector if a selector string is specified.
if (typeof container == "string")
container = document.querySelector(container);

// This will be our main .timelens div, which will contain all new elements.
if (container.className != "") {
container.className += " ";
}
container.className += "timelens";

// Create div which contains the preview thumbnails.
const thumbnail = document.createElement("div");
thumbnail.className = "timelens-thumbnail";

// Create div which contains the thumbnail time.
const time = document.createElement("div");
time.className = "timelens-time";

// Create .timeline img, which displays the visual timeline.
const timeline = document.createElement("img");
timeline.src = options.timeline;
// Prevent the timeline image to be dragged
timeline.setAttribute("draggable", "false");

// Create .marker div, which is used to display the current position.
if (options.position) {
var marker = document.createElement("div");
marker.className = "timelens-marker-border";
container.appendChild(marker);

var markerInner = document.createElement("div");
markerInner.className = "timelens-marker";
marker.appendChild(markerInner);
}

// Assemble everything together.
container.appendChild(timeline);
container.appendChild(thumbnail);
thumbnail.appendChild(time);

// When clicking the timeline, seek to the respective position.
if (options.seek) {
timeline.onclick = function(event) {
const progress = progressAtMouse(event, timeline);
options.seek(progress * duration);
};
}

timeline.onmousemove = function(event) {
// Calculate click position in seconds.
const progress = progressAtMouse(event, timeline);
const seconds = progress * duration;
const x = progress * timeline.offsetWidth;

const thumbnail_dir = options.thumbnails.substring(
0,
options.thumbnails.lastIndexOf("/") + 1
);

// Find the first entry in `thumbnails` which contains the current position.
let active_thumbnail = null;
for (let t of thumbnails) {
if (seconds >= t.from && seconds <= t.to) {
active_thumbnail = t;
break;
}
}

// Set respective background image.
thumbnail.style["background-image"] =
"url(" + thumbnail_dir + active_thumbnail.file + ")";
// Move background to the correct location.
thumbnail.style["background-position"] =
-active_thumbnail.x + "px " + -active_thumbnail.y + "px";

// Set thumbnail div to correct size.
thumbnail.style.width = active_thumbnail.w + "px";
thumbnail.style.height = active_thumbnail.h + "px";

// Move thumbnail div to the correct position.
thumbnail.style.marginLeft =
Math.min(
Math.max(0, x - thumbnail.offsetWidth / 2),
timeline.offsetWidth - thumbnail.offsetWidth
) + "px";

time.innerHTML = to_timestamp(seconds);
};

if (options.position) {
setInterval(function() {
marker.style.marginLeft =
(options.position() / duration) * timeline.offsetWidth + "px";
}, 1);
}
}

// Convert a WebVTT timestamp (which has the format [HH:]MM:SS.mmm) to seconds.
function from_timestamp(timestamp) {
const matches = timestamp.match(/(.*):(.*)\.(.*)/);

const minutes = parseInt(matches[1]);
const seconds = parseInt(matches[2]);
const mseconds = parseInt(matches[3]);

const seconds_total = mseconds / 1000 + seconds + 60 * minutes;

return seconds_total;
}

// Convert a position in seconds to a [H:]MM:SS timestamp.
function to_timestamp(seconds_total) {
const hours = Math.floor(seconds_total / 60 / 60);
const minutes = Math.floor(seconds_total / 60 - hours * 60);
const seconds = Math.floor(seconds_total - 60 * minutes - hours * 60 * 60);

const timestamp = minutes + ":" + pad(seconds, 2);

if (hours > 0) {
return hours + ":" + pad(timestamp, 5);
} else {
return timestamp;
}
}

// How far is the mouse into the timeline, in a range from 0 to 1?
function progressAtMouse(event, timeline) {
const x = event.offsetX ? event.offsetX : event.pageX - timeline.offsetLeft;
return x / timeline.offsetWidth;
}

// Parse a VTT file pointing to JPEG files using media fragment notation.
function parseVTT(vtt) {
let from = 0;
let to = 0;

let thumbnails = [];

for (let line of vtt.split("\n")) {
if (/-->/.test(line)) {
// Parse a "cue timings" part.
const matches = line.match(/(.*) --> (.*)/);

from = from_timestamp(matches[1]);
to = from_timestamp(matches[2]);
} else if (/jpg/.test(line)) {
// Parse a "cue payload" part.
const matches = line.match(/(.*)\?xywh=(.*),(.*),(.*),(.*)/);

thumbnails.push({
from: from,
to: to,
file: matches[1],
x: matches[2],
y: matches[3],
w: matches[4],
h: matches[5]
});
}
}

return thumbnails;
}

function pad(num, size) {
return ("000000000" + num).substr(-size);
}

/* MediaElement.js */

if (typeof MediaElementPlayer !== "undefined") {
Object.assign(MediaElementPlayer.prototype, {
buildtimelens(player, controls, layers, media) {
const t = this;

// Get the timeline from the video's "timeline" attribute.
const vid = media.querySelector("video");
const timeline = vid.dataset.timeline;

// Get the thumbnails VTT from a "thumbnails" track.
const thumbnailsTrack = vid.querySelector(
'track[label="thumbnails"]'
);
const thumbnails = thumbnailsTrack.src;

const slider = controls.querySelector(
"." + t.options.classPrefix + "time-slider"
);

// Initialize the Timelens interface.
timelens(slider, {
timeline: timeline,
thumbnails: thumbnails,
position: function() {
return player.currentTime;
}
});
}
});
}

/* Clappr */

if (typeof Clappr !== "undefined") {
class TimelensPlugin extends Clappr.UICorePlugin {
get name() {
return "timelens";
}

constructor(core) {
super(core);
}

bindEvents() {
this.listenTo(
this.core.mediaControl,
Clappr.Events.MEDIACONTROL_RENDERED,
this._init
);
}

_init() {
const bar = this.core.mediaControl.el.querySelector(
".bar-background"
);

let t = this;

// Initialize the Timelens interface.
timelens(bar, {
timeline: this.core.options.timelens.timeline,
thumbnails: this.core.options.timelens.thumbnails,
position: function() {
return t.core.containers[0].getCurrentTime();
}
});
}
}

window.TimelensPlugin = TimelensPlugin;
}
Loading