player, Video HTML streaming

Cross browser video player

I tried using videojs for playing encrypted HLS video in all common browsers. It didn’t go well. I had a lot of trouble to get it working in Safari browser on MacOS (seeking). The best thing I did was writing my own video player. Here is an example of it.

Example of index.html:

<html>
<head>
   <title></title>
   <meta charset="utf-8" />
   <link rel="stylesheet" type="text/css" href="test.css" />
   <script src="Static/js/hls.js"></script>
   <script src="Static/js/test.js"></script>
</head>
<body>
   <div id ="video-container">
      <video id ="video" autoplay>
         <source src ="hls/master.m3u8" type="application/x-mpegURL" />
         Your browser does not support the HLS video or the video tag.
      </video>
   </div>
</body>
</html>

Example of test.js file:

var INIT_VOLUME = 0.3;
 
function isFullscreen() {
   if (document.fullscreenElement || document.webkitCurrentFullScreenElement ||
      document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) {
      return true;
   }
   return false;
}
function requestFullscreen(el) {
   if (el.requestFullscreen) {
      el.requestFullscreen();
   }
   else if (el.mozRequestFullScreen) {
      el.mozRequestFullScreen();
   }
   else if (el.webkitRequestFullScreen) {
      el.webkitRequestFullScreen();
   }
   else if (el.webkitRequestFullscreen) {
      el.webkitRequestFullscreen();
   }
   else if (el.msRequestFullscreen) {
      el.msRequestFullscreen();
   }
}
function cancelFullscreen() {
   if (document.exitFullscreen) {
      document.exitFullscreen();
   }
   else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen();
   }
   else if (document.webkitCancelFullScreen) {
      document.webkitCancelFullScreen();
   }
   else if (document.msExitFullscreen) {
      document.msExitFullscreen();
   }
}
function fromSeconds(value) {
   var hours = Math.floor(value / 3600);
   value = value - hours * 3600;
   var minutes = Math.floor(value / 60);
   value = value - minutes * 60;
   var hoursStr = ("0" + hours).slice(-2);
   var minutesStr = ("0" + minutes).slice(-2);
   var secondsStr = ("0" + Math.round(value)).slice(-2);
   if (hours > 0)
      return hoursStr + ":" + minutesStr + ":" + secondsStr;
   return minutesStr + ":" + secondsStr;
}
function createControls() {
   $('#video-container').append('< div id="video-controls">' +
      '<button id="play"><i class="icon icon-play"></i></button>' +
      '<button id="volume-btn"><i class="icon icon-volume"></i></button>' +
      '<span id="current-time">00:00</span>' +
      '< div id="progress">' +
      '< div id="progress-buf"></div>' +
      '< div id="progress-bar"></div>' +
      '</div>' +
      '<span id="total-time"></span>' +
      '<button id="fullscreen"><i class="icon icon-fullscreen"></i></button>' +
      '<button id="levels">auto</button>' +
      '</div>' +
      '< div class="loader"></div>');
   var volumeBar = $('< div id="volume" style="display:none">< div id="volume-bar">< div id="volume-value" style="height: ' + INIT_VOLUME * 100 + '%"></div></div></div>');
   $('#video-controls').append(volumeBar);
   $('#volume-btn').click(function () {
      if (volumeBar.is(":visible")) {
         $(this).removeClass('active');
         volumeBar.hide();
      }
      else {
         $(this).addClass('active');
         volumeBar.show();
      }
   });
}
function createLevelButtons(hls) {
   var levels = hls.levelController._levels;
   var list = $('<ul id="video-levels" style="display:none">');
   for (var i = 0; i < levels.length; i++) {
      list.append('<li level="' + i + '">' + levels[i].height + 'p</li>');
   }
   list.append('<li level="-1" style="display:none">auto</li>');
   $('#video-controls').append(list);
   $("body").on("click""#video-levels li"function () {
      var level = parseInt($(this).attr("level"));
      hls.currentLevel = level; //immediate (nextLevel for later change)
      $('#levels').text($(this).text());
      list.hide();
      $("#video-levels li").show();
      $(this).hide();
      $('#levels').removeClass('active');
      $(".loader").show();
      $('#progress-buf').width(0);
   });
   $('#levels').click(function () {
      if (list.is(":visible")) {
         $(this).removeClass('active');
         list.hide();
      }
      else {
         $(this).addClass('active');
         list.show();
      }
   });
}
 
$(document).ready(function () {
   var video = document.getElementById('video');
   if (Hls.isSupported()) {
      createControls();
      var vid = $("#video");
      var getLoadedPercentage = function () {
         try {
            var range = 0;
            var bf = video.buffered;
            var time = video.currentTime;
            while (!(bf.start(range) <= time && time <= bf.end(range))) {
               range += 1;
            }
            return bf.end(range) / video.duration;
         }
         catch (e) {
            return 0;
         }
      };
      $(window).resize(function () {
         $('#video-controls').width(vid.width());
      });
      vid.bind('webkitfullscreenchange mozfullscreenchange msfullscreenchange fullscreenchange resize orientationchange'function () {
         $('#video-controls').width(vid.width());
      });
      var hls = new Hls();
      hls.loadSource('hls/master.m3u8');
      hls.attachMedia(video);
      hls.on(Hls.Events.MANIFEST_PARSED, function () {
         createLevelButtons(hls);
         video.volume = INIT_VOLUME;
         video.play();
      });
      hls.on(Hls.Events.BUFFER_APPENDED, function () {
         var loadEndPercentage = getLoadedPercentage();
         $('#progress-buf').width((loadEndPercentage * 100+ "%");
      });
      $('#play').click(function () {
         if (video.paused)
            video.play();
         else
            video.pause();
      });
      $('#volume-bar').click(function (e) {
         var y = e.pageY - this.offsetTop - $(this).parent()[0].offsetTop - $(this).parent().parent()[0].offsetTop;
         var position = 1 - (y / $('#volume-bar').height());
         video.volume = position;
         $('#volume-value').css('height', (position * 100+ '%');
      });
      $('#progress').click(function (e) {
         var x = e.pageX - this.offsetLeft - $(this).parent()[0].offsetLeft;
         var position = x / $('#progress').width();
         video.currentTime = position * video.duration;
         $('#progress-bar').css('width', (position * 100+ '%');
      });
      $('#fullscreen').click(function () {
         if (isFullscreen()) {
            cancelFullscreen();
            $('body').removeClass("fullscreen");
            $(this).removeClass('active');
         }
         else {
            var videoContainerEl = videoContainer[0];
            requestFullscreen(videoContainerEl);
            $('body').addClass("fullscreen");
            $(this).addClass('active');
         }
      });
      vid.on('timeupdate'function () {
         var percentage = video.currentTime / video.duration;
         $('#progress-bar').css('width', (100 * percentage) + '%');
         $('#current-time').text(fromSeconds(video.currentTime));
      });
      vid.on('loadedmetadata'function () {
         $('#total-time').text(fromSeconds(video.duration));
         $('#video-controls').width(vid.width());
      });
      vid.on('playing'function () {
         $('#play').find('i').removeClass('icon-play').addClass('icon-pause');
         $(".loader").hide();
      });
      vid.on('seeking'function () {
         $(".loader").show();
      });
      vid.on('seeked'function () {
         $(".loader").hide();
      });
      vid.on('pause'function () {
         $('#play').find('i').removeClass('icon-pause').addClass('icon-play');
      });
      vid.on('loadstart'function (event) {
         $(".loader").show();
      });
      vid.on('canplay'function (event) {
         $(".loader").hide();
      });
      vid.on('waiting'function (event) {
         $(".loader").show();
      });
   }
   else {
      video.setAttribute("controls""controls");
   }
});
 

Example of test.css file:

.icon-fullscreen {
      background-imageurl('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDQ0IDQ0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA0NCA0NCIgd2lkdGg9IjUxMnB4IiBoZWlnaHQ9IjUxMnB4Ij4KICA8cGF0aCBkPSJtMjIsMGMtMTIuMiwwLTIyLDkuOC0yMiwyMnM5LjgsMjIgMjIsMjIgMjItOS44IDIyLTIyLTkuOC0yMi0yMi0yMnptLTQsMzBoMWMwLjYsMCAxLDAuNCAxLDF2MmMwLDAuNi0wLjQsMS0xLDFoLThjLTAuNiwwLTEtMC40LTEtMXYtOGMwLTAuNiAwLjQtMSAxLTFoMmMwLjYsMCAxLDAuNCAxLDF2MWMwLDAuNCAwLjUsMC43IDAuOSwwLjRsMi0yYzAuNC0wLjQgMS0wLjQgMS40LDBsMS40LDEuNGMwLjQsMC40IDAuNCwxIDAsMS40bC0yLDJjLTAuMywwLjMtMC4xLDAuOCAwLjMsMC44em0yLTE3YzAsMC42LTAuNCwxLTEsMWgtMWMtMC40LDAtMC43LDAuNS0wLjQsMC45bDIsMmMwLjQsMC40IDAuNCwxIDAsMS40bC0xLjQsMS40Yy0wLjQsMC40LTEsMC40LTEuNCwwbC0yLTJjLTAuMy0wLjMtMC45LTAuMS0wLjksMC40djFjMCwwLjYtMC40LDEtMSwxaC0yYy0wLjYsMC0xLTAuNC0xLTF2LThjMC0wLjYgMC40LTEgMS0xaDhjMC42LDAgMSwwLjQgMSwxdjEuOWgwLjF6bTE0LDIwYzAsMC42LTAuNCwxLTEsMWgtOGMtMC42LDAtMS0wLjQtMS0xdi0yYzAtMC42IDAuNC0xIDEtMWgxYzAuNCwwIDAuNy0wLjUgMC40LTAuOWwtMi0yYy0wLjQtMC40LTAuNC0xIDAtMS40bDEuNC0xLjRjMC40LTAuNCAxLTAuNCAxLjQsMGwyLDJjMC4zLDAuMyAwLjksMC4xIDAuOS0wLjR2LTFjMC0wLjYgMC40LTEgMS0xaDJjMC42LDAgMSwwLjQgMSwxdjguMWgtMC4xem0wLTE0YzAsMC42LTAuNCwxLTEsMWgtMmMtMC42LDAtMS0wLjQtMS0xdi0xYzAtMC40LTAuNS0wLjctMC45LTAuNGwtMiwyYy0wLjQsMC40LTEsMC40LTEuNCwwbC0xLjQtMS40Yy0wLjQtMC40LTAuNC0xIDAtMS40bDItMmMwLjMtMC4zIDAuMS0wLjktMC40LTAuOWgtMWMtMC42LDAtMS0wLjQtMS0xdi0yYzAtMC42IDAuNC0xIDEtMWg4YzAuNiwwIDEsMC40IDEsMXY4LjFoMC4xeiIgZmlsbD0iI0ZGRkZGRiIvPgo8L3N2Zz4K');
   }
 
   .icon-play {
      background-imageurl('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCAzMDAgMzAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAzMDAgMzAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgd2lkdGg9IjUxMnB4IiBoZWlnaHQ9IjUxMnB4Ij4KPGc+Cgk8Zz4KCQk8cGF0aCBkPSJNMTUwLDBDNjcuMTU3LDAsMCw2Ny4xNjIsMCwxNTBjMCw4Mi44NDEsNjcuMTU3LDE1MCwxNTAsMTUwczE1MC02Ny4xNTksMTUwLTE1MEMzMDAsNjcuMTYyLDIzMi44NDMsMCwxNTAsMHogICAgIE0yMDUuODQ2LDE1OC4yNjZsLTg2LjU1Nyw0OS45NzFjLTEuMzIsMC43NjUtMi43OTksMS4xNDQtNC4yNzIsMS4xNDRjLTEuNDczLDAtMi45NDktMC4zNzktNC4yNzQtMS4xNDQgICAgYy0yLjY0LTEuNTI1LTQuMjY5LTQuMzQ3LTQuMjY5LTcuNDAyVjEwMC44OWMwLTMuMDUzLDEuNjMxLTUuODgsNC4yNjktNy40MDJjMi42NDgtMS41MjgsNS45MDYtMS41MjgsOC41NTEsMGw4Ni41NTcsNDkuOTc0ICAgIGMyLjY0NSwxLjUzLDQuMjc0LDQuMzUyLDQuMjY5LDcuNDAyQzIxMC4xMiwxNTMuOTE2LDIwOC40OTQsMTU2Ljc0MSwyMDUuODQ2LDE1OC4yNjZ6IiBmaWxsPSIjRkZGRkZGIi8+Cgk8L2c+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==');
   }
 
   .icon-pause {
      background-imageurl('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCAzMDAuMDAzIDMwMC4wMDMiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwMC4wMDMgMzAwLjAwMzsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSI1MTJweCIgaGVpZ2h0PSI1MTJweCI+CjxnPgoJPGc+CgkJPHBhdGggZD0iTTE1MC4wMDEsMGMtODIuODM4LDAtMTUwLDY3LjE1OS0xNTAsMTUwYzAsODIuODM4LDY3LjE2MiwxNTAuMDAzLDE1MCwxNTAuMDAzYzgyLjg0MywwLDE1MC02Ny4xNjUsMTUwLTE1MC4wMDMgICAgQzMwMC4wMDEsNjcuMTU5LDIzMi44NDYsMCwxNTAuMDAxLDB6IE0xMzQuNDEsMTk0LjUzOGMwLDkuNDk4LTcuNywxNy4xOTgtMTcuMTk4LDE3LjE5OHMtMTcuMTk4LTcuNy0xNy4xOTgtMTcuMTk4VjEwNS40NiAgICBjMC05LjQ5OCw3LjctMTcuMTk4LDE3LjE5OC0xNy4xOThzMTcuMTk4LDcuNywxNy4xOTgsMTcuMTk4VjE5NC41Mzh6IE0xOTguOTU1LDE5NC41MzhjMCw5LjQ5OC03LjcwMSwxNy4xOTgtMTcuMTk4LDE3LjE5OCAgICBjLTkuNDk4LDAtMTcuMTk4LTcuNy0xNy4xOTgtMTcuMTk4VjEwNS40NmMwLTkuNDk4LDcuNy0xNy4xOTgsMTcuMTk4LTE3LjE5OHMxNy4xOTgsNy43LDE3LjE5OCwxNy4xOThWMTk0LjUzOHoiIGZpbGw9IiNGRkZGRkYiLz4KCTwvZz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K');
   }
 
   .icon-volume {
      background-imageurl('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCAzMDAuMDAzIDMwMC4wMDMiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwMC4wMDMgMzAwLjAwMzsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSI1MTJweCIgaGVpZ2h0PSI1MTJweCI+CjxnPgoJPGc+CgkJPHBhdGggZD0iTTE1MC4wMDUsMEM2Ny4xNjQsMCwwLjAwMSw2Ny4xNTksMC4wMDEsMTUwYzAsODIuODM4LDY3LjE2MiwxNTAuMDAzLDE1MC4wMDMsMTUwLjAwM1MzMDAuMDAyLDIzMi44MzgsMzAwLjAwMiwxNTAgICAgQzMwMC4wMDIsNjcuMTU5LDIzMi44NDQsMCwxNTAuMDA1LDB6IE0xOTcuMjQ1LDEwMC41MDZsMjcuMzU4LTE1Ljc5M2MzLjA4MS0xLjc3OSw3LjAxNi0wLjcyNCw4Ljc5NSwyLjM1NSAgICBjMS43NzcsMy4wODEsMC43MjQsNy4wMTgtMi4zNTUsOC43OTVsLTI3LjM1OCwxNS43OTVjLTMuMDc5LDEuNzc5LTcuMDE2LDAuNzI0LTguNzk1LTIuMzU1ICAgIEMxOTMuMTE0LDEwNi4yMjIsMTk0LjE2NywxMDIuMjg4LDE5Ny4yNDUsMTAwLjUwNnogTTE2NS44NjUsMjI0LjYyNGMtMS40MDMsMC42ODUtMi45MTgsMS4wMjItNC40MjUsMS4wMjIgICAgYy0yLjIxOCwwLTQuNDItMC43MzEtNi4yMzUtMi4xNTNMMTIyLjA0LDE5Ny41MWMtMS41MzUsMC4zNzktMy4xMzYsMC42MDctNC43OTYsMC42MDdIODQuMDEgICAgYy0xMC44OTgsMC0xOS43MzUtOC44MzYtMTkuNzM1LTE5LjczMnYtNTcuNTZjMC0xMC44OTYsOC44MzctMTkuNzM1LDE5LjczNS0xOS43MzVoMzMuMjM1YzEuOTMyLDAsMy43OTIsMC4yOSw1LjU2MSwwLjgwOSAgICBsMzIuMzk3LTI1LjM4OWMzLjA0Mi0yLjM4NCw3LjE4Ny0yLjgyNCwxMC42Ni0xLjEzMWMzLjQ3NSwxLjY5MSw1LjY4Myw1LjIxOCw1LjY4Myw5LjA4OHYxMzEuMDY5aDAuMDAyICAgIEMxNzEuNTQ3LDIxOS40MDYsMTY5LjMzNywyMjIuOTMzLDE2NS44NjUsMjI0LjYyNHogTTIzNC4yMzcsMjEzLjM4M2MtMS43NzksMy4wODEtNS43MTQsNC4xMzQtOC43OTUsMi4zNTVsLTI3LjM1OC0xNS43OTMgICAgYy0zLjA3OS0xLjc4Mi00LjEzMi01LjcxNi0yLjM1NS04Ljc5OGMxLjc3OS0zLjA4MSw1LjcxNi00LjEzNCw4Ljc5NS0yLjM1NWwyNy4zNTgsMTUuNzk1ICAgIEMyMzQuOTYxLDIwNi4zNjIsMjM2LjAxMSwyMTAuMjk5LDIzNC4yMzcsMjEzLjM4M3ogTTIzMS4xMzMsMTU2Ljc5NUgxOTkuNTVjLTMuNTU2LDAtNi40MzctMi44NzktNi40MzctNi40MzcgICAgYzAtMy41NTYsMi44ODItNi40MzcsNi40MzctNi40MzdoMzEuNTg1YzMuNTU2LDAsNi40MzcsMi44ODIsNi40MzcsNi40MzdDMjM3LjU3MywxNTMuOTE2LDIzNC42ODgsMTU2Ljc5NSwyMzEuMTMzLDE1Ni43OTV6IiBmaWxsPSIjRkZGRkZGIi8+Cgk8L2c+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==');
   }
 
   .icon {
      background-size30px;
      width30px;
      height30px;
   }
 
   #video-wrapper {
      positionabsolute;
      top0;
      left0;
      right0;
      bottom0;
      z-index2147483646;
      max-width100%;
      marginauto;
   }
 
   #video {
      height50%;
      positionabsolute;
      top0;
      left0;
      right0;
      bottom0;
      z-index2147483645;
      max-width100%;
      marginauto;
      background-colorblack;
   }
 
   #video-controls {
      positionabsolute;
      top75%;
      left0;
      right0;
      bottom0;
      z-index2147483647;
      max-width100%;
      marginauto;
      height40px;
      margin-top0;
      background-color#1d35e4;
      colorwhite;
   }
 
   body {
      text-aligncenter;
   }
 
      html,
      body,
      body.fullcreen > main {
         height100%;
      }
 
   #video-container {
      heightcalc(100% - 4px);
      margin0;
      padding0;
   }
 
   main {
      heightcalc(100% - 64px);
   }
 
   #progress {
      height10px;
      widthcalc(100% - 301px);
      background-colorwhite;
      displayinline-block;
      margin-left5px;
      cursorpointer;
   }
 
      #progress > #progress-bar {
         height100%;
         background-color#366ac7;
         width0;
         margin-top-10px;
      }
 
      #progress > #progress-buf {
         height100%;
         background-color#d8d4d4;
         width0;
      }
 
   #video-controls > span {
      width60px;
      displayinline-block;
      height40px;
      padding-top10px;
   }
 
   #video-controls > span > p{
      margin:0;
   }
 
   #video-controls > span#total-time {
      margin-left5px;
   }
 
   #video-controls > button {
      width40px;
      height40px;
      bordernone;
      padding0;
      background-colortransparent;
      floatleft;
   }
 
      #video-controls > button.active {
         background-color#19abd0;
      }
 
      #video-controls > button:hover {
         background-color#19abd0;
      }
 
      #video-controls > button#levels {
         floatright;
         width50px;
      }
 
      #video-controls > button#fullscreen {
         floatright;
      }
 
   body.fullcreen > header,
   body.fullcreen > main > .progress {
      displaynone;
   }
 
   body.fullscreen #video,
   body.fullscreen #video-wrapper {
      heightcalc(100% - 40px);
      bottom40px;
   }
 
   body.fullscreen #video-controls {
      width100% !important;
      topcalc(100% - 40px);
   }
 
   #video-levels {
      positionabsolute;
      right40px;
      bottom25px;
      background-color#19abd0;
      padding5px;
      width50px;
      z-index2147483647;
   }
 
      #video-levels > li {
         cursorpointer;
      }
 
         #video-levels > li:hover {
            background-color#1d35e4;
         }
 
   #volume {
      positionabsolute;
      width40px;
      height100px;
      background-color#19abd0;
      bottom40px;
      left41px;
   }
 
   #volume-bar {
      background-colorwhite;
      widthcalc(100% - 20px);
      heightcalc(100% - 20px);
      margin10px;
      positionrelative;
      cursorpointer;
   }
 
   #volume-value {
      background-color#366ac7;
      bottom0;
      positionabsolute;
      width100%;
   }
 
   .loader {
      border16px solid #f3f3f3/* Light grey */
      border-top16px solid #1d35e4/* Blue */
      border-radius50%;
      width120px;
      height120px;
      animationspin 2s linear infinite;
      positionabsolute;
      topcalc(50% - 60px);
      leftcalc(50% - 60px);
      z-index2147483647;
   }
 
   @keyframes spin {
      0% {
         transformrotate(0deg);
      }
 
      100% {
         transformrotate(360deg);
      }
   }

You can use it. There can be minor problems, because I parsed this code from my working solution and I don’t want to share everything. Please replace “< div” with “<div”. The wordpress formatting is shit.

ffmpeg, Video HTML streaming

FFmpeg – watermarks

If you want to create watermarks for video using ffmpeg, this might be useful. Here is an example.

We have our input video file source.mp4 and image file image.png. We want to create output file output.mp4 with watermark using the image file.

ffmpeg -y -i source.mp4 -i image.png -filter_complex "[1]format=bgra,colorchannelmixer=aa=1,rotate=0:c=black@0:ow=rotw(0):oh=roth(0)[image];[0][image]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2" output.mp4

The number in [] represents number of input (0,1,…). We have here only two inputs: [0] and [1]. First we are setting up image:

  • opacity – aa=1 means that the image is not transparent (value can be 0 – 1)
  • rotation – rotate=0, rotw(0), roth(0) means that the image is not rotated (instead of “0” we can add any value in radians)

Secondly we are overlaying input [0] (source.mp4) with [image] (our rotated and transparent image).

Now we have our video with watermarks. But what if we need to add more watermarks to one video. Here is an example of adding 3 different watermarks to video (image1.png, image2.png and image3.png):

ffmpeg -y -i source.mp4 -i image1.png -i image2.png -i image3.png -filter_complex "[1]format=bgra,colorchannelmixer=aa=1,rotate=0:c=black@0:ow=rotw(0):oh=roth(0)[image1];[2]format=bgra,colorchannelmixer=aa=0.8,rotate=1.5:c=black@0:ow=rotw(1.5):oh=roth(1.5)[image2];[3]format=bgra,colorchannelmixer=aa=0.5,rotate=-2:c=black@0:ow=rotw(-2):oh=roth(-2)[image3];[0][image1]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2[overlay1];[overlay1][imag2]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2[overlay2];[overlay2][image3]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2" output.mp4

First, we are setting up three different images:

  • [image1] – input [1], opacity = 1, rotation = 0
  • [image2] – input [2], opacity = 0.8, rotation 1.5 rad
  • [image3] – input [3], opacity = 0.5, rotation -2 (minus means opposite direction)

Secondly:

  • overlaying input [0] (our source.mp4) with [image1] and creating [overlay1]
  • overlaying [overlay1] with [image2] and creating [overlay2]
  • overlaying [overlay2] with [image3] and creating output.mp4 with all watermarks

I have written generation of arguments for ffmpeg in C# for adding watermarks:

private static string CreateArgs(string input, List<Watermark> watermarkInputs)
      {
         var buffer = new StringBuilder();
         var buffer2 = new StringBuilder();
         buffer.Append("-y ");
         buffer.AppendFormat("-i \"{0}\" ", input);
 
         foreach (var watermarkInput in watermarkInputs)
            buffer.AppendFormat("-i \"{0}\" ", watermarkInput.Path);
 
         if (watermarkInputs.Any())
            buffer.Append("-filter_complex \"");
 
         var previousOverlay = "0";
         var last = watermarkInputs.LastOrDefault();
         foreach (var watermarkInput in watermarkInputs)
         {
            buffer.AppendFormat("[{0}]format=bgra,colorchannelmixer=aa={1},rotate={2}:c=black@0:ow=rotw({2}):oh=roth({2})[{3}];",
               watermarkInput.Order,
               watermarkInput.Opacity.HasValue ? watermarkInput.Opacity.Value : 1,
               watermarkInput.Rotation / -57.2958, //to radians and oposite direction
               watermarkInput.Id);
            buffer2.AppendFormat("[{0}][{1}]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2", previousOverlay, watermarkInput.Id);
            if (!last.Equals(watermarkInput))
            {
               var newOverlay = Guid.NewGuid().ToString();
               buffer2.AppendFormat("[{0}];", newOverlay);
               previousOverlay = newOverlay;
            }
         }
 
         if (watermarkInputs.Any())
         {
            buffer.Append(buffer2.ToString());
            buffer.Append("\" ");
         }
 
         buffer.AppendFormat("output.mp4");
 
         return buffer.ToString();
      }

You will need this class for using this method:

public class Watermark
   {
      public int Rotation { get; set; } // in degrees
      public double? Opacity { get; set; } // 0 - 1
      public string Path { get; set; } // path to image file
      public string Id { get; set; } // unique id (Guid)
      public int Order { get; set; } // from 1 -> * (0 is video input)
   }

You can combine this with conversion and encrypting:

ffmpeg -y -i source.mp4 -i image1.png -i image2.png -i image3.png -filter_complex "[1]format=bgra,colorchannelmixer=aa=1,rotate=0:c=black@0:ow=rotw(0):oh=roth(0)[image1];[2]format=bgra,colorchannelmixer=aa=0.8,rotate=1.5:c=black@0:ow=rotw(1.5):oh=roth(1.5)[image2];[3]format=bgra,colorchannelmixer=aa=0.5,rotate=-2:c=black@0:ow=rotw(-2):oh=roth(-2)[image3];[0][image1]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2[overlay1];[overlay1][imag2]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2[overlay2];[overlay2][image3]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2" -hls_time 1 -hls_key_info_file key.info -hls_playlist_type vod -hls_segment_filename "output\fileSequence%d.ts" "output\stream.m3u8"

About converting to HLS and encrypting your video for most browsers, go to HLS – encrypting and playing video (videojs).

Video HTML streaming, videojs

Videojs – Quality selection (HLS)

This solution is for HLS – encrypting and playing video (videojs).

We can add quality options for videojs player. We have to add this javascript library:
videojs-contrib-quality-levels.js

Videojs options

It will iterate through video sources in master.m3u8 file and show buttons for them.

Example in typescript (javascript):

export function run() {
      var selectedBitrate = "auto";
      var lastPosition = 0;
      var qLevels = [];
 
      var options = {
         inactivityTimeout: 0,
         controls: true,
         autoplay: true,
         preload: "auto",
         notSupportedMessage: "Please use different browser (Mozzila Firefox, Google Chrome, Safari, Microsoft Edge)"
      };
 
      var srcOptions = {
         src: 'api/doc/' + DOCUMENT_ID + '/Video/master.m3u8',
         type: 'application/x-mpegurl'
      };
 
      var player = videojs('vid', options);
      player.qualityLevels().on('addqualitylevel'function (event) {
         event.qualityLevel.enabled = selectedBitrate === "auto" || event.qualityLevel.height.toString() === selectedBitrate;
      });
 
      player.on("loadedmetadata"function () {
         var qualityLevels = player.qualityLevels();
         for (var i = 0; i < qualityLevels.length; i++) {
            var quality = qualityLevels[i];
 
            if (quality.height != undefined && $.inArray(quality.height, qLevels) === -1) {
               qLevels.push(quality.height);
 
               if (!$('.quality_ul').length) {
                  var h = '< div class="quality_setting vjs-menu-button vjs-menu-button-popup vjs-control vjs-button">' +
                     '<button class="vjs-menu-button vjs-menu-button-popup vjs-button" type="button" aria-live="polite" aria-disabled="false" title="Quality" aria-haspopup="true" aria-expanded="false">' +
                     '<span aria-hidden="true" class="vjs-icon-cog"></span>' +
                     '<span class="vjs-control-text">Quality</span></button>' +
                     '< div class="vjs-menu"><ul class="quality_ul vjs-menu-content" role="menu"></ul></div></div>';
 
                  $(".vjs-fullscreen-control").before(h);
               } else {
                  $('.quality_ul').empty();
               }
 
               qLevels.sort();
               qLevels.reverse();
 
               var j = 0;
 
               $.each(qLevels, function (i, val) {
                  $(".quality_ul").append('<li class="vjs-menu-item" tabindex="' + i + '" role="menuitemcheckbox" aria-live="polite" aria-disabled="false" aria-checked="false" bitrate="' + val +
                     '"><span class="vjs-menu-item-text">' + val + 'p</span></li>');
 
                  j = i;
               });
 
               $(".quality_ul").append('<li class="vjs-menu-item vjs-selected" tabindex="' + (j + 1+ '" role="menuitemcheckbox" aria-live="polite" aria-disabled="false" aria-checked="true" bitrate="auto">' +
                  '<span class="vjs-menu-item-text">Auto</span></li>');
            }
         }
         player.currentTime(lastPosition);
      });
 
 
      $("body").on("click"".quality_ul li"function () {
         $(".quality_ul li").removeClass("vjs-selected");
         $(".quality_ul li").prop("aria-checked""false");
 
         $(this).addClass("vjs-selected");
         $(this).prop("aria-checked""true");
 
         selectedBitrate = $(this).attr("bitrate");
         lastPosition = player.currentTime();
         var levels = player.qualityLevels();
         var level = levels[levels.selectedIndex].height;
         if (selectedBitrate !== level.toString() && (selectedBitrate !== "auto" || levels.selectedIndex !== (levels.length - 1))) {
            player.src(srcOptions);
         }
         else {
            for (var i = 0; i < levels.length; i++)
               levels[i].enabled = selectedBitrate === "auto" || levels[i].height.toString() === selectedBitrate;
         }
      });
 
      player.on('fullscreenchange'function () {
         var levels = player.qualityLevels();
         if (selectedBitrate === "auto" && player.isFullscreen() && levels.selectedIndex !== (levels.length - 1)) {
            lastPosition = player.currentTime();
            player.src(srcOptions);
         }
      });
      player.src(srcOptions);
   }

Note: Please replace “< div” with “<div”. The formating is pissing me off!

hls, Video HTML streaming

HLS – encrypting and playing video

This solution works for all common web browsers including Internet Explorer, Microsoft Edge, Google Chrome (PC, Android), Mozilla Firefox, Safari (Mac, iOS). It will not work on IE on Windows 7 and less. It took me a lot of time to create working player for encrypted HLS. I you already have encrypted HLS video, click here for creating cross browser video player. Here is how I created encrypted HLS files from source.mp4 (choose your own key and key URL).

key: 617D8A125A284DF48E3C6B1866348A3F

You need these files:
key.bin – binary file representing key
key.info – text file with key information
master.m3u8 – text file with information about HLS sources

key.info file:

http://localhost:52523/api/hlskey
key.bin

note: The player will send GET request for key to this url http://localhost:52523/api/hlskey. On third line in this file can be IV.

Here is an example of implementation of HlsKeyController in C#:

public class HlsKeyController : ApiController
   {
      [HttpGet]
      [ActionName("get")]
      public HttpResponseMessage Get()
      {
         var response = Request.CreateResponse(HttpStatusCode.OK);
         response.Content = new ByteArrayContent(Guid.Parse("617D8A125A284DF48E3C6B1866348A3F").ToByteArray());
         response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
         return response;
      }
   }

master.m3u8 file:

#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="audio/stream.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=500000,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=426x240,AUDIO="audio"
video240/stream.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=640x360,AUDIO="audio"
video360/stream.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1500000,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=852x480,AUDIO="audio"
video480/stream.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=1280x720,AUDIO="audio"
video720/stream.m3u8

note: output is generated in .\hls directory

Here is a run.bat file. You will need this tool:
ffmpeg

mkdir hls\video720
mkdir hls\video480
mkdir hls\video360
mkdir hls\video240
mkdir hls\audio

ffmpeg -y -i source.mp4 -an -c:v libx264 -preset veryslow -profile:v high -level 4.2 -b:v 2000k -minrate 2000k -maxrate 2000k -bufsize 4000k -g 96 -keyint_min 96 -sc_threshold 0 -filter:v "scale='trunc(oh*a/2)*2:720'" -pix_fmt yuvj420p -hls_time 5 -hls_key_info_file key.info -hls_playlist_type vod -hls_segment_filename "hls\video720\fileSequence%%d.ts" hls\video720\stream.m3u8 ^
-an -c:v libx264 -preset veryslow -profile:v high -level 4.2 -b:v 1500k -minrate 1500k -maxrate 1500k -bufsize 3000k -g 96 -keyint_min 96 -sc_threshold 0 -filter:v "scale='trunc(oh*a/2)*2:480'" -pix_fmt yuvj420p -hls_time 5 -hls_key_info_file key.info -hls_playlist_type vod -hls_segment_filename "hls\video480\fileSequence%%d.ts" hls\video480\stream.m3u8 ^
-an -c:v libx264 -preset veryslow -profile:v high -level 4.2 -b:v 1000k -minrate 1000k -maxrate 1000k -bufsize 2000k -g 96 -keyint_min 96 -sc_threshold 0 -filter:v "scale='trunc(oh*a/2)*2:360'" -pix_fmt yuvj420p -hls_time 5 -hls_key_info_file key.info -hls_playlist_type vod -hls_segment_filename "hls\video360\fileSequence%%d.ts" hls\video360\stream.m3u8 ^
-an -c:v libx264 -preset veryslow -profile:v high -level 4.2 -b:v 500k -minrate 500k -maxrate 500k -bufsize 1000k -g 96 -keyint_min 96 -sc_threshold 0 -filter:v "scale='trunc(oh*a/2)*2:240'" -pix_fmt yuvj420p -hls_time 5 -hls_key_info_file key.info -hls_playlist_type vod -hls_segment_filename "hls\video240\fileSequence%%d.ts" hls\video240\stream.m3u8 ^
-vn -acodec aac -ab 128k -hls_time 1 -hls_key_info_file key.info -hls_playlist_type vod -hls_segment_filename "hls\audio\fileSequence%%d.ts" hls\audio\stream.m3u8

copy master.m3u8 hls\master.m3u8

If you wish to use this commands in CMD instead of calling run.bat file, please replace “fileSequence%%d” with “fileSequence%d”. Otherwise It will not work. Now you have created encrypted files for HLS and you can now play it in web browser using JavaScript.

You will need this library:
hls.js

Here is example of index.html file:

<html>
<head>
   <title></title>
   <meta charset="utf-8"   />
   <script src="Static/js/hls.js"></script>
   <script src="Static/js/test.js"></script>
</head>
<body>
   <video id='example-video'  controls>
      <source src="hls/master.m3u8" type="application/x-mpegURL" />
      Your browser does not support the HLS video or the video tag.
   </video>
</body>
</html>

Here is example of my test.js file:

$(document).ready(function () {
   if (Hls.isSupported()) {
      var video = document.getElementById('example-video');
      var hls = new Hls();
      hls.loadSource('hls/master.m3u8');
      hls.attachMedia(video);
      hls.on(Hls.Events.MANIFEST_PARSED, function () {
         video.play();
      });
   }
});

I hope this will help somebody. I was doing research for HLS and it took me a lot of time to understand this. If you don’t understand this or there is something wrong, leave a comment.

Note: For adding quality options (buttons) to videojs player, click here. Videojs player has some problems with Safari on MacOS. Recommending creating your own video player.
For adding watermarks to your video, click here.