2021-06-09 17:27:52 +02:00

412 lines
11 KiB
Elm

port module Main exposing (..)
import Browser
import DOM as Dom
import Element exposing (Element, alignRight, centerY, el, fill, padding, rgb255, row, spacing, text, width)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input
import Html
import Html.Attributes
import Html.Events
import Icons
import Json.Decode as Decode
main : Program Decode.Value Model Msg
main =
Browser.application
{ init = \_ _ _ -> init
, update = update
, view = view
, subscriptions = \_ -> nowHasQualities NowHasQualities
, onUrlChange = \_ -> Noop
, onUrlRequest = \_ -> Noop
}
type alias Model =
{ url : String
, playing : Bool
, position : Float
, duration : Float
, loaded : List ( Float, Float )
, volume : Float
, muted : Bool
, isFullscreen : Bool
, qualities : List Int
}
type Msg
= Noop
| PlayPause
| Seek Float
| RequestFullscreen
| ExitFullscreen
| NowPlaying
| NowPaused
| NowHasDuration Float
| NowAtPosition Float
| NowAtVolume Float Bool
| NowLoaded (List ( Float, Float ))
| NowIsFullscreen Bool
| NowHasQualities (List Int)
init : ( Model, Cmd Msg )
init =
( Model
"video/manifest.m3u8"
False
0.0
1.0
[]
1.0
False
False
[]
, initVideo ()
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Noop ->
( model, Cmd.none )
PlayPause ->
( model, playPause () )
Seek ratio ->
( model, seek (ratio * model.duration) )
RequestFullscreen ->
( model, requestFullscreen () )
ExitFullscreen ->
( model, exitFullscreen () )
NowPlaying ->
( { model | playing = True }, Cmd.none )
NowPaused ->
( { model | playing = False }, Cmd.none )
NowHasDuration duration ->
( { model | duration = duration }, Cmd.none )
NowAtPosition position ->
( { model | position = position }, Cmd.none )
NowAtVolume volume muted ->
( { model | volume = volume, muted = muted }, Cmd.none )
NowLoaded loaded ->
( { model | loaded = loaded }, Cmd.none )
NowIsFullscreen fullscreen ->
( { model | isFullscreen = fullscreen }, Cmd.none )
NowHasQualities qualities ->
( { model | qualities = qualities }, Cmd.none )
view : Model -> Browser.Document Msg
view model =
{ title = "Hello"
, body = [ Element.layout [] (video model) ]
}
video : Model -> Element Msg
video model =
let
seen =
round (model.position * 1000)
loaded =
List.filter (\( start, end ) -> start < model.position) model.loaded
loadedToShow =
every model.duration loaded
showRange : ( Float, Float, Bool ) -> Element msg
showRange ( start, end, isLoaded ) =
let
portion =
round (1000 * (end - start))
in
Element.el
[ Element.width (Element.fillPortion portion)
, Element.height Element.fill
, if isLoaded then
Background.color (Element.rgba 1 1 1 0.5)
else
Background.color (Element.rgba 1 1 1 0)
]
Element.none
loadedElement =
Element.row
[ Element.width Element.fill
, Element.height (Element.px 5)
, Element.centerY
, Border.rounded 5
]
(List.map showRange loadedToShow)
remaining =
round ((model.duration - model.position) * 1000)
bar =
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
[ 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
]
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 ]
]
]
in
Element.el (Element.inFront bar :: Element.width (Element.px 1000) :: Element.htmlAttribute (Html.Attributes.id "full") :: playerEvents)
(Element.html (Html.video videoEvents []))
playPauseButton : Bool -> Element Msg
playPauseButton playing =
let
icon =
if playing then
Icons.pause True
else
Icons.play True
in
Input.button []
{ label = icon
, onPress = Just PlayPause
}
fullscreenButton : Bool -> Element Msg
fullscreenButton isFullscreen =
Input.button []
(if isFullscreen then
{ label = Icons.minimize False
, onPress = Just ExitFullscreen
}
else
{ label = Icons.maximize False
, onPress = Just RequestFullscreen
}
)
playerEvents : List (Element.Attribute Msg)
playerEvents =
List.map Element.htmlAttribute
[ Html.Events.on "fullscreenchange" decodeFullscreenChange
]
videoEvents : List (Html.Attribute Msg)
videoEvents =
[ Html.Attributes.id "video"
, Html.Events.on "playing" (Decode.succeed NowPlaying)
, Html.Events.on "pause" (Decode.succeed NowPaused)
, Html.Events.on "durationchange" decodeDurationChanged
, Html.Events.on "timeupdate" decodePosition
, Html.Events.on "volumechange" decodeVolumeChange
, Html.Events.on "progress" decodeProgress
]
seekBarEvents : List (Element.Attribute Msg)
seekBarEvents =
List.map Element.htmlAttribute
[ Html.Events.on "click" decodeSeek
]
decodeDurationChanged : Decode.Decoder Msg
decodeDurationChanged =
Dom.target <|
Decode.map NowHasDuration
(Decode.field "duration" Decode.float)
decodePosition : Decode.Decoder Msg
decodePosition =
Dom.target <|
Decode.map NowAtPosition
(Decode.field "currentTime" Decode.float)
decodeVolumeChange : Decode.Decoder Msg
decodeVolumeChange =
Dom.target <|
Decode.map2 NowAtVolume
(Decode.field "volume" Decode.float)
(Decode.field "muted" Decode.bool)
decodeSeek : Decode.Decoder Msg
decodeSeek =
Decode.map2 (\x y -> Seek (toFloat x / toFloat y))
(Decode.field "layerX" Decode.int)
(Dom.target <| Decode.field "offsetWidth" Decode.int)
decodeProgress : Decode.Decoder Msg
decodeProgress =
decodeTimeRanges
|> Decode.field "asArray"
|> Decode.field "buffered"
|> Dom.target
|> Decode.map NowLoaded
decodeTimeRanges : Decode.Decoder (List ( Float, Float ))
decodeTimeRanges =
Decode.list decodeTimeRange
decodeTimeRange : Decode.Decoder ( Float, Float )
decodeTimeRange =
Decode.map2 (\x y -> ( x, y ))
(Decode.field "start" Decode.float)
(Decode.field "end" Decode.float)
decodeFullscreenChange : Decode.Decoder Msg
decodeFullscreenChange =
Decode.value
|> Decode.nullable
|> Decode.field "fullscreenElement"
|> Decode.field "document"
|> Dom.target
|> Decode.map (\x -> NowIsFullscreen (x /= Nothing))
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)
everyAux : Float -> Float -> List ( Float, Float, Bool ) -> List ( Float, Float ) -> List ( Float, Float, Bool )
everyAux duration currentTime currentState input =
case input of
[] ->
( currentTime, duration, False ) :: currentState
[ ( start, end ) ] ->
( end, duration, False ) :: ( start, end, True ) :: ( currentTime, start, False ) :: currentState
( start, end ) :: t ->
everyAux duration end (( start, end, True ) :: ( currentTime, start, False ) :: currentState) t
formatTime : Float -> String
formatTime s =
let
seconds =
round s
minutes =
seconds // 60 |> modBy 60
hours =
seconds // 3600
secs =
modBy 60 seconds
secsString =
if secs < 10 then
"0" ++ String.fromInt secs
else
String.fromInt secs
minutesString =
if minutes < 10 && hours > 0 then
"0" ++ String.fromInt minutes
else
String.fromInt minutes
hoursString =
if hours == 0 then
""
else
String.fromInt hours ++ ":"
in
hoursString ++ minutesString ++ ":" ++ secsString
port initVideo : () -> Cmd msg
port playPause : () -> Cmd msg
port seek : Float -> Cmd msg
port requestFullscreen : () -> Cmd msg
port exitFullscreen : () -> Cmd msg
port nowHasQualities : (List Int -> msg) -> Sub msg