Working on quality change

This commit is contained in:
Thomas Forgione 2021-06-11 11:39:12 +02:00
parent 8d10add379
commit e99a5ed1e3
4 changed files with 348 additions and 59 deletions

View File

@ -40,10 +40,12 @@
}
});
let hls;
app.ports.initVideo.subscribe(function(arg) {
const video = document.getElementById('video');
if (Hls.isSupported()) {
const hls = new Hls();
hls = new Hls();
hls.loadSource(arg);
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
@ -54,16 +56,10 @@
});
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 x = document.querySelectorAll(".plyr__menu__container [data-plyr='settings'] span")[3];
// if (x.innerHTML.startsWith("Auto") || x.innerHTML === "0p") {
// x.innerHTML = span.innerHTML;
// }
app.ports.nowHasQuality.send({
auto: hls.autoLevelEnabled,
height: hls.levels[data.level].height
});
})
hls.attachMedia(video);
@ -93,6 +89,30 @@
app.ports.exitFullscreen.subscribe(function() {
document.exitFullscreen();
});
app.ports.setPlaybackRate.subscribe(function(arg) {
const video = document.getElementById('video');
video.playbackRate = arg;
});
app.ports.setQuality.subscribe(function(arg) {
var old = hls.currentLevel;
if (arg.auto) {
hls.currentLevel = -1;
} else {
hls.levels.forEach((level, levelIndex) => {
if (level.height === arg.height) {
hls.currentLevel = levelIndex;
}
});
}
if (old === hls.currentLevel) {
app.ports.nowHasQuality.send({
auto: hls.autoLevelEnabled,
height: hls.currentLevel === -1 ? 0 : hls.levels[hls.currentLevel].height
});
}
});
</script>
</body>
</html>

View File

@ -1,10 +1,4 @@
module Icons exposing
( maximize
, minimize
, pause
, play
, playCircle
)
module Icons exposing (..)
import Element exposing (Element)
import Html exposing (Html)
@ -69,3 +63,18 @@ playCircle =
[ Svg.circle [ cx "12", cy "12", r "10" ] []
, Svg.polygon [ points "10 8 16 12 10 16 10 8" ] []
]
settings : Bool -> Element msg
settings =
svgFeatherIcon "settings"
[ Svg.circle [ cx "12", cy "12", r "3" ] []
, Svg.path [ d "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" ] []
]
check : Bool -> Element msg
check =
svgFeatherIcon "check"
[ Svg.polyline [ points "20 6 9 17 4 12" ] []
]

View File

@ -3,7 +3,7 @@ port module Main exposing (..)
import Browser
import Browser.Events
import DOM as Dom
import Element exposing (Element, alignRight, centerY, el, fill, padding, rgb255, row, spacing, text, width)
import Element exposing (Element)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
@ -13,6 +13,7 @@ import Html.Attributes
import Html.Events
import Icons
import Json.Decode as Decode
import Quality
import Simple.Animation as Animation exposing (Animation)
import Simple.Animation.Animated as Animated
import Simple.Animation.Property as P
@ -28,6 +29,15 @@ main =
\_ ->
Sub.batch
[ nowHasQualities NowHasQualities
, nowHasQuality
(\x ->
case Decode.decodeValue Quality.decode x of
Ok s ->
NowHasQuality s
_ ->
Noop
)
, Browser.Events.onAnimationFrameDelta AnimationFrameDelta
, Browser.Events.onResize (\x y -> NowHasWindowSize ( x, y ))
]
@ -45,18 +55,32 @@ type alias Model =
, volume : Float
, muted : Bool
, isFullscreen : Bool
, quality : Maybe Quality.Quality
, qualities : List Int
, showBar : Bool
, animationFrame : Float
, videoSize : ( Int, Int )
, screenSize : ( Int, Int )
, playbackRate : Float
, settings : Settings
, showSettings : Bool
}
type Settings
= All
| Speed
| Quality
type Msg
= Noop
| PlayPause
| Seek Float
| ToggleSettings
| SetSettings Settings
| SetPlaybackRate Float
| SetQuality Quality.Quality
| RequestFullscreen
| ExitFullscreen
| AnimationFrameDelta Float
@ -69,8 +93,10 @@ type Msg
| NowLoaded (List ( Float, Float ))
| NowIsFullscreen Bool
| NowHasQualities (List Int)
| NowHasQuality Quality.Quality
| NowHasVideoSize ( Int, Int )
| NowHasWindowSize ( Int, Int )
| NowHasPlaybackRate Float
init : Decode.Value -> ( Model, Cmd Msg )
@ -97,11 +123,15 @@ init flags =
1.0
False
False
Nothing
[]
True
0
( 0, 0 )
( width, height )
1.0
All
False
, initVideo url
)
@ -118,14 +148,30 @@ update msg model =
Seek ratio ->
( model, seek (ratio * model.duration) )
SetPlaybackRate rate ->
( { model | showSettings = False, settings = All }, setPlaybackRate rate )
ToggleSettings ->
( { model | showSettings = not model.showSettings }, Cmd.none )
SetSettings s ->
( { model | settings = s }, Cmd.none )
RequestFullscreen ->
( model, requestFullscreen () )
ExitFullscreen ->
( model, exitFullscreen () )
SetQuality q ->
( { model | showSettings = False, settings = All }, setQuality q )
AnimationFrameDelta delta ->
( { model | animationFrame = model.animationFrame + delta }, Cmd.none )
if model.animationFrame + delta > 3500 then
( { model | animationFrame = model.animationFrame + delta, showSettings = False, settings = All }, Cmd.none )
else
( { model | animationFrame = model.animationFrame + delta }, Cmd.none )
MouseMove ->
( { model | animationFrame = 0 }, Cmd.none )
@ -154,12 +200,18 @@ update msg model =
NowHasQualities qualities ->
( { model | qualities = qualities }, Cmd.none )
NowHasQuality quality ->
( { model | quality = Just quality }, Cmd.none )
NowHasVideoSize size ->
( { model | videoSize = size }, Cmd.none )
NowHasWindowSize size ->
( { model | screenSize = size }, Cmd.none )
NowHasPlaybackRate rate ->
( { model | playbackRate = rate }, Cmd.none )
view : Model -> Browser.Document Msg
view model =
@ -220,53 +272,58 @@ video model =
[ Element.width Element.fill, Element.height Element.fill ]
(Element.column
[ Element.width Element.fill
, Element.padding 10
, Element.alignBottom
, Font.color (Element.rgba 1 1 1 0.85)
, Background.gradient { angle = 0, steps = [ Element.rgba 0 0 0 0.75, Element.rgba 0 0 0 0 ] }
]
[ Element.row
[ settings model
, Element.column
[ Element.width Element.fill
, Element.height (Element.px 30)
, Border.rounded 5
, Element.behindContent
(Element.el
[ Background.color (Element.rgba 1 1 1 0.25)
, Element.width Element.fill
, Element.padding 10
, Background.gradient { angle = 0, steps = [ Element.rgba 0 0 0 0.75, Element.rgba 0 0 0 0 ] }
]
[ Element.row
[ Element.width Element.fill
, Element.height (Element.px 30)
, Border.rounded 5
, Element.behindContent
(Element.el
[ Background.color (Element.rgba 1 1 1 0.25)
, Element.width Element.fill
, Element.height (Element.px 5)
, Element.centerY
, Border.rounded 5
]
Element.none
)
, Element.behindContent loadedElement
, Element.inFront
(Element.el
(Element.width Element.fill
:: Element.height Element.fill
:: Element.pointer
:: seekBarEvents
)
Element.none
)
]
[ Element.el
[ Background.color (Element.rgba 1 0 0 0.75)
, Element.width (Element.fillPortion seen)
, Element.height Element.fill
, Border.roundEach { topLeft = 5, topRight = 0, bottomLeft = 5, bottomRight = 0 }
, Element.height (Element.px 5)
, Element.centerY
, Border.rounded 5
]
Element.none
)
, Element.behindContent loadedElement
, Element.inFront
(Element.el
(Element.width Element.fill
:: Element.height Element.fill
:: Element.pointer
:: seekBarEvents
)
Element.none
)
]
[ Element.el
[ Background.color (Element.rgba 1 0 0 0.75)
, Element.width (Element.fillPortion seen)
, Element.height Element.fill
, Border.roundEach { topLeft = 5, topRight = 0, bottomLeft = 5, bottomRight = 0 }
, Element.height (Element.px 5)
, Element.centerY
, Element.el [ Element.width (Element.fillPortion remaining) ] Element.none
]
, Element.row
[ Element.spacing 10, Element.width Element.fill ]
[ playPauseButton model.playing
, Element.el [ Element.moveDown 2.5 ] (Element.text (formatTime model.position ++ " / " ++ formatTime model.duration))
, Element.row [ Element.spacing 10, Element.alignRight ]
[ settingsButton, fullscreenButton model.isFullscreen ]
]
Element.none
, Element.el [ Element.width (Element.fillPortion remaining) ] Element.none
]
, Element.row
[ Element.spacing 10, Element.width Element.fill ]
[ playPauseButton model.playing
, Element.el [ Element.moveDown 2.5 ] (Element.text (formatTime model.position ++ " / " ++ formatTime model.duration))
, Element.row [ Element.spacing 10, Element.alignRight ]
[ fullscreenButton model.isFullscreen ]
]
]
)
@ -318,6 +375,134 @@ video model =
)
settings : Model -> Element Msg
settings model =
let
makeMenuButton : Settings -> Element Msg -> Element Msg -> Element Msg
makeMenuButton s key value =
Input.button [ Element.width Element.fill, Element.paddingXY 0 10 ]
{ label =
Element.row [ Element.width Element.fill, Element.spacing 20 ]
[ Element.el [ Font.bold, Element.alignLeft ] key
, Element.el [ Element.alignRight ] value
]
, onPress = Just (SetSettings s)
}
speedButton =
makeMenuButton Speed (Element.text "Speed") (Element.text ("x" ++ String.fromFloat model.playbackRate))
qualityButton =
case model.quality of
Just q ->
makeMenuButton Quality (Element.text "Quality") (Element.text (Quality.toString q))
_ ->
Element.none
returnButton =
Input.button
[ Element.width Element.fill
, Element.paddingXY 0 10
, Border.widthEach
{ bottom = 1
, top = 0
, left = 0
, right = 0
}
, Border.color (Element.rgba 0.5 0.5 0.5 0.75)
]
{ label = Element.text "Return"
, onPress = Just (SetSettings All)
}
speedOptions =
[ 0.5, 0.75, 1, 1.5, 2 ]
|> List.map
(\x ->
Input.button [ Element.width Element.fill, Element.paddingXY 0 10 ]
{ label =
Element.row [ Element.width Element.fill ]
[ if x == model.playbackRate then
Icons.check False
else
Element.el [ Font.color (Element.rgba 0 0 0 0) ] (Icons.check False)
, Element.el
[ Element.paddingEach
{ left = 10
, right = 0
, top = 0
, bottom = 0
}
]
(Element.text ("x" ++ String.fromFloat x))
]
, onPress = Just (SetPlaybackRate x)
}
)
|> (\x -> returnButton :: x)
qualityOptions =
model.qualities
|> List.map
(\x ->
Input.button [ Element.width Element.fill, Element.paddingXY 0 10 ]
{ label =
Element.row [ Element.width Element.fill ]
[ if Quality.isSameOption (Just { auto = False, height = x }) model.quality then
Icons.check False
else
Element.el [ Font.color (Element.rgba 0 0 0 0) ] (Icons.check False)
, Element.el
[ Element.paddingEach
{ left = 10
, right = 0
, top = 0
, bottom = 0
}
]
(Element.text (Quality.toString { auto = False, height = x }))
]
, onPress = Just (SetQuality { auto = x == 0, height = x })
}
)
|> (\x -> returnButton :: x)
buttons =
case model.settings of
All ->
[ speedButton, qualityButton ]
Speed ->
speedOptions
Quality ->
qualityOptions
in
animatedEl
(if model.showSettings then
fadeIn
else
fadeOut
)
[ Element.padding 10
, Element.width Element.fill
, Element.height Element.fill
, Element.moveDown 20
]
(Element.column
[ Background.color (Element.rgba 0.2 0.2 0.2 0.75)
, Element.alignRight
, Element.paddingXY 20 10
, Border.rounded 10
]
buttons
)
playPauseButton : Bool -> Element Msg
playPauseButton playing =
let
@ -349,6 +534,14 @@ fullscreenButton isFullscreen =
)
settingsButton : Element Msg
settingsButton =
Input.button []
{ label = Icons.settings False
, onPress = Just ToggleSettings
}
playerEvents : List (Element.Attribute Msg)
playerEvents =
List.map Element.htmlAttribute
@ -367,6 +560,7 @@ videoEvents =
, Html.Events.on "volumechange" decodeVolumeChange
, Html.Events.on "progress" decodeProgress
, Html.Events.on "resize" decodeVideoResize
, Html.Events.on "ratechange" decodePlaybackRateChange
]
@ -422,7 +616,7 @@ decodeTimeRanges =
decodeTimeRange : Decode.Decoder ( Float, Float )
decodeTimeRange =
Decode.map2 (\x y -> ( x, y ))
Decode.map2 Tuple.pair
(Decode.field "start" Decode.float)
(Decode.field "end" Decode.float)
@ -445,6 +639,13 @@ decodeVideoResize =
(Decode.field "videoHeight" Decode.int)
decodePlaybackRateChange : Decode.Decoder Msg
decodePlaybackRateChange =
Dom.target <|
Decode.map NowHasPlaybackRate
(Decode.field "playbackRate" Decode.float)
every : Float -> List ( Float, Float ) -> List ( Float, Float, Bool )
every duration input =
everyAux duration 0.0 [] input |> List.reverse |> List.filter (\( x, y, _ ) -> x /= y)
@ -517,9 +718,18 @@ port requestFullscreen : () -> Cmd msg
port exitFullscreen : () -> Cmd msg
port setPlaybackRate : Float -> Cmd msg
port setQuality : Quality.Quality -> Cmd msg
port nowHasQualities : (List Int -> msg) -> Sub msg
port nowHasQuality : (Decode.Value -> msg) -> Sub msg
fadeIn : Animation
fadeIn =
Animation.fromTo

50
src/Quality.elm Normal file
View File

@ -0,0 +1,50 @@
module Quality exposing (Quality, decode, isSameOption, toString)
import Json.Decode as Decode
type alias Quality =
{ auto : Bool
, height : Int
}
toString : Quality -> String
toString { auto, height } =
if height == 0 then
"Auto"
else if auto then
"Auto (" ++ String.fromInt height ++ "p)"
else
String.fromInt height ++ "p"
isSameOption : Maybe Quality -> Maybe Quality -> Bool
isSameOption quality1 quality2 =
case ( quality1, quality2 ) of
( Just q1, Just q2 ) ->
autoHeight q1 == autoHeight q2
( Nothing, Nothing ) ->
True
_ ->
False
autoHeight : Quality -> Int
autoHeight { auto, height } =
if auto then
0
else
height
decode : Decode.Decoder Quality
decode =
Decode.map2 Quality
(Decode.field "auto" Decode.bool)
(Decode.field "height" Decode.int)