From 74e7cbbb40e74d836bc68f4e174f915116cde7f2 Mon Sep 17 00:00:00 2001 From: Florian Larysch Date: Tue, 30 Oct 2018 21:39:09 +0100 Subject: [PATCH 1/2] assets: add timelens plugin --- assets/clapprio/timelens.css | 113 +++++++++++++++ assets/clapprio/timelens.js | 268 +++++++++++++++++++++++++++++++++++ template/page.phtml | 1 + 3 files changed, 382 insertions(+) create mode 100644 assets/clapprio/timelens.css create mode 100644 assets/clapprio/timelens.js diff --git a/assets/clapprio/timelens.css b/assets/clapprio/timelens.css new file mode 100644 index 00000000..267cab6f --- /dev/null +++ b/assets/clapprio/timelens.css @@ -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; +} diff --git a/assets/clapprio/timelens.js b/assets/clapprio/timelens.js new file mode 100644 index 00000000..ed714c4e --- /dev/null +++ b/assets/clapprio/timelens.js @@ -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; +} diff --git a/template/page.phtml b/template/page.phtml index 9fb24adc..fc34c4f7 100644 --- a/template/page.phtml +++ b/template/page.phtml @@ -63,6 +63,7 @@ + isEnabled()): ?> From e6f2c91b900c3637c232c7284102da8c8300280b Mon Sep 17 00:00:00 2001 From: Florian Larysch Date: Tue, 30 Oct 2018 21:57:15 +0100 Subject: [PATCH 2/2] relive: add Timelens support --- assets/js/lustiges-script.js | 16 ++++++++++++++-- template/assemblies/player/relive.phtml | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/assets/js/lustiges-script.js b/assets/js/lustiges-script.js index 22c4e04a..c3cf8808 100644 --- a/assets/js/lustiges-script.js +++ b/assets/js/lustiges-script.js @@ -34,9 +34,17 @@ $(function() { var $relivePlayer = $('body.relive-player .video-wrap'); if($relivePlayer.length > 0) { + var plugins = [PlaybackRatePlugin]; var sprites = []; - if($relivePlayer.data("sprites")) { + if ($relivePlayer.data("timelens-timeline")) { + plugins.push(TimelensPlugin); + + // the timelens CSS always causes an extra bar to be drawn even if we're not using it. Hotfix: + $('head').append(''); + } else if($relivePlayer.data("sprites")) { + plugins.push(ClapprThumbnailsPlugin); + sprites = ClapprThumbnailsPlugin.buildSpriteConfig( $relivePlayer.data("sprites"), $relivePlayer.data("sprites-n"), @@ -49,7 +57,7 @@ $(function() { var player = new Clappr.Player({ baseUrl: 'assets/clapprio/', plugins: { - core: [ClapprThumbnailsPlugin, PlaybackRatePlugin] + core: plugins }, source: $relivePlayer.data('m3u8'), @@ -61,6 +69,10 @@ $(function() { spotlightHeight: 84, thumbs: sprites }, + timelens: { + timeline: $relivePlayer.data("timelens-timeline"), + thumbnails: $relivePlayer.data("timelens-thumbs") + }, events: { onReady: function() { var playback = player.core.getCurrentContainer().playback; diff --git a/template/assemblies/player/relive.phtml b/template/assemblies/player/relive.phtml index 361e1bbc..064bf53d 100644 --- a/template/assemblies/player/relive.phtml +++ b/template/assemblies/player/relive.phtml @@ -10,5 +10,9 @@ data-sprites-cols="" data-sprites-interval="" + + data-timelens-timeline="" + data-timelens-thumbs="" + >