plyr icon indicating copy to clipboard operation
plyr copied to clipboard

Quality switcher for HLS

Open tolew1 opened this issue 4 years ago • 32 comments

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

tolew1 avatar Mar 24 '20 10:03 tolew1

Upvoting, this is really a much need feature

lofcz avatar Mar 25 '20 06:03 lofcz

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

Matho avatar Mar 31 '20 08:03 Matho

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:

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;
}

tca3 avatar Apr 02 '20 09:04 tca3

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

Matho avatar Apr 02 '20 10:04 Matho

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

soheil avatar Apr 03 '20 17:04 soheil

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

// Here is the data format
var data = [
    {
        src: 'https://bitmovin-a.akamaihd.net/content/sintel/sintel.mpd',
        size: 720,
        mode: 'mpd',// How to analyze
    },
    {
        // Resource address
        src: 'https://vod02.cdn.web.tv/am/8c/am8cvvsejym_,240,360,480,720,1080,.mp4.urlset/master.m3u8',
        size: 1080,// Definition
        mode: 'hls'// How to analyze
    },
];

player.source = {
    // type: 'audio',
    type: 'video',
    title: '新播放',
    sources: data,
};

player.on('qualitychange', event => {
    $.each(data, function () {
        initData();
    })
});

function initData () {
    const video = document.querySelector('video');
    $.each(data, function () {
        // hls Adaptation
        if (this.mode === 'hls' && this.size === player.config.quality.selected) {
            // For more options see: https://github.com/sampotts/plyr/#options
            // captions.update is required for captions to work with hls.js
            if (!Hls.isSupported()) {
                video.src = this.src;
            } else {
                const hls = new Hls();
                hls.loadSource(this.src);
                hls.attachMedia(video);
                window.hls = hls;
                // Handle changing captions
                player.on('languagechange', () => {
                    // Caption support is still flaky. See: https://github.com/sampotts/plyr/issues/994
                    setTimeout(() => hls.subtitleTrack = player.currentTrack, 50);
                });
            }

            // Expose player so it can be used from the console
            window.player = player;
            return false;
        }
        // dash Adaptation
        if (this.mode === 'mpd' && this.size === player.config.quality.selected) {
            // For more dash options, see https://github.com/Dash-Industry-Forum/dash.js
            const dash = dashjs.MediaPlayer().create();
            dash.initialize(video, this.src, true);
            // Expose player and dash so they can be used from the console
            window.player = player;
            window.dash = dash;
        }
    })
}
initData();

Then you can run your code to see how it works. If you have any questions, please consult [email protected]

okxaas avatar Apr 04 '20 02:04 okxaas

but a single .m3u8 can have support for multiple qualities within it

soheil avatar Apr 04 '20 02:04 soheil

Please implement this feature.

pratheekhegde avatar Apr 09 '20 14:04 pratheekhegde

Anyone figured out how to workaround the file not found error message, when initialising the player after hls?

GET blob:http://1.localhost:4300/b12d9d80-92a3-47b6-95e8-d29d1e8c98f1 net::ERR_FILE_NOT_FOUND

Benny739 avatar Apr 10 '20 09:04 Benny739

I tried using what @ThomasCantonnet has suggested using plain JS but it seems the hls levels in not propagated to the playerOptions can @IT-Pony and @sampotts shed some light on this.

My Implementation looking at Thomas's snippet in TS

<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8">
      <title>HLS Demo</title>
      <link rel="stylesheet" href="https://cdn.plyr.io/3.5.10/plyr.css" />
      <style>
         body {
         max-width: 1024px;
         }
      </style>
   </head>
   <body>
      <video preload="none" id="player" autoplay controls crossorigin></video>
      <script src="https://cdn.plyr.io/3.5.10/plyr.js"></script>
      <script src="https://cdn.jsdelivr.net/hls.js/latest/hls.js"></script>
      <script>
         (function () {
         	var video = document.querySelector('#player');
         	var playerOptions= {
         		quality: {
         			default: '720',
         			options: ['720']
         		}
         	};
         	var player;
         	 player = new Plyr(video,playerOptions);
         	if (Hls.isSupported()) {
         		var hls = new Hls();
         		hls.loadSource('https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8');
         				//hls.loadSource('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
         	hls.attachMedia(video);
         	hls.on(Hls.Events.MANIFEST_PARSED,function(event,data) {

         		console.log('levels', hls.levels);
         		playerOptions.quality = {
         			default: hls.levels[hls.levels.length - 1].height,
         			options: hls.levels.map((level) => level.height),
         			forced: true,
             // Manage quality changes
             onChange: (quality) => {
             	console.log('changes',quality);
             	hls.levels.forEach((level, levelIndex) => {
             		if (level.height === quality) {
             			hls.currentLevel = levelIndex;
             		}
             	});
             }
         };
        });
       }
         
          // Start HLS load on play event
          player.on('play', () => hls.startLoad());
         
          // Handle HLS quality changes
          player.on('qualitychange', () => {
          	console.log('changed');
          	if (player.currentTime !== 0) {
          		hls.startLoad();
          	}
          });
         })();
         
      </script>
   </body>
</html>

rahulrsingh09 avatar Apr 19 '20 07:04 rahulrsingh09

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 avatar Apr 20 '20 13:04 phuongncn

@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,
         }

rahulrsingh09 avatar Apr 20 '20 15:04 rahulrsingh09

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

tolew1 avatar Apr 24 '20 18:04 tolew1

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

JohnTrabusca avatar Apr 30 '20 16:04 JohnTrabusca

All of this does not reliably work with mpeg dash

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

peterfranzo avatar May 10 '20 15:05 peterfranzo

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

sampotts avatar May 10 '20 23:05 sampotts

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 are 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 avatar Jun 07 '20 23:06 datlife

@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?

Benny739 avatar Jun 17 '20 23:06 Benny739

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.

Benny739 avatar Jun 21 '20 20:06 Benny739

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?

tolew1 avatar Jul 10 '20 03:07 tolew1

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.

tolew1 avatar Jul 22 '20 13:07 tolew1

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

adis0308 avatar Sep 21 '20 21:09 adis0308

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";
  1. 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 avatar Oct 01 '20 13:10 Dirard

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

codehunter12345 avatar Oct 16 '20 15:10 codehunter12345

@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

Dirard avatar Oct 16 '20 16:10 Dirard

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!

hrishikesh-k avatar Nov 27 '20 20:11 hrishikesh-k

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.

hrishikesh-k avatar Nov 28 '20 05:11 hrishikesh-k

@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, This changes the text in the list of available qualities but when we go back of the main menu, it shows the text "0dp" when auto is selected. Everything else is working perfectly fine. ddhth

abhinandan-chakraborty avatar Feb 15 '21 09:02 abhinandan-chakraborty

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

You dont need to change getLabel function There is a better approach using i18n and passing qualityLabel in conifg object to it like this :

new Plyr(playerEl, {
            ...,
            i18n: {
              qualityLabel: {
                0: 'Auto',
              },
            },
          })

miladazhdehnia avatar May 09 '21 11:05 miladazhdehnia

Just for reference. This is a working example with many suggestion in this thread (getLabel stuff, quality switch). The @datlife plus.

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

	const defaultOptions = {};

	if (!Hls.isSupported()) {
		video.src = source;
		var player = new Plyr(video, defaultOptions);
	} else {
		// 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)
	      	availableQualities.unshift(0) //prepend 0 to quality array

	      	    // Add new qualities to option
		    defaultOptions.quality = {
		    	default: 0, //Default - AUTO
		        options: availableQualities,
		        forced: true,        
		        onChange: (e) => updateQuality(e),
		    }
		    // Add Auto Label 
		    defaultOptions.i18n = {
		    	qualityLabel: {
		    		0: 'Auto',
		    	},
		    }

		    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`
	          }
	        })
    
             // Initialize new Plyr player with quality options
		     var player = new Plyr(video, defaultOptions);
         });	

	hls.attachMedia(video);
    	window.hls = hls;		 
    }

    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;
          }
        });
      }
    }
});

borismacek avatar Jun 09 '21 19:06 borismacek

For anyone who using react, this is how I do it.

p/s: I took the code from plyr-react then I implemented quality selector. https://www.npmjs.com/package/plyr-react

import Hls from "hls.js";
import PlyrJS, { Options, PlyrEvent as PlyrJSEvent, SourceInfo } from "plyr";
import React, { HTMLProps, MutableRefObject, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import "plyr/dist/plyr.css";

export type PlyrInstance = PlyrJS;
export type PlyrEvent = PlyrJSEvent;
export type PlyrCallback = (this: PlyrJS, event: PlyrEvent) => void;

export type PlyrProps = HTMLProps<HTMLVideoElement> & {
  source?: SourceInfo;
  options?: Options;
};
export interface HTMLPlyrVideoElement {
  plyr?: PlyrInstance;
}

export const Plyr = React.forwardRef<HTMLPlyrVideoElement, PlyrProps>(
  (props, ref) => {
    const { options = null, source, ...rest } = props;
    const innerRef = useRef<HTMLPlyrVideoElement>();
    const hls = useRef(new Hls());

    const videoOptions: PlyrJS.Options = {
      ...options,
      quality: {
        default: 720,
        options: [720],
      },
    };

    const createPlayer = () => {
      const plyrPlayer = new PlyrJS(".plyr-react", videoOptions);

      if (innerRef.current?.plyr) {
        innerRef.current.plyr = plyrPlayer;
      }
    };

    hls.current.on(Hls.Events.MANIFEST_LOADED, () => {
      videoOptions.quality = {
        default: hls.current.levels[hls.current.levels.length - 1].height,
        options: hls.current.levels.map((level) => level.height),
        forced: true,
        // Manage quality changes
        onChange: (quality: number) => {
          hls.current.levels.forEach((level, levelIndex) => {
            if (level.height === quality) {
              hls.current.currentLevel = levelIndex;
            }
          });
        },
      };

      createPlayer();
    });

    useEffect(() => {
      if (!innerRef.current) return;

      if (Hls.isSupported()) {
        hls.current.loadSource(source?.sources[0].src!);
        hls.current.attachMedia(innerRef.current as HTMLMediaElement);
      } else {
        createPlayer();
      }

      if (typeof ref === "function") {
        if (innerRef.current) ref(innerRef.current);
      } else {
        if (ref && innerRef.current) ref.current = innerRef.current;
      }

      if (innerRef.current?.plyr && source) {
        innerRef.current.plyr.source = source;
      }

      innerRef.current.plyr?.on("play", () => hls.current.startLoad());

      innerRef.current.plyr?.on("qualitychange", () => {
        if (innerRef.current?.plyr?.currentTime !== 0) {
          hls.current.startLoad();
        }
      });
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [videoOptions]);

    return (
      <video
        ref={innerRef as unknown as MutableRefObject<HTMLVideoElement>}
        className="plyr-react plyr"
        {...rest}
      />
    );
  }
);

hoangvu12 avatar Aug 11 '21 09:08 hoangvu12

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 are 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;
      }
    });
  }
});

Hi, Could you implement the AUTO option to change the quality automatically? Thank you.

abelhoyos avatar Feb 08 '22 08:02 abelhoyos