sampotts / plyr

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

How to add change quality video Using hls.js and plyr #652

Open osamay opened 7 years ago

osamay commented 7 years ago

How to add change quality video Using hls.js and plyr

`

<script src="https://cdn.plyr.io/2.0.3/plyr.js"></script>
<script src="https://cdn.jsdelivr.net/hls.js/latest/hls.js"></script>

`
friday commented 6 years ago

You can't currently. I suggest we add support for this via events so developers can trigger events to update the quality options and catch user changes to it.

vuongtaquoc commented 6 years ago

Are you complete this feature?

sampotts commented 5 years ago

@friday was looking at it but has reduced his involvement in the project. If anyone has time to look at it, it'd be greatly appreciated.

friday commented 5 years ago

I haven't touched this for a while, but from memory I think what you need to add on the plyr side is mostly a method (event listener) that adds "qualities" to the current controls, and a method to trigger events (though it's possible to trigger them externally on the html element). There's already an event for quality changes, that you can listen to and override with hls.js code.

In the end I felt blocked by other problems, but it's possible to add this functionality without solving them.

This is what I wrote in the slack #development-channel about this issue:

I started looking at the quality selector. We don't need to do much to support streaming libs. I actually have it working, but not working well, locally for hls.js.

Some of the problems has to do with the logic and assumptions for the labels and badges.

  1. Like captions, it's possible that a video source don't represent a "quality", but rather an option, like hardcoded captions, audio description and such (like philipgiuliani already stated and worked to improve). Hls and dash also has support for audio tracks, which might cover some of the. Mp4 probably does too, but native HTML5 video has no way to switch them afaik.
  2. For streaming it's also likely that there are duplicate resolutions, with different bitrates.
  3. In addition, there could be duplicate resolutions with different frame rates.
  4. "720p" or "1080p" is often used for wider formats, as if they were padded to 16:9. I think this is unofficial, but useful.
  5. "2K", "4K", "5K" and "8K" are mostly correlating with the width (not height), where 2K is near 2000px wide and 8K is near 8000px wide regardless of the aspect ratio (https://en.wikipedia.org/wiki/8K_resolution#Resolutions). It's sort of a marketing thing and doesn't fully make sense.

I think the most reasonable thing to do is to avoid handling this at all. Instead of the size property (non-standard), we could use label, and badge directly. Or data-label and data-badge. It leaves more control to users and less for us to handle for HTML5. Providers (like YouTube) have their own API's and video formats, so we can't "make" them provide us with labels and badges, but since YouTube doesn't actually support switching the quality any more, and Vimeo never did, perhaps this is a problem that can be addressed later? If we make them plugins as planned we could handle it in that level. For streaming, I have implemented an event that can be triggered, and Plyr will handle it and get the options. It's not something I'm ready to push yet (needs more discussion), but providing both labels and optional badges via this logic is not a problem.

The changes wouldn't be possible with the current quality getter/setter API (ex: player.quality = 720). > Unlike player.language I don't think player.quality is as useful of an abstraction and I wouldn't miss it if replaced with player.level = 1 for example. This would be breaking compatibility of course, just like changing the size attribute for something else.

Alternatively, we could keep the current API and not break anything by using size and the aspect ratio (the ratio setting or player.media.videoHeight / player.media.videoWidth, or the equivalent for the container element) to get the width, or add an attribute for width. In addition adding an optional attribute label for bitrate, fps and such if users want to be have control of the label.

The sample HLS stream I've been using on Codepen for HLS has these levels: 422x180 (258k), 638x272 (520k), 638x272 (831k), 958x408 (1144k), 1277x554 (1558k), 1921x818 (4149k), 1921x818 (6214k), 4096x1744 (10285k) When I round the width and use the method in 4. they turn into the more familiar 360p, 540p, 720p, 1080p etc (point being it's useful to have the control to do this outside of Plyr).

Akash187 commented 1 year ago

Below is my React Component for Hls with a quality selector if anyone needs it.

import { useEffect, useRef, useState } from 'react'
import Hls from 'hls.js'
import Plyr, { Options } from 'plyr'
import 'plyr/dist/plyr.css'

type IProps = {
    selectorId: string
    videoSource: string
    hlsSource?: string
    poster?: string
    autoPlay?: boolean
}

const plyrOptions: Plyr.Options = { ratio: '16:9', autopause: true }

const VideoPlayer = ({
    selectorId,
    videoSource,
    hlsSource,
    poster,
    autoPlay = false
}: IProps) => {
    const video = useRef<HTMLMediaElement | null>(null)
    const [supported] = useState(Hls.isSupported())
    const hls = useRef<Hls | null>(null)
    const player = useRef<Plyr | null>(null)

    useEffect(() => {
        console.log('initialized')
        video.current = document.getElementById(selectorId) as HTMLMediaElement

        if (poster) {
            //@ts-ignore
            video.current.poster = poster
        }

        if (!supported || !hlsSource) {
            video.current.src = videoSource
            player.current = new Plyr(video.current, plyrOptions)

            if (autoPlay) {
                player.current.play()
            }

            return () => {
                player.current?.destroy(() => console.log('player destroyed'))
            }
        }

        hls.current = new Hls({ maxMaxBufferLength: 10, autoStartLoad: false })
        hls.current.loadSource(hlsSource)
        hls.current.attachMedia(video.current! as HTMLMediaElement)

        hls.current.on(Hls.Events.MANIFEST_PARSED, () => {
            console.log('Mainfest parsed')

            const levels = hls?.current?.levels!
            const quality: Options['quality'] = {
                default: levels[levels.length - 1].height,
                options: levels.map((level) => level.height),
                forced: true,
                onChange: (newQuality: number) => {
                    console.log('changes', newQuality)
                    levels.forEach((level, levelIndex) => {
                        if (level.height === newQuality) {
                            hls.current!.currentLevel = levelIndex
                        }
                    })
                }
            }

            player.current = new Plyr(video.current!, {
                ...plyrOptions,
                quality
            })

            player?.current?.on('play', () => {
                console.log('playing')
                hls?.current?.startLoad()
            })

            player?.current?.on('pause', () => {
                console.log('paused')
                hls?.current?.stopLoad()
            })

            if (autoPlay) {
                player.current.play()
            }
        })

        return () => {
            console.log('destroyed')
            hls.current?.destroy()
            player.current?.destroy()
        }
    }, [])

    return (
        <div
            style={{
                width: 'min(1080px, 100%)'
            }}
        >
            <video id={selectorId} />
        </div>
    )
}

export default VideoPlayer