Skip to content
Advertisement

Add a video control on top of a local html video

The following is a link which shows a nice example of playing a local video in a browser:

http://jsfiddle.net/dsbonev/cCCZ2/

<h1>HTML5 local video file player example</h1>
<div id="message"></div>
<input type="file" accept="video/*"/>
<video controls autoplay></video>

However, on top of this, I would like to allow the user to create a “trailer clip” of a particular segment of their video. In this, I would like to have some sort of adjustable playhead such as the following:

enter image description here

Here is a site that does this exact same thing: https://www.flexclip.com/editor/app?ratio=landscape. Any insight into how I would go about building something like this that plays the local file and allows me to select a segment of it?

Advertisement

Answer

There’s a few different things, though the particular question seems to be how to draw a preview bar of a local video.

Local Files

From the example in your question, you can see it’s not too different working with a local file than with a remote file; you can create an object URL of the local file via URL.createObjectURL and then work with it as you would a normal video link.

Drawing the Preview Bar

Drawing the preview bar should consist of seeking to the appropriate time in the video, and then drawing that frame onto a canvas. You can seek to a specific point by setting the .currentTime property of the video and waiting for the seeked event. You can then use the canvasContext.drawImage method to draw an image using the video element.

I’d also recommend using a video element that isn’t attached to the DOM for this, for a better user experience that doesn’t have an onscreen video jumping to different times.

It could look something like this:

function goToTime(video, time) {
  return new Promise ((resolve) => {
    function cb () {
      video.removeEventListener('seeked', cb);
      resolve();
    }
    video.addEventListener('seeked', cb);
    video.currentTime = time;
  });
}

async function drawPreviewBar(video) {
  const { duration, videoWidth, videoHeight } = video;
  const previewBarFrameWidth = previewBarHeight  * videoWidth / videoHeight;
  const previewBarFrames = previewBarWidth / previewBarFrameWidth;
  for (let i = 0; i < previewBarFrames; i++) {
    await goToTime(video, i * duration * previewBarFrameWidth / previewBarWidth);
    previewBarContext.drawImage(video, 0, 0,
      videoWidth, videoHeight,
      previewBarFrameWidth * i, 0,
      previewBarFrameWidth, previewBarHeight
    );
  }
}

Resizing / Moving the Selected Clip

If you want to create a UI to resize or move around the clip, you can use standard drag handlers.

Example

Here’s a basic example:

const previewBarWidth = 600;
const previewBarHeight = 40;
const videoElement = document.getElementById('video');
const inputElement = document.getElementById('file-picker');
const invisibleVideo = document.createElement('video');
const previewBarCanvas = document.getElementById('preview-bar-canvas');
previewBarCanvas.width = previewBarWidth;
previewBarCanvas.height = previewBarHeight;
const selector = document.getElementById('selector');
const previewBarContext = previewBarCanvas.getContext('2d');
const topHandle = document.getElementById('selector-top-handle');
const leftHandle = document.getElementById('selector-left-handle');
const rightHandle = document.getElementById('selector-right-handle');
const leftMask = document.getElementById('selector-left-mask');
const rightMask = document.getElementById('selector-right-mask');

var selectorLeft = 0, selectorWidth = previewBarWidth;
var minimumPreviewBarWidth = 80; // may want to dynamically change this based on video duration
var videoLoaded = false;

function goToTime(video, time) {
  return new Promise ((resolve) => {
    function cb () {
      video.removeEventListener('seeked', cb);
      resolve();
    }
    video.addEventListener('seeked', cb);
    video.currentTime = time;
  });
}

async function drawPreviewBar(video) {
  const { duration, videoWidth, videoHeight } = video;
  const previewBarFrameWidth = previewBarHeight  * videoWidth / videoHeight;
  const previewBarFrames = previewBarWidth / previewBarFrameWidth;
  for (let i = 0; i < previewBarFrames; i++) {
    await goToTime(video, i * duration * previewBarFrameWidth / previewBarWidth);
    previewBarContext.drawImage(video, 0, 0, videoWidth, videoHeight, previewBarFrameWidth * i, 0, previewBarFrameWidth, previewBarHeight);
  }
}

function loadVideo(file) {
  var src = URL.createObjectURL(file);
  var loaded = new Promise(function (resolve) {
    invisibleVideo.addEventListener('loadedmetadata', resolve);
  }).then(function () {
    videoLoaded = true;
    document.body.classList.add('loaded');
    return drawPreviewBar(invisibleVideo);
  });
  videoElement.src = src;
  invisibleVideo.src = src;
  return loaded;
}

function updateSelectorBar() {
  selector.style.width = selectorWidth + 'px';
  selector.style.left = selectorLeft + 'px';
  leftMask.style.width = selectorLeft + 'px';
  rightMask.style.left = (selectorLeft + selectorWidth) + 'px';
  rightMask.style.width = (previewBarWidth - selectorWidth - selectorLeft) + 'px';
}

function selectorUpdated() {
  if (!videoLoaded) {
    return;
  }
  var startFraction = selectorLeft / previewBarWidth;
  var durationFraction = selectorWidth / previewBarWidth;
  var startTime = startFraction * invisibleVideo.duration;
  var duration = durationFraction * invisibleVideo.duration;
  var endTime = startTime + duration;
  // do something with startTime, endTime, and duration, maybe;
  videoElement.currentTime = startTime;
}

function addDragHandler (event, cb, ecb) {
  var startX = event.clientX;
  function dragged(e) {
    cb(e.clientX - startX);
  }
  window.addEventListener('mousemove', dragged);
  window.addEventListener('mouseup', function ended() {
    window.removeEventListener('mousemove', dragged);
    window.removeEventListener('mouseup', ended);
    ecb();
  });
}

updateSelectorBar();
topHandle.addEventListener('mousedown', function (e) {
  var startLeft = selectorLeft;
  addDragHandler(e, function (dx) {
    selectorLeft = Math.max(0, Math.min(previewBarWidth - selectorWidth, startLeft + dx));
    updateSelectorBar();
  }, selectorUpdated);
});

leftHandle.addEventListener('mousedown', function (e) {
  var startLeft = selectorLeft;
  var startWidth = selectorWidth;
  addDragHandler(e, function (dx) {
    selectorLeft = Math.max(0, Math.min(selectorLeft + selectorWidth - minimumPreviewBarWidth, startLeft + dx));
    selectorWidth = (startWidth + startLeft - selectorLeft);
    updateSelectorBar();
  }, selectorUpdated);
});

rightHandle.addEventListener('mousedown', function (e) {
  var startWidth = selectorWidth;
  addDragHandler(e, function (dx) {
    selectorWidth = Math.max(minimumPreviewBarWidth, Math.min(previewBarWidth - selectorLeft, startWidth + dx));
    updateSelectorBar();
  }, selectorUpdated);
});

var pendingLoad = Promise.resolve();
inputElement.addEventListener('change', function () {
  let file = inputElement.files[0];
  pendingLoad = pendingLoad.then(function () {
   return loadVideo(file)
  });
});
#video {
  width: 100%;
  height: auto;
}

#preview-bar {
  position: relative;
  margin-top: 10px;
}
.canvas-container {
  position: relative;
  left: 5px;
}

#preview-bar-canvas {
  position: relative;
  top: 2px;

}

#selector {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  border: 2px solid orange;
  border-left-width: 5px;
  border-right-width: 5px;
  border-radius: 3px;
  overflow: show;
  opacity: 0;
  transition: opacity 1s;
  pointer-events: none;
}

.loaded #selector{
  opacity: 1;
  pointer-events: initial;
}

#selector-top-handle {
  position: absolute;
  top: 0;
  height: 10px;
  width: 100%;
  max-width: 30px;
  left: 50%;
  transform: translate(-50%,-100%);
  background: darkgreen;
  border-top-left-radius: 5px;
  border-top-right-radius: 5px;
  cursor: move;
}

.resize-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 5px;
  cursor: ew-resize;
  background: darkgreen;
  opacity: 0.75;
  transition: opacity 1s;
}

.resize-handle:hover {
  opacity: 1;
}

.preview-mask {
  position: absolute;
  background: black;
  opacity: 0.6;
  top: 0;
  bottom: 0;
}

#selector-left-mask {
  left: 0;
}

#selector-left-handle {
  left: -5px;
}
#selector-right-handle {
  right: -5px;
}
<input type="file" id="file-picker" />
<video id="video"></video>
<div id="preview-bar">
  <div class="canvas-container">
    <canvas id="preview-bar-canvas"></canvas>
    <div id="selector-left-mask" class="preview-mask"></div>
    <div id="selector-right-mask" class="preview-mask"></div>
  </div>
  <div id ="selector">
     <div id="selector-top-handle"></div>
     <div id="selector-left-handle" class="resize-handle"></div>
     <div id="selector-right-handle" class="resize-handle"></div>
  </div>
</div>
User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement