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!

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.