Video Thumbnail Generator

Online Snap Video Thumb

2024-10-26 19:48:46 - Coderja

index.php
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Online Video Thumbnail Generator</title>
  <meta name="description" content="Online tool to create snapshots of videos (local file or online by URL)">
  <link rel="stylesheet" href="css/style.css" >
  <style>

  </style>
  <!-- Global site tag (gtag.js) - Google Analytics -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=G-RKLKKLZFDQ"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag() { dataLayer.push(arguments); }
    gtag('js', new Date());
    gtag('config', 'G-RKLKKLZFDQ');
  </script>
  <link href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css" rel="stylesheet">
  <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>
  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
  <script>
    function showInput(id, opt) {
      console.log(opt)
      document.getElementById('select-file').style.display = 'none';
      document.getElementById('select-url').style.display = 'none';
      document.getElementById(id).style.display = '';
    }
    function loadPage() {
      var textFields = document.querySelectorAll('.mdc-text-field');
      for (let index = 0; index < textFields.length; index++) {
        var element = textFields[index];
        new mdc.textField.MDCTextField(element);
      }
    }
    window.addEventListener("load", loadPage);
  </script>
</head>

<body>
  <header class="mdc-top-app-bar">
    <div class="mdc-top-app-bar__row">
      <section class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start">
        <!--<button class="material-icons mdc-top-app-bar__navigation-icon mdc-icon-button" aria-label="Open navigation menu">menu</button>-->
        <span class="mdc-top-app-bar__title">Online Video Thumbnail Generator</span>
      </section>
      <!-- <section class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end" role="toolbar">
        <a class="toolbar-menu" target="_blank" href="https://forms.gle/AkvW2rRQsTfobxFE8"
          title="Feedback and feature requests">Feedback</a>
        &nbsp;
        <button onclick="document.location='https://github.com/fraigo/online-video-thumbnail-generator'"
          class="material-icons mdc-top-app-bar__action-item mdc-icon-button" aria-label="Options">
          <img src="https://github.com/fluidicon.png" width="48"></button>
      </section> -->
    </div>
  </header>
  <main class="mdc-top-app-bar--fixed-adjust">
    <div class="video_data">
      <div class="mdc-card mdc-card--outlined">
        <div>
          <div class="mdc-form-field" >
            <div class="mdc-radio">
              <input class="mdc-radio__native-control" type="radio" onclick="showInput('select-url', this)" id="video_url"
                name="video_source">
              <div class="mdc-radio__background">
                <div class="mdc-radio__outer-circle"></div>
                <div class="mdc-radio__inner-circle"></div>
              </div>
              <div class="mdc-radio__ripple"></div>
            </div>
            <label for="video_url">Video from URL</label>
          </div>
 
          <div class="mdc-form-field" >
            <div class="mdc-radio">
              <input class="mdc-radio__native-control" type="radio" onclick="showInput('select-file', this)" id="video_file"
                checked name="video_source">
              <div class="mdc-radio__background">
                <div class="mdc-radio__outer-circle"></div>
                <div class="mdc-radio__inner-circle"></div>
              </div>
              <div class="mdc-radio__ripple"></div>
            </div>
            <label for="video_file">Video from file</label>
          </div>
        </div>

        <div class="instructions mdc-form-field">(Currently, youtube/vimeo video URLS are not supported, only URLs
          pointing to a video resource)</div>
      </div>
      <br />
      <div class="mdc-card mdc-card--outlined select-source">
        <div id="select-file">
          <input type="file" class="mdc-text-field__input" id="videofile" accept=".mp4" onchange="loadVideoFile()">
        </div>
        <div id="select-url" style="display:none">
          <label class="mdc-text-field mdc-text-field--filled">
            <span class="mdc-text-field__ripple"></span>
            <span class="mdc-floating-label" id="url-label">Video URL</span>
            <input id="videourl" size="100" class="mdc-text-field__input" type="text" placeholder="https://"
              aria-labelledby="url-label">
            <span class="mdc-line-ripple"></span>
          </label>
          <div class="mdc-touch-target-wrapper">
            <button class="mdc-button mdc-button--raised"
              onclick="loadVideoURL(document.getElementById('videourl').value)">
              <span class="mdc-button__ripple"></span>
              <span class="mdc-button__touch"></span>
              <span class="mdc-button__label">Load video</span>
            </button>
          </div>
          <div class="instructions mdc-form-field">
            For youtube URLs, you can use a service to convert Youtube videos to MP4 files (Google for "Youtube mp4").
            <ul>
              <li>Use the provided "Download link" URL here, or </li>
              <li>Download the file to your device and select "Video from file"</li>
            </ul>
          </div>
        </div>
      </div>
      <div class="mdc-card mdc-card--outlined video-preview">
        <video id="video" controls width="640">
          <source src="blank.mp4" type="video/mp4">
        </video>
        <div id="videoInfo" class="mdc-card">
        </div>
        <div>Select the frame to capture using video controls.</div>
        <div id="videoControls" class="video-controls" style="display:none">
          <div>
              <input id="slider" oninput="goToTime(video,this.value)" onmouseover="this.title=this.value+'seg'" type="range" min="0" max="100" value="0" name="position">
          </div>
          <div class="button-container">
            <button category="controls" onclick="goToTime(video,0)">⏮<br><span>&nbsp;</span></button>
            <button category="controls" onclick="goToTime(video,video.currentTime-60)">⏪<br><span>-1m</span></button>
            <button category="controls" onclick="goToTime(video,video.currentTime-5)">⏪<br><span>-5s</span></button>
            <button category="controls" onclick="goToTime(video,video.currentTime-1)">⏪<br><span>-1s</span></button>
            <button category="controls" onclick="goToTime(video,video.currentTime-1/25)">⏪<br><span>-1fr</span></button>
            <button category="controls" onclick="video.paused?video.play():video.pause()">⏯<br><span>&nbsp;</span></button>
            <button category="controls" onclick="goToTime(video,video.currentTime+1/25)">⏩<br><span>+1fr</span></button>
            <button category="controls" onclick="goToTime(video,video.currentTime+1)">⏩<br><span>+1s</span></button>
            <button category="controls" onclick="goToTime(video,video.currentTime+5)">⏩<br><span>+5s</span></button>
            <button category="controls" onclick="goToTime(video,video.currentTime+60)">⏩<br><span>+1m</span></button>
            <button category="controls" onclick="goToTime(video,video.duration)">⏭<br><span>&nbsp;</span></button>  
          </div>
        </div>
      </div>
    </div>

    <div class="video_output">
      <div class="mdc-card mdc-card--outlined select-source">
        <div>
          <label class="mdc-text-field mdc-text-field--filled" id="videowidth">
            <span class="mdc-text-field__ripple"></span>
            <span class="mdc-floating-label" id="size-label">Image width</span>
            <input id="videow" type=number size="5" onchange="resize()" value="640" class="mdc-text-field__input"
              type="text" placeholder="" aria-labelledby="size-label" readonly>
            <span class="mdc-line-ripple"></span>
          </label>
          <button id="snap" class="mdc-button mdc-button--raised snap_photo_btn" onclick="snapPicture()" disabled>
            <span class="mdc-button__ripple"></span>
            <span class="mdc-button__touch"></span>
            <span class="mdc-button__label">Snap photo</span>
            <span></span>
          </button>
        </div>
        <div class="canvas-container">
          <canvas id="canvas" width="640" height="480"></canvas>
          <div>
            <button id="save" class="mdc-button mdc-button--raised save_btn" onclick="savePicture(this)" disabled>
              <span class="mdc-button__ripple"></span>
              <span class="mdc-button__touch"></span>
              <span class="mdc-button__label save_txt">Save Image</span>
              <span></span>
            </button>
            <span id="snapsize"></span>
          </div>
        </div>
        <a href="" id="imagelink" style="display:none">Image link</a>
      </div>
    </div>

  </main>

  <!-- start the script ... within that declare variables as follows... -->
  <script src="js/app.js?20230605"></script>

</body>

</html>
app.js


var video = document.querySelector('#video');
var canvas = document.querySelector('#canvas');
var file = document.querySelector('#videofile');
var videoControls = document.querySelector('#videoControls');
var videow = document.querySelector('#videow');
var snap = document.querySelector('#snap');
var save = document.querySelector('#save');
var videoInfo = document.querySelector('#videoInfo');
var snapSize = document.querySelector('#snapsize');
var context = canvas.getContext('2d');
var slider = document.querySelector('#slider');
var w, h, ratio;
//add loadedmetadata which will helps to identify video attributes

function timeUpdate() {
  slider.setAttribute('max', Math.ceil(video.duration))
  slider.value=video.currentTime
  videoInfo.style.display='block';
  videoInfo.innerHTML = [
    "Video size: " + video.videoWidth + "x" + video.videoHeight,
    "Video length: " + (Math.round(video.duration * 10) / 10) + "sec",
    "Playback position: " + (Math.round(video.currentTime * 10) / 10) + "sec",
  ].join('<br>');
}

function goToTime(video,time){
  video.currentTime=Math.min(video.duration,Math.max(0,time));
  timeUpdate()
}

video.addEventListener('timeupdate', timeUpdate)

video.addEventListener('loadedmetadata', function () {
  console.log("Metadata loaded");
  videow.value = video.videoWidth;
  videoInfo.innerHTML = [
    "Video size: " + video.videoWidth + "x" + video.videoHeight,
    "Video length: " + (Math.round(video.duration * 10) / 10) + "sec",
  ].join('<br>');
  video.objectURL = false;
  video.play();
  video.pause();
  resize();
}, false);

function resize() {
  ratio = video.videoWidth / video.videoHeight;
  w = videow.value;
  h = parseInt(w / ratio, 10);
  canvas.width = w;
  canvas.height = h;
}

function snapPicture() {
  context.fillRect(0, 0, w, h);
  context.drawImage(video, 0, 0, w, h);
  snapSize.innerHTML = w + "x" + h;
}

function selectVideo() {
  file.click();
}

function loadVideoFile() {
  var fileInput = file.files[0];
  if (fileInput) {
    console.log("Loading...");
    console.log(fileInput);
    /*
    var reader  = new FileReader();
    reader.addEventListener("error", function () {
      console.log("Error loading video data");
    });
    reader.addEventListener('progress',function(ev){
      console.log("progress", ev.loaded, ev.total, Math.round(ev.loaded*100.0/ev.total));
    });
    reader.addEventListener("load", function () {
        console.log("Video data loaded");
        video.preload="metadata";
        video.src = reader.result;
      }, false);
    reader.readAsDataURL(fileInput);
    */
    if (video.objectURL && video.src) {
      URL.revokeObjectURL(video.src);
    }
    video.pleload = "metadata";
    video.objectURL = true;
    video.src = URL.createObjectURL(fileInput);
    videow.removeAttribute("readonly");
    snap.disabled = false;
    save.disabled = false;
    videoControls.style.display = '';
  }
}

function loadVideoFromFile(file) {
  let reader = new FileReader();
  reader.readAsArrayBuffer(file);
  reader.onload = function (e) {
    // The file reader gives us an ArrayBuffer:
    let buffer = e.target.result;
    // We have to convert the buffer to a blob:
    let videoBlob = new Blob([new Uint8Array(buffer)], { type: 'video/mp4' });
    // The blob gives us a URL to the video file:
    let url = window.URL.createObjectURL(videoBlob);
    video.src = url;
  }
}

function loadVideoURL(url) {
  video.preload = "metadata";
  video.src = url;
  videow.removeAttribute("readonly");
  snap.disabled = false;
  save.disabled = false;
}

function savePicture(btn) {
  btn.disabled = true
  var dataURL = canvas.toDataURL();
  var link = document.getElementById("imagelink");
  link.style.display = '';
  link.style.opacity = 0
  link.href = dataURL;
  var rnd = Math.round((Math.random() * 10000));
  link.setAttribute("download", "bacolchina.com-" + rnd + ".png");
  link.click();
  setTimeout(function(){
    btn.disabled = false
    link.style.display = 'none';
  },100)
}

window.addEventListener("load", function () {
  var buttons = document.querySelectorAll('button');
  for (let index = 0; index < buttons.length; index++) {
    var element = buttons[index];
    element.addEventListener('click', function () {
      var name = this.innerText.trim();
      var category = "button";
      if (this.getAttribute('category') == 'controls') {
        name = 'Video Controls';
        category = "controls";
      }
      var id = name.toLowerCase().replace(' ', '_');
      gtag("event", category + "-" + id, {});
    })
  }
})



style.css
```css
/* Updated CSS for a more beautiful look */

body {
    padding: 0;
    margin: 0;
    font-family: Arial, sans-serif;
}

main {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}

canvas {
    border: 1px solid #ddd;
    max-width: 45vw;
    max-height: 90vh;
    margin-bottom: 15px;
    border-radius: 8px;
}

.mdc-top-app-bar__row {
    height: 50px !important;
}

.mdc-top-app-bar--fixed-adjust {
    padding-top: 50px !important;
}

video {
    max-width: 45vw;
    max-height: 90vh;
    margin: 10px auto;
    display: block;
    border-radius: 8px;
}

#videourl {
    max-width: 82%;
}

#videoInfo {
    background-color: #f5f5f5;
    display: none;
    padding: 10px;
    border-radius: 8px;
}

main>div {
    margin: 20px 12px;
}

.select-source {
    padding: 12px;
    flex-wrap: wrap;
}

.video-preview {
    text-align: center;
}

.instructions {
    padding: 12px;
    flex-wrap: wrap;
}

.toolbar-menu {
    color: #fff !important;
    padding-right: 12px;
}

.video_data {
    width: 50%;
}

.video_output {
    display: flex;
    flex: 1;
}

.video_output > .mdc-card {
    width: 100%;
    border-radius: 8px;
}

.instructions ul {
    margin-bottom: 0;
}

.mdc-touch-target-wrapper {
    position: absolute;
    top: 22px;
    right: 12px;
}

.mdc-text-field--filled {
    background-color: #fff !important;
}

.mdc-text-field--filled:active {
    background-color: #fff !important;
}

.snap_photo_btn {
    margin-bottom: 7px;
    margin-left: 15px;
}

.buttons {
    display: flex;
}

.buttons h4 {
    padding-right: 15px;
    font-family: Roboto;
    font-weight: normal;
    color: rgba(0, 0, 0, 0.87);
}

.space {
    width: 20px;
}

.mdc-card {
    font-family: Roboto, sans-serif;
    padding: 8px;
    margin: 4px 0;
    border-radius: 8px;
}

.snap_button {
    background: #6200ee;
    border: 0;
    border-radius: 4px;
    padding: 0 15px 2px;
    font-size: inherit;
    color: #fff;
    cursor: pointer;
    box-shadow: 0px 5px 3px -3px rgba(0, 0, 0, 0.50);
    height: 37px;
    margin: 10px 0;
    transition: background 0.3s;
}

.snap_button:hover {
    background: #8731ff;
}

.snap_button:active {
    background: #c49dff;
}

.canvas-container {
    margin-top: 16px;
    text-align: center;
}

#videoControls > div {
    margin: 8px 0;
}

#videoControls .button-container {
    text-align: center;
}

#videoControls button {
    font-size: 2em;
    border-radius: 5px;
    background-color: #eee;
    border: 1px solid #aaa;
    padding: 8px 16px;
}

#slider {
    width: 100%;
}

#videoControls button > span {
    font-size: 16px;
}

@media (max-width: 1166px) {
    #videoControls button {
        font-size: 24px;
    }
}

@media (max-width: 1023px) {
    main {
        flex-wrap: wrap;
    }

    .video_data {
        width: 100%;
    }

    .video_output {
        width: 100%;
    }

    video {
        max-width: 90vw;
    }

    canvas {
        max-width: 90vw;
    }
}

@media (max-width: 600px) {
    #videoControls button {
        font-size: 20px;
    }
}

@media (max-width: 480px) {
    #snap {
        width: 100px;
    }

    #videoControls > div {
        display: flex;
        justify-content: space-between;
    }

    #videoControls button {
        font-size: 18px;
        padding: 0 4px;
    }

    #videoControls button > span {
        display: block;
        text-align: center;
    }
}

More Posts