Plyr: Quality switcher for HLS

Created on 24 Mar 2020  Â·  27Comments  Â·  Source: sampotts/plyr

It appears that HLS quality switching is a highly requested feature and several other players seem to have this plugin. I see the ground work was laid with this commit/issue below.
Is there any time frame to implement this feature? I think overall it's a great player and loads HLS videos faster for me than other open source players.

1607

Most helpful comment

Hey everyone, here is my working example of combining with Hls.js and plyr. The main idea is to configure option properly based on recent PR by @sampotts .

TLDR: working example https://codepen.io/datlife/pen/dyGoEXo

Main Idea

The goal is to set qualitiy options based on MANIFEST data loaded by HLS.

Notes before jumping into code

  • By streaming video using HLS protocol, we may just need to add single source tag in HTML (not multiple mp4 sources in the Plyr example). The reason is all available qualities is in the MANIFEST of playlist.m3u8. Therefore, we'd like the HLS performs the switching, not Plyr.

For example, our HTML video will be something like:

<video controls crossorigin playsinline >
  <source
      type="application/x-mpegURL" 
      <!-- playlist contains all available qualities for this stream --->
      src="https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8">
</video>
  • The manifest's playlist contains all available qualities, subtitles. Notice it has different RESOLUTIONS for the same video.
#EXTM3U
...

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=258157,CODECS="avc1.4d400d,mp4a.40.2",AUDIO="stereo",RESOLUTION=422x180,SUBTITLES="subs"
video/250kbit.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=520929,CODECS="avc1.4d4015,mp4a.40.2",AUDIO="stereo",RESOLUTION=638x272,SUBTITLES="subs"
video/500kbit.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=831270,CODECS="avc1.4d4015,mp4a.40.2",AUDIO="stereo",RESOLUTION=638x272,SUBTITLES="subs"
video/800kbit.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1144430,CODECS="avc1.4d401f,mp4a.40.2",AUDIO="surround",RESOLUTION=958x408,SUBTITLES="subs"
video/1100kbit.m3u8
....

#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="de",URI="subtitles_de.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",URI="subtitles_en.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Espanol",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",URI="subtitles_es.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",URI="subtitles_fr.m3u8"

Implementation

document.addEventListener("DOMContentLoaded", () => {
  const video = document.querySelector("video");
  const source = video.getElementsByTagName("source")[0].src;

  // For more options see: https://github.com/sampotts/plyr/#options
  const defaultOptions = {};

  if (Hls.isSupported()) {
    // For more Hls.js options, see https://github.com/dailymotion/hls.js
    const hls = new Hls();
    hls.loadSource(source);

    // From the m3u8 playlist, hls parses the manifest and returns
    // all available video qualities. This is important, in this approach,
    // we will have one source on the Plyr player.
    hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {

      // Transform available levels into an array of integers (height values).
      const availableQualities = hls.levels.map((l) => l.height)

      // Add new qualities to option
      defaultOptions.quality = {
        default: availableQualities[0],
        options: availableQualities,
        // this ensures Plyr to use Hls to update quality level
        // Ref: https://github.com/sampotts/plyr/blob/master/src/js/html5.js#L77
        forced: true,        
        onChange: (e) => updateQuality(e),
      }

      // Initialize new Plyr player with quality options
      const player = new Plyr(video, defaultOptions);
    });
    hls.attachMedia(video);
    window.hls = hls;
  } else {
    // default options with no quality update in case Hls is not supported
    const player = new Plyr(video, defaultOptions);
  }

  function updateQuality(newQuality) {
    window.hls.levels.forEach((level, levelIndex) => {
      if (level.height === newQuality) {
        console.log("Found quality match with " + newQuality);
        window.hls.currentLevel = levelIndex;
      }
    });
  }
});

All 27 comments

Upvoting, this is really a much need feature

Hi

We have implemented Plyr to our project, but have also found, that Plyr by default do not support multiple qualities for HLS. We needed it, so we tried to implement it.

The diff of our solution can be found at https://gist.github.com/Matho/b88d10da98471c114ca1a855882bbc85
It is not cleanest solution and a lot of work I have did in Ruby, because it was simple easier for me. Take this as the one of a way, or demonstration.

In ruby code, I'm downloading and parsing m3u8 file. Then, I'm extracting src links with quality information and passing to the html5 video element as various src elements. This should be done in javascript.

If you check the code in plyr.js.coffee, you can see how I switch the qualities. On quality change, I'm loading new file for HLS library. Because I know the seek time at that time, I set the seek time to be equal when playing new file. Thanks to it, I can continue with playing from the same point with different quality.

We have found bug, which is I think specific to our project. When I have activated the pip mode and changed the http url in browser, the pip video paused. So I have implemented the fix. It is the fix with browser_paused_pip variable.

I'm not saving quality to local storage (see storage option). Instead, I'm selecting the 'middle' quality as the default quality, when the player starts.

Maybe this will help you all. Maybe we will rewrite the m3u8 parsing to JS one day. Or maybe somebody will help me with this step?

Thanks and have a nice day

Hello folks, here's our solution in TS - it basically uses the levels parsed from the manifest and adds each quality automatically, hope it helps anyone:

```// When playlist manifest is parsed, load player
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
this.player = this.loadPlayer();
});

loadPlayer() {
const playerOptions = {
quality: {
default: '720',
options: ['720']
}
};

// If HLS is supported (ie non-mobile), we add video quality settings
if (Hls.isSupported()) {
playerOptions.quality = {
default: this.hls.levels[this.hls.levels.length - 1].height,
options: this.hls.levels.map((level) => level.height),
forced: true,
// Manage quality changes
onChange: (quality: number) => {
this.hls.levels.forEach((level, levelIndex) => {
if (level.height === quality) {
this.hls.currentLevel = levelIndex;
}
});
}
};
}

this.player = new Plyr(this.videoElement.nativeElement, playerOptions);

// Start HLS load on play event
this.player.on('play', () => this.hls.startLoad());

// Handle HLS quality changes
this.player.on('qualitychange', () => {
if (this.player.currentTime !== 0) {
this.hls.startLoad();
}
});

return this.player;
}

Hi @ThomasCantonnet Many thanks for sharing your code! I did the parsing in Ruby, now I can rewrite the app to do it with hls.js

Is there another issue keeping track of this or is this the canonical place for this feature?

Add the following code in the <head>

<script src="https://cdn.plyr.io/3.5.10/plyr.polyfilled.js"></script>
<script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.10/plyr.css" />

Add the following code in the

After hours of trying and testing, these tricks worked for me. I now have swich quality button, and it maintain the current playing time, too :D

  1. Make Plyr treat HLS link like video/mp4 so you will be able to access the switch quality button menu.
  2. Make sure when user click play button, the HLS will load, or the play won't work (because Plyr couldn't recognize the .m3u8 file).
  3. Check event on qualitychange, then reset the HLS link each time user click on it.

HTML

<link rel="stylesheet" href="//path/to/plyr.css" />
<div class="container">
    <video id="player" width="100%" dura="" src="" poster="" data-plyr-config='{"quality":{"default": 480}}' preload="none" controlsList="nodownload" controls crossorigin playsinline poster="">
           <source src="//path/to/video_480.m3u8" type="video/mp4" size="480"/>
           <source src="//path/to/video_720.m3u8" type="video/mp4" size="720"/>
           <source src="//path/to/video_1080.m3u8" type="video/mp4" size="1080"/>
    </video>
<script src="//path/to/plyr.js"></script>
<script src="//path/to/hls.min.js"></script>

JS

<script>
   document.addEventListener('DOMContentLoaded', () => {
    var video = document.querySelector('video');
    var default_src = jQuery('source[size=480]')[0];
    var player = new Plyr('#susu_player');

    var first = true; 
    player.on('play', () => {
        if(first) { 
            source = default_src.getAttribute('src');
            loadHLS(source); 
        }
        first = false;
    });

    var hls = []; var i = 0;
    function loadHLS(source) {
        hls[i] = new Hls(); 
        hls[i].loadSource(source);
        hls[i].attachMedia(video);
        video.play();
        i++;
    }

    // quality change
    player.on('qualitychange', () => {
        seek = player.currentTime;
        source = video.getAttribute('src');
        loadHLS(source);
    });
</script>

Tested on Chrome & Safari.

@phuongncn Thanks for spending time on this issue but , in most cases with hls format , the quality is parsed via the manifest file and not passed in as source which makes it dynamic. This way we need to get hold of the streams which may or may not be possible

        ```
hls.on(Hls.Events.MANIFEST_PARSED,function(event,data) {

            console.log('levels', hls.levels); // this is the quality level  defined
            playerOptions.quality = {
                default: hls.levels[hls.levels.length - 1].height,
                options: hls.levels.map((level) => level.height),
                forced: true,
         }

```

It's nice to see the participation on this issue. Maybe someone could put together a full good working code here so it's not spread out over several replies. @phuongncn @rahulrsingh09 @rahulrsingh09
Thanks

Using VOD HLS with Quality Switch works flawless, but I'm trying to make them jump to a specific time of the video, but it's not working. @phuongncn I'm using your example, how would you make it jump to a specific time ? Thanks in Advance

All of this does not reliably work with mpeg dash

Why isn't https://github.com/sampotts/plyr/pull/1607 getting merged?

Why isn't #1607 getting merged?

Read through the comments on it and you'll see what happened. I ended up having to manually merge the changes. https://github.com/sampotts/plyr/commit/6ffaef35cf667d0e2e14227882a1a8e329b2c2c2

Hey everyone, here is my working example of combining with Hls.js and plyr. The main idea is to configure option properly based on recent PR by @sampotts .

TLDR: working example https://codepen.io/datlife/pen/dyGoEXo

Main Idea

The goal is to set qualitiy options based on MANIFEST data loaded by HLS.

Notes before jumping into code

  • By streaming video using HLS protocol, we may just need to add single source tag in HTML (not multiple mp4 sources in the Plyr example). The reason is all available qualities is in the MANIFEST of playlist.m3u8. Therefore, we'd like the HLS performs the switching, not Plyr.

For example, our HTML video will be something like:

<video controls crossorigin playsinline >
  <source
      type="application/x-mpegURL" 
      <!-- playlist contains all available qualities for this stream --->
      src="https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8">
</video>
  • The manifest's playlist contains all available qualities, subtitles. Notice it has different RESOLUTIONS for the same video.
#EXTM3U
...

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=258157,CODECS="avc1.4d400d,mp4a.40.2",AUDIO="stereo",RESOLUTION=422x180,SUBTITLES="subs"
video/250kbit.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=520929,CODECS="avc1.4d4015,mp4a.40.2",AUDIO="stereo",RESOLUTION=638x272,SUBTITLES="subs"
video/500kbit.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=831270,CODECS="avc1.4d4015,mp4a.40.2",AUDIO="stereo",RESOLUTION=638x272,SUBTITLES="subs"
video/800kbit.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1144430,CODECS="avc1.4d401f,mp4a.40.2",AUDIO="surround",RESOLUTION=958x408,SUBTITLES="subs"
video/1100kbit.m3u8
....

#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="de",URI="subtitles_de.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",URI="subtitles_en.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Espanol",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",URI="subtitles_es.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",URI="subtitles_fr.m3u8"

Implementation

document.addEventListener("DOMContentLoaded", () => {
  const video = document.querySelector("video");
  const source = video.getElementsByTagName("source")[0].src;

  // For more options see: https://github.com/sampotts/plyr/#options
  const defaultOptions = {};

  if (Hls.isSupported()) {
    // For more Hls.js options, see https://github.com/dailymotion/hls.js
    const hls = new Hls();
    hls.loadSource(source);

    // From the m3u8 playlist, hls parses the manifest and returns
    // all available video qualities. This is important, in this approach,
    // we will have one source on the Plyr player.
    hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {

      // Transform available levels into an array of integers (height values).
      const availableQualities = hls.levels.map((l) => l.height)

      // Add new qualities to option
      defaultOptions.quality = {
        default: availableQualities[0],
        options: availableQualities,
        // this ensures Plyr to use Hls to update quality level
        // Ref: https://github.com/sampotts/plyr/blob/master/src/js/html5.js#L77
        forced: true,        
        onChange: (e) => updateQuality(e),
      }

      // Initialize new Plyr player with quality options
      const player = new Plyr(video, defaultOptions);
    });
    hls.attachMedia(video);
    window.hls = hls;
  } else {
    // default options with no quality update in case Hls is not supported
    const player = new Plyr(video, defaultOptions);
  }

  function updateQuality(newQuality) {
    window.hls.levels.forEach((level, levelIndex) => {
      if (level.height === newQuality) {
        console.log("Found quality match with " + newQuality);
        window.hls.currentLevel = levelIndex;
      }
    });
  }
});

@datlife nice solution, the only problem I had with a similar implementation was the error in the console, when plyr tries to load the blob that hls sets as the video src.

GET blob:http://localhost:4200/d16a5b78-1f6b-4f90-af7f-149e92a820be net::ERR_FILE_NOT_FOUND

You found a way around that?

Anyone figured out how to set an 'auto' option in the quality options?
My new idea is adding all qualities that could be available and set them all to display: none and then dynamically set display: flex for all the available qualities after the manifest has been parsed.
Only problem atm is, that I can not set a string value as default or choose a string value from the list.

I'm also still not able to get @datlife solution to work. It doesn't even give me quality button. @sampotts is there no hope of getting an actual universal feature implemented into the player itself instead of all the different custom solutions?

Well although there are nice solutions from different people there's no one standard correctly and a place to put actual plugins. There seems to be a PR for plugin support but seems PR's are behind. For now I'm going with VideoJs because it has everything I need including several HLS switcher plugins that work and the original reason for me switching to Plyr is fixed. Plyr is a good player but may try again when it's more mature.

You can check how to display multi-resolution mpeg dash source on the following link https://codepen.io/adis0308/pen/bGpQmwr

Anyone figured out how to set an 'auto' option in the quality options?
My new idea is adding all qualities that could be available and set them all to display: none and then dynamically set display: flex for all the available qualities after the manifest has been parsed.
Only problem atm is, that I can not set a string value as default or choose a string value from the list.

1) add custom 'Auto' label to getLabel function

getLabel: function (e, t) {
...
case "quality":
//inside if add below
if (t === 0) return "Auto";

2) add listener Hls.Events.LEVEL_SWITCHED

if (Hls.isSupported()) {
      var hls = new Hls(config)
      hls.loadSource(source);
      hls.attachMedia(video);
      window.hls = hls;

      hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
        const availableQualities = hls.levels.map((l) => l.height)
        availableQualities.unshift(0) //prepend 0 to quality array
        defaultOptions.quality = {
          default: 0, //Default - AUTO
          options: availableQualities,
          forced: true,        
          onChange: (e) => updateQuality(e),
        }
        hls.on(Hls.Events.LEVEL_SWITCHED, function (event, data) {
          var span = document.querySelector(".plyr__menu__container [data-plyr='quality'][value='0'] span")
          if (hls.autoLevelEnabled) {
            span.innerHTML = `AUTO (${hls.levels[data.level].height}p)`
          } else {
            span.innerHTML = `AUTO`
          }
        })
        var player = new Plyr(video, defaultOptions);
      })
    }
    function updateQuality(newQuality) {
      if (newQuality === 0) {
        window.hls.currentLevel = -1; //Enable AUTO quality if option.value = 0
      } else {
        window.hls.levels.forEach((level, levelIndex) => {
          if (level.height === newQuality) {
            console.log("Found quality match with " + newQuality);
            window.hls.currentLevel = levelIndex;
          }
        });
      }
    }

@Dirard, Sorry if my question seems so obvious! Where is the getLabel function? Can you please provide the complete code?

@Dirard, Sorry if my question seems so obvious! Where is the getLabel function? Can you please provide the complete code?

https://github.com/sampotts/plyr/blob/30989e4dbc6acb9d1caf4a83af4d6cd12d2548db/dist/plyr.mjs#L2412

Hi @datlife, thanks a lot for the amazing solution. If it won't take a lot of your time, could you please share a way of adding such a support for multiple videos on a page? The code you provided works for one video flawlessly, except for the not-so-important 404 error as @Benny739 mentioned. However, as soon as I modify it to support multiple videos on a single page, things stop working. Plyr just doesn't load the video at all. I can only see the video thumbnail and no trace of Plyr actually loading the video (that is, no class names change, no controls rendered, nothing).

Here's how I have set it up:

var video = document.querySelectorAll('video');
for (var i = 0; i < video.length; i++)
  {
    var source = video[i].getElementsByTagName('source')[0].src;
    var plyrOptions =
      {
        previewThumbnails:
          {
            enabled: true,
            src: video[i].getAttribute('thumbnails')
          }
      };
    if (Hls.isSupported())
      {
        var hls = new Hls();
        hls.loadSource(source);
        hls.on(Hls.Events.MANIFEST_PARSED, function()
          {
            var availableQualities = hls.levels.map((l) => l.height)
            plyrOptions.quality =
              {
                forced: true,
                options: availableQualities,
                default: availableQualities[0],
                onChange: (e) => updateQuality(e),
              }
            var player = Plyr.setup('video[i]', plyrOptions); // changing it to  Plyr.setup(video[i], plyrOptions); makes no difference
          });
        hls.attachMedia(video[i]);
        window.hls = hls;
      }
    else
      {
        var player = Plyr.setup('video[i]', plyrOptions);
      }

Adding Plyr before the hls.on... loads the video, however, only the first one. I am completely lost even after hours of trying various stuff.

Any help would be greatly appreciated. I was using Plyr on my previous website, but, moved to Video JS 2 days back for this new website, for the sole reason that it has support for HLS quality (using plugins). But the styling and any kind of modification and the overall look and feel brought me back to Plyr.

EDIT: Done! Fixed it! Now I use it like this: var player = Plyr.setup('.vid', plyrOptions);. Finally!

Okay, I was wrong. It's still not working as expected. The video resolution would always be stuck to the same one even if we choose another one from menu. Kindly help.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Zsavajji picture Zsavajji  Â·  3Comments

Skwai picture Skwai  Â·  4Comments

thang-nm picture thang-nm  Â·  4Comments

Antonio-Laguna picture Antonio-Laguna  Â·  3Comments

onigetoc picture onigetoc  Â·  3Comments