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.

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.

dash, Video HTML streaming

DASH – encrypting and playing video with ClearKey (videojs)

This does not work in all browsers. The better solution is HLS. It took me a lot of time to create working videojs player for encrypted DASH. Here is how I created encrypted DASH files from source.mp4 (choose your own key, KID, IV).

key: 617D8A125A284DF48E3C6B1866348A3F
KID: B326F895B6A24CC5A4DC70995728059C
IV: random

note: output is generated in .\dash directory

You will need these tools:
ffmpeg
Bento4 tools

ffmpeg -i source.mp4 -vn -acodec aac -ab 128k audio.mp4 
ffmpeg.exe -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 video-720p.mp4
ffmpeg.exe -i source.mp4 -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 video-480p.mp4
ffmpeg.exe -i source.mp4 -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 video-360p.mp4
ffmpeg.exe -i source.mp4 -an -c:v libx264 -preset veryslow -profile:v high -level 4.2 -b:v 700k -minrate 700k -maxrate 700k -bufsize 1400k -g 96 -keyint_min 96 -sc_threshold 0 -filter:v "scale='trunc(oh*a/2)*2:240'" -pix_fmt yuvj420p video-240p.mp4 
 
mp4fragment video-720p.mp4 video-720p-fragmented.mp4
mp4fragment video-480p.mp4 video-480p-fragmented.mp4
mp4fragment video-360p.mp4 video-360p-fragmented.mp4
mp4fragment video-240p.mp4 video-240p-fragmented.mp4 
mp4fragment audio.mp4 audio-fragmented.mp4 
 
mp4encrypt --method MPEG-CENC --key 1:617D8A125A284DF48E3C6B1866348A3F:random --property 1:KID:B326F895B6A24CC5A4DC70995728059C  --global-option mpeg-cenc.eme-pssh:true video-240p-fragmented.mp4 video-240p-fragmented-encrypted.mp4
mp4encrypt --method MPEG-CENC --key 1:617D8A125A284DF48E3C6B1866348A3F:random --property 1:KID:B326F895B6A24CC5A4DC70995728059C  --global-option mpeg-cenc.eme-pssh:true video-360p-fragmented.mp4 video-360p-fragmented-encrypted.mp4
mp4encrypt --method MPEG-CENC --key 1:617D8A125A284DF48E3C6B1866348A3F:random --property 1:KID:B326F895B6A24CC5A4DC70995728059C  --global-option mpeg-cenc.eme-pssh:true video-480p-fragmented.mp4 video-480p-fragmented-encrypted.mp4
mp4encrypt --method MPEG-CENC --key 1:617D8A125A284DF48E3C6B1866348A3F:random --property 1:KID:B326F895B6A24CC5A4DC70995728059C  --global-option mpeg-cenc.eme-pssh:true video-720p-fragmented.mp4 video-720p-fragmented-encrypted.mp4
mp4encrypt --method MPEG-CENC --key 1:617D8A125A284DF48E3C6B1866348A3F:random --property 1:KID:B326F895B6A24CC5A4DC70995728059C  --global-option mpeg-cenc.eme-pssh:true audio-fragmented.mp4 audio-fragmented-encrypted.mp4
  
call mp4dash -o dash video-240p-fragmented-encrypted.mp4 video-360p-fragmented-encrypted.mp4 video-480p-fragmented-encrypted.mp4 video-720p-fragmented-encrypted.mp4 audio-fragmented-encrypted.mp4
 
del /F /Q audio.mp4 video-720p.mp4 video-480p.mp4 video-360p.mp4 video-240p.mp4 audio-fragmented.mp4 video-720p-fragmented.mp4 video-480p-fragmented.mp4 video-360p-fragmented.mp4 video-240p-fragmented.mp4 video-240p-fragmented-encrypted.mp4 video-360p-fragmented-encrypted.mp4 video-480p-fragmented-encrypted.mp4 video-720p-fragmented-encrypted.mp4 audio-fragmented-encrypted.mp4

Now you have created encrypted files for DASH and you can now play it in web browser using JavaScript.

You will need these libraries:
video.js
videojs-contrib-dash
dash.js

Here is example of index.html file:

<!DOCTYPE html>
<html>
<head>
   <link href="Static/css/video-js.css" rel="stylesheet">
   <title></title>
   <meta charset="utf-8" />
   <script src="Static/js/video.js"></script>
   <script src="Static/js/dash.all.debug.js"></script>
   <script src="Static/js/videojs-dash.min.js"></script>
   <script src="Static/js/test.js"></script>
</head>
<body onload="init()">
   <video id='example-video' width=600 height=300 class="video-js vjs-default-skin" controls> </video>
</body>
</html>

Here is example of my test.js file:

note:
“syb4lbaiTMWk3HCZVygFnA” is base64 string equal to KID hexadecimal string “B326F895B6A24CC5A4DC70995728059C”
“YX2KElooTfSOPGsYZjSKPw” is base64 string equal to key hexadecimal string “617D8A125A284DF48E3C6B1866348A3F”

var init = function () {
   var options = {
      src: 'video/dash/stream.mpd',
      type: 'application/dash+xml',
      keySystemOptions: [
          {
             name: 'org.w3.clearkey',
             options: {
                //serverURL: 'api/dashkey',
                clearkeys: {
                   "syb4lbaiTMWk3HCZVygFnA": "YX2KElooTfSOPGsYZjSKPw"
                }
             }
          }
      ]
   };
   var player = videojs('example-video');
   player.src(options);
   player.play();
};

You can use serverURL property instead of clearkeys. Here is an example of implementation of DashKeyController in C# for this request:

public class DashKey
   {
      public string kid;
      public string k;
   }
   public class DashResponse
   {
      public IEnumerable<DashKey> keys;
   }
   public class DashKeyController : ApiController
   {
      [HttpGet]
      [ActionName("get")]
      public HttpResponseMessage Get()
      {
         return Request.CreateResponse(HttpStatusCode.OK, new DashResponse()
         {
            keys = new List<DashKey>() { new DashKey() {
               kid = "syb4lbaiTMWk3HCZVygFnA",
               k = "YX2KElooTfSOPGsYZjSKPw"
            } }
         });
      }
   }

I hope this will help somebody. I was doing research for DASH 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.