sampotts / plyr

A simple HTML5, YouTube and Vimeo player
https://plyr.io
MIT License
26.32k stars 2.92k forks source link

No option for quality control with HLS #1919

Open LukeMadhanga opened 4 years ago

LukeMadhanga commented 4 years ago

Expected behaviour

I am putting together a prototype video player. I added all of the required settings to allow for the quality selector to show, but it is not.

Actual behaviour

The settings button only has a button for the speed selector, but not the quality selector.

Environment


Notes

The manifest files were generated with Google's Shaka Packager, so I don't think they will be the problem. I've switched between preferring HLS and MPD, but either way, the settings do not show. I'm testing on my local environment with a test URL of http://apg.co (stating because I don't know whether there are access controls that may restrict quality switching).


HTML

<link rel="stylesheet" href="https://cdn.plyr.io/3.6.2/plyr.css" />

<video id="video" width="640" class="plyr-player" playsinline controls preload="auto">
    <source src="http://media.apg.co/1/h264_master.m3u8"/>
    <source src="http://media.apg.co/1/h264.mpd"/>
</video>

<script src="http://cdn.dashjs.org/latest/dash.all.min.js"></script>
<script src="https://cdn.plyr.io/3.6.2/plyr.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/0.14.7/hls.min.js"></script>

JS

(function () {

"use strict";

let initPlayer = selector => {
    let options = {};
    let hls;
    let supportsDash = typeof (window.MediaSource || window.WebKitMediaSource) === 'function';
    let supportsHls = window.Hls.isSupported();
    let players = document.querySelectorAll(selector);
    let adaptiveSources = [];
    let nonAdaptiveSources = [];
    let playerOptions = {
        debug: false,
        controls: [
            'play-large', // The large play button in the center
            'restart', // Restart playback
            'rewind', // Rewind by the seek time (default 10 seconds)
            'play', // Play/pause playback
            'fast-forward', // Fast forward by the seek time (default 10 seconds)
            'progress', // The progress bar and scrubber for playback and buffering
            'current-time', // The current time of playback
            'duration', // The full duration of the media
            'mute', // Toggle mute
            'volume', // Volume control
            'captions', // Toggle captions
            'settings', // Settings menu
            'pip', // Picture-in-picture (currently Safari only)
            'airplay', // Airplay (currently Safari only)
            'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
            'fullscreen', // Toggle fullscreen
        ],
        settings: ['captions', 'quality', 'speed', 'loop']
    };

    players.forEach(player => {
        let source;
        let j;
        let sources = $('source', player);
        let autoplay = options.autoplay || player.getAttribute('autoplay');

        // playerOptions.autoplay = autoplay;

        if (sources.length < 1) {
            return console.error('No sources found in player')
        }

        sources.each((_, source) => {
            let type;
            let src = source.getAttribute('src') || '';

            if (src.toLowerCase().endsWith('.m3u8')) {
                type = 'hls'
            } else if (src.toLowerCase().endsWith('.mpd')) {
                type = 'dash'
            }

            if (type === 'hls') {
                if (supportsHls && (!options.force || options.force === 'hls')) {
                    // https://codepen.io/sampotts/pen/JKEMqB
                    hls = new window.Hls();
                    hls.loadSource(src);
                    hls.attachMedia(player);
                    hls.on(window.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
                        playerOptions.quality = {
                            default: Math.max(...availableQualities),
                            options: availableQualities,
                            forced: true,
                            onChange: (e) => function (e) {},
                        }

                        if (autoplay) {
                            player.play()
                        }
                    })

                    adaptiveSources.push({type, source});
                    console.log(playerOptions);
                }
            } else if (type === 'dash') {
                if (supportsDash && (!options.force || options.force === 'dash')) {
                    // https://codepen.io/sampotts/pen/BzpJXN
                    // var dash = window.dashjs.MediaPlayer().create();
                    // dash.initialize(player, src, autoplay);
                    // adaptiveSources.push({type, source});
                }
            }
        });

        player.innerHTML = ''

        for (let x in adaptiveSources) {
            player.appendChild(adaptiveSources[x].source);
            break
        }

        new Plyr(player, playerOptions);
    });
}

$(function () {
    initPlayer('.plyr-player');
});

}());

MPD

<?xml version="1.0" encoding="UTF-8"?>
<!--Generated with https://github.com/google/shaka-packager version v2.4.2-c60e988-release-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT331.5S">
  <Period id="0">
    <AdaptationSet id="0" contentType="video" maxWidth="1280" maxHeight="720" frameRate="12288/512" par="16:9">
      <Representation id="0" bandwidth="323612" codecs="avc1.64000c" mimeType="video/mp4" sar="1:1" width="256" height="144">
        <BaseURL>h264_144p.mp4</BaseURL>
        <SegmentBase indexRange="866-1329" timescale="12288">
          <Initialization range="0-865"/>
        </SegmentBase>
      </Representation>
      <Representation id="1" bandwidth="454846" codecs="avc1.640015" mimeType="video/mp4" sar="1:1" width="426" height="240">
        <BaseURL>h264_240p.mp4</BaseURL>
        <SegmentBase indexRange="867-1330" timescale="12288">
          <Initialization range="0-866"/>
        </SegmentBase>
      </Representation>
      <Representation id="2" bandwidth="705164" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1" width="640" height="360">
        <BaseURL>h264_360p.mp4</BaseURL>
        <SegmentBase indexRange="867-1330" timescale="12288">
          <Initialization range="0-866"/>
        </SegmentBase>
      </Representation>
      <Representation id="3" bandwidth="997215" codecs="avc1.64001f" mimeType="video/mp4" sar="1:1" width="854" height="480">
        <BaseURL>h264_480p.mp4</BaseURL>
        <SegmentBase indexRange="867-1330" timescale="12288">
          <Initialization range="0-866"/>
        </SegmentBase>
      </Representation>
      <Representation id="4" bandwidth="2902577" codecs="avc1.640028" mimeType="video/mp4" sar="1:1" width="1280" height="720">
        <BaseURL>h264_720p.mp4</BaseURL>
        <SegmentBase indexRange="867-1330" timescale="12288">
          <Initialization range="0-866"/>
        </SegmentBase>
      </Representation>
    </AdaptationSet>
    <AdaptationSet id="1" contentType="audio" lang="en" subsegmentAlignment="true">
      <Representation id="5" bandwidth="134798" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="48000">
        <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
        <BaseURL>audio.mp4</BaseURL>
        <SegmentBase indexRange="822-1525" timescale="48000">
          <Initialization range="0-821"/>
        </SegmentBase>
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>

HLS

#EXTM3U
## Generated with https://github.com/google/shaka-packager version v2.4.2-c60e988-release

#EXT-X-MEDIA:TYPE=AUDIO,URI="audio.m3u8",GROUP-ID="audio",LANGUAGE="en",NAME="stream_4",AUTOSELECT=YES,CHANNELS="2"

#EXT-X-STREAM-INF:BANDWIDTH=458410,AVERAGE-BANDWIDTH=333227,CODECS="avc1.64000c,mp4a.40.2",RESOLUTION=256x144,FRAME-RATE=24.000,AUDIO="audio"
h264_144p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=589644,AVERAGE-BANDWIDTH=433138,CODECS="avc1.640015,mp4a.40.2",RESOLUTION=426x240,FRAME-RATE=24.000,AUDIO="audio"
h264_240p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=839962,AVERAGE-BANDWIDTH=533184,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=24.000,AUDIO="audio"
h264_360p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1132013,AVERAGE-BANDWIDTH=633401,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=854x480,FRAME-RATE=24.000,AUDIO="audio"
h264_480p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3037375,AVERAGE-BANDWIDTH=1633773,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=24.000,AUDIO="audio"
h264_720p.m3u8

#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=23968,AVERAGE-BANDWIDTH=14847,CODECS="avc1.64000c",RESOLUTION=256x144,FRAME-RATE=24.000,URI="h264_144p_iframe.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=45913,AVERAGE-BANDWIDTH=25810,CODECS="avc1.640015",RESOLUTION=426x240,FRAME-RATE=24.000,URI="h264_240p_iframe.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=71858,AVERAGE-BANDWIDTH=36407,CODECS="avc1.64001e",RESOLUTION=640x360,FRAME-RATE=24.000,URI="h264_360p_iframe.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=101086,AVERAGE-BANDWIDTH=47309,CODECS="avc1.64001f",RESOLUTION=854x480,FRAME-RATE=24.000,URI="h264_480p_iframe.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=221010,AVERAGE-BANDWIDTH=99597,CODECS="avc1.640028",RESOLUTION=1280x720,FRAME-RATE=24.000,URI="h264_720p_iframe.m3u8"
sampotts commented 4 years ago

This issue comes up a lot, so I know it's important but can you use the current issue for this, please?

LukeMadhanga commented 4 years ago

Okay, do you have a preferred issue that I can add to, or any of the existing ones?

bekee commented 3 years ago

There has been no solution to this issue. It's really a problem

ShowDon commented 3 years ago

Hello! I think the 'no option for quality control with HLS' issue appears if you make a instance of Plyr. I think to prevent this just remove the id from video tag or you can work around.

If anyone got the solution other than mine, please let me know. Thanks.

denilly commented 3 years ago

In "Issues" I found some collections that together delivered the solution for manual and automatic quality control. Here is the source code:

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset="utf-8">
  <title>Plyr+HLS</title>
  <link rel="stylesheet" href="./src/plyr.css" />

  <style>
     body {
        max-width: 640px;
        margin-left: auto;
        margin-right: auto;
     }
  </style>

</head>
<body>
  <div class="container">
    <video controls crossorigin playsinline >
      <source 
        src="https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8"
      >
    </video>
  </div>
  <script src="./src/plyr.js"></script>
  <script src="./src/hls.js"></script> 
  <script>
  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
    var hls = new Hls();
    hls.loadSource(source);
    hls.attachMedia(video);
    window.hls = hls;

    // 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),
      }
      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);
      player.on('languagechange', () => {
            // Caption support is still flaky. See: https://github.com/sampotts/plyr/issues/994
            setTimeout(() => hls.subtitleTrack = player.currentTrack, 50);
        });
    });
  } else {
    // default options with no quality update in case Hls is not supported
    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;
          }
        });
      }
    }
});
  </script>
    </body>
</html>