Started cleaning

This commit is contained in:
Thomas Forgione 2021-06-14 12:02:55 +02:00
parent 37a8253387
commit 3193f3c382
5 changed files with 908 additions and 780 deletions

View File

@ -42,7 +42,7 @@
let hls; let hls;
app.ports.initVideo.subscribe(function(arg) { app.ports.polymnyVideoInit.subscribe(function(arg) {
const video = document.getElementById('video'); const video = document.getElementById('video');
if (Hls.isSupported()) { if (Hls.isSupported()) {
hls = new Hls(); hls = new Hls();
@ -52,11 +52,11 @@
// Transform available levels into an array of integers (height values). // Transform available levels into an array of integers (height values).
const availableQualities = hls.levels.map((l) => l.height); const availableQualities = hls.levels.map((l) => l.height);
availableQualities.unshift(0); availableQualities.unshift(0);
app.ports.nowHasQualities.send(availableQualities); app.ports.polymnyVideoNowHasQualities.send(availableQualities);
}); });
hls.on(Hls.Events.LEVEL_SWITCHED, function (event, data) { hls.on(Hls.Events.LEVEL_SWITCHED, function (event, data) {
app.ports.nowHasQuality.send({ app.ports.polymnyVideoNowHasQuality.send({
auto: hls.autoLevelEnabled, auto: hls.autoLevelEnabled,
height: hls.levels[data.level].height height: hls.levels[data.level].height
}); });
@ -68,7 +68,7 @@
} }
}); });
app.ports.playPause.subscribe(function() { app.ports.polymnyVideoPlayPause.subscribe(function() {
const video = document.getElementById('video'); const video = document.getElementById('video');
if (video.paused) { if (video.paused) {
video.play(); video.play();
@ -77,26 +77,26 @@
} }
}); });
app.ports.seek.subscribe(function(arg) { app.ports.polymnyVideoSeek.subscribe(function(arg) {
const video = document.getElementById('video'); const video = document.getElementById('video');
console.log(arg); console.log(arg);
video.currentTime = arg; video.currentTime = arg;
}); });
app.ports.requestFullscreen.subscribe(function() { app.ports.polymnyVideoRequestFullscreen.subscribe(function() {
document.getElementById('full').requestFullscreen(); document.getElementById('full').requestFullscreen();
}); });
app.ports.exitFullscreen.subscribe(function() { app.ports.polymnyVideoExitFullscreen.subscribe(function() {
document.exitFullscreen(); document.exitFullscreen();
}); });
app.ports.setPlaybackRate.subscribe(function(arg) { app.ports.polymnyVideoSetPlaybackRate.subscribe(function(arg) {
const video = document.getElementById('video'); const video = document.getElementById('video');
video.playbackRate = arg; video.playbackRate = arg;
}); });
app.ports.setQuality.subscribe(function(arg) { app.ports.polymnyVideoSetQuality.subscribe(function(arg) {
var old = hls.currentLevel; var old = hls.currentLevel;
if (arg.auto) { if (arg.auto) {
hls.currentLevel = -1; hls.currentLevel = -1;
@ -108,14 +108,14 @@
}); });
} }
if (old === hls.currentLevel) { if (old === hls.currentLevel) {
app.ports.nowHasQuality.send({ app.ports.polymnyVideoNowHasQuality.send({
auto: hls.autoLevelEnabled, auto: hls.autoLevelEnabled,
height: hls.currentLevel === -1 ? 0 : hls.levels[hls.currentLevel].height height: hls.currentLevel === -1 ? 0 : hls.levels[hls.currentLevel].height
}); });
} }
}); });
app.ports.setVolume.subscribe(function(arg) { app.ports.polymnyVideoSetVolume.subscribe(function(arg) {
const video = document.getElementById('video'); const video = document.getElementById('video');
video.volume = arg.volume; video.volume = arg.volume;
video.muted = arg.muted; video.muted = arg.muted;

190
src/Events.elm Normal file
View File

@ -0,0 +1,190 @@
module Events exposing (player, seekBar, subs, video)
import Browser.Events
import DOM as Dom
import Element
import Html
import Html.Attributes
import Html.Events
import Json.Decode as Decode
import Quality
import Video exposing (Video)
subs : Video -> Sub Video.Msg
subs model =
Sub.batch
[ Video.nowHasQualities Video.NowHasQualities
, Video.nowHasQuality
(\x ->
case Decode.decodeValue Quality.decode x of
Ok s ->
Video.NowHasQuality s
_ ->
Video.Noop
)
, Browser.Events.onAnimationFrameDelta Video.AnimationFrameDelta
, Browser.Events.onKeyDown (decodeKeyDown model)
]
player : List (Element.Attribute Video.Msg)
player =
List.map Element.htmlAttribute
[ Html.Events.on "fullscreenchange" decodeFullscreenChange
, Html.Events.on "mousemove" (Decode.succeed Video.MouseMove)
]
video : List (Html.Attribute Video.Msg)
video =
[ Html.Attributes.id "video"
, Html.Events.on "playing" (Decode.succeed Video.NowPlaying)
, Html.Events.on "pause" (Decode.succeed Video.NowPaused)
, Html.Events.on "durationchange" decodeDurationChanged
, Html.Events.on "timeupdate" decodePosition
, Html.Events.on "volumechange" decodeVolumeChange
, Html.Events.on "progress" decodeProgress
, Html.Events.on "resize" decodeVideoResize
, Html.Events.on "ratechange" decodePlaybackRateChange
]
seekBar : Video -> List (Element.Attribute Video.Msg)
seekBar model =
List.map Element.htmlAttribute
[ Html.Events.on "click" (decodeSeek model)
]
decodeDurationChanged : Decode.Decoder Video.Msg
decodeDurationChanged =
Dom.target <|
Decode.map Video.NowHasDuration
(Decode.field "duration" Decode.float)
decodePosition : Decode.Decoder Video.Msg
decodePosition =
Dom.target <|
Decode.map Video.NowAtPosition
(Decode.field "currentTime" Decode.float)
decodeVolumeChange : Decode.Decoder Video.Msg
decodeVolumeChange =
Dom.target <|
Decode.map2 Video.NowAtVolume
(Decode.field "volume" Decode.float)
(Decode.field "muted" Decode.bool)
decodeSeek : Video -> Decode.Decoder Video.Msg
decodeSeek model =
Decode.map2 (\x y -> Video.Seek (toFloat x / toFloat y * model.duration))
(Decode.field "layerX" Decode.int)
(Dom.target <| Decode.field "offsetWidth" Decode.int)
decodeProgress : Decode.Decoder Video.Msg
decodeProgress =
decodeTimeRanges
|> Decode.field "asArray"
|> Decode.field "buffered"
|> Dom.target
|> Decode.map Video.NowLoaded
decodeTimeRanges : Decode.Decoder (List ( Float, Float ))
decodeTimeRanges =
Decode.list decodeTimeRange
decodeTimeRange : Decode.Decoder ( Float, Float )
decodeTimeRange =
Decode.map2 Tuple.pair
(Decode.field "start" Decode.float)
(Decode.field "end" Decode.float)
decodeFullscreenChange : Decode.Decoder Video.Msg
decodeFullscreenChange =
Decode.value
|> Decode.nullable
|> Decode.field "fullscreenElement"
|> Decode.field "document"
|> Dom.target
|> Decode.map (\x -> Video.NowIsFullscreen (x /= Nothing))
decodeVideoResize : Decode.Decoder Video.Msg
decodeVideoResize =
Dom.target <|
Decode.map2 (\x y -> Video.NowHasSize ( x, y ))
(Decode.field "videoWidth" Decode.int)
(Decode.field "videoHeight" Decode.int)
decodePlaybackRateChange : Decode.Decoder Video.Msg
decodePlaybackRateChange =
Dom.target <|
Decode.map Video.NowHasPlaybackRate
(Decode.field "playbackRate" Decode.float)
decodeKeyDown : Video -> Decode.Decoder Video.Msg
decodeKeyDown model =
Decode.field "keyCode" Decode.int
|> Decode.andThen
(\x ->
case x of
-- Enter key
32 ->
Decode.succeed Video.PlayPause
-- J key
74 ->
Decode.succeed (Video.Seek (max 0 (model.position - 10)))
-- L key
76 ->
Decode.succeed (Video.Seek (min model.duration (model.position + 10)))
-- K key
75 ->
Decode.succeed Video.PlayPause
-- Left arrow
37 ->
Decode.succeed (Video.Seek (max 0 (model.position - 5)))
-- Right arrow
39 ->
Decode.succeed (Video.Seek (min model.duration (model.position + 5)))
-- Down arrow
40 ->
Decode.succeed (Video.SetVolume (max 0 (model.volume - 0.1)) model.muted)
-- Top arrow
38 ->
Decode.succeed (Video.SetVolume (min 1 (model.volume + 0.1)) model.muted)
-- M key
77 ->
Decode.succeed (Video.SetVolume model.volume (not model.muted))
-- F key
70 ->
Decode.succeed
(if model.isFullscreen then
Video.ExitFullscreen
else
Video.RequestFullscreen
)
_ ->
Decode.fail ("no shortcut for code " ++ String.fromInt x)
)

View File

@ -1,4 +1,4 @@
port module Main exposing (..) module Main exposing (..)
import Browser import Browser
import Browser.Events import Browser.Events
@ -8,6 +8,7 @@ import Element.Background as Background
import Element.Border as Border import Element.Border as Border
import Element.Font as Font import Element.Font as Font
import Element.Input as Input import Element.Input as Input
import Events
import Html import Html
import Html.Attributes import Html.Attributes
import Html.Events import Html.Events
@ -17,6 +18,8 @@ import Quality
import Simple.Animation as Animation exposing (Animation) import Simple.Animation as Animation exposing (Animation)
import Simple.Animation.Animated as Animated import Simple.Animation.Animated as Animated
import Simple.Animation.Property as P import Simple.Animation.Property as P
import Video exposing (Video)
import Views
main : Program Decode.Value Model Msg main : Program Decode.Value Model Msg
@ -28,19 +31,8 @@ main =
, subscriptions = , subscriptions =
\model -> \model ->
Sub.batch Sub.batch
[ nowHasQualities NowHasQualities [ Events.subs model.video |> Sub.map VideoMsg
, nowHasQuality , Browser.Events.onResize (\x y -> NowHasScreenSize ( x, y ))
(\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 ))
, Browser.Events.onKeyDown (decodeKeyDown model)
] ]
, onUrlChange = \_ -> Noop , onUrlChange = \_ -> Noop
, onUrlRequest = \_ -> Noop , onUrlRequest = \_ -> Noop
@ -48,23 +40,8 @@ main =
type alias Model = type alias Model =
{ url : String { video : Video
, playing : Bool
, position : Float
, duration : Float
, loaded : List ( Float, Float )
, volume : Float
, muted : Bool
, isFullscreen : Bool
, quality : Maybe Quality.Quality
, qualities : List Int
, showBar : Bool
, animationFrame : Float
, videoSize : ( Int, Int )
, screenSize : ( Int, Int ) , screenSize : ( Int, Int )
, playbackRate : Float
, settings : Settings
, showSettings : Bool
} }
@ -76,29 +53,8 @@ type Settings
type Msg type Msg
= Noop = Noop
| PlayPause | VideoMsg Video.Msg
| Seek Float | NowHasScreenSize ( Int, Int )
| ToggleSettings
| SetSettings Settings
| SetPlaybackRate Float
| SetQuality Quality.Quality
| SetVolume Float Bool
| RequestFullscreen
| ExitFullscreen
| AnimationFrameDelta Float
| MouseMove
| NowPlaying
| NowPaused
| NowHasDuration Float
| NowAtPosition Float
| NowAtVolume Float Bool
| 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 ) init : Decode.Value -> ( Model, Cmd Msg )
@ -116,25 +72,8 @@ init flags =
Decode.decodeValue (Decode.field "height" Decode.int) flags Decode.decodeValue (Decode.field "height" Decode.int) flags
|> Result.withDefault 0 |> Result.withDefault 0
in in
( Model ( { video = Video.fromUrl url, screenSize = ( width, height ) }
url , Video.init url
False
0.0
1.0
[]
1.0
False
False
Nothing
[]
True
0
( 0, 0 )
( width, height )
1.0
All
False
, initVideo url
) )
@ -144,708 +83,22 @@ update msg model =
Noop -> Noop ->
( model, Cmd.none ) ( model, Cmd.none )
PlayPause -> VideoMsg m ->
( model, playPause () ) let
( video, cmd ) =
Video.update m model.video
in
( { model | video = video }, Cmd.map VideoMsg cmd )
Seek time -> NowHasScreenSize size ->
( model, seek time )
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 )
SetVolume v m ->
( model, setVolume { volume = v, muted = m } )
AnimationFrameDelta delta ->
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 )
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 )
NowHasQuality quality ->
( { model | quality = Just quality }, Cmd.none )
NowHasVideoSize size ->
( { model | videoSize = size }, Cmd.none )
NowHasWindowSize size ->
( { model | screenSize = size }, Cmd.none ) ( { model | screenSize = size }, Cmd.none )
NowHasPlaybackRate rate ->
( { model | playbackRate = rate }, Cmd.none )
view : Model -> Browser.Document Msg view : Model -> Browser.Document Msg
view model = view model =
{ title = "Hello" { title = "Hello"
, body = [ Element.layout [ Element.height Element.fill ] (video model) ] , body =
[ Element.layout [ Element.height Element.fill ]
(Element.map VideoMsg (Views.embed model.screenSize model.video))
]
} }
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 =
animatedEl
(if model.animationFrame < 3000 then
fadeIn
else
fadeOut
)
[ Element.width Element.fill, Element.height Element.fill ]
(Element.column
[ Element.width Element.fill
, Element.alignBottom
, Font.color (Element.rgba 1 1 1 0.85)
]
[ settings model
, Element.column
[ 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 model
)
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
, volumeButton model.volume model.muted
, 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 ]
]
]
]
)
videoAspectRatio =
toFloat (Tuple.first model.videoSize) / toFloat (Tuple.second model.videoSize)
screenAspectRatio =
toFloat (Tuple.first model.screenSize) / toFloat (Tuple.second model.screenSize)
( ( x, y ), ( w, h ) ) =
if videoAspectRatio > screenAspectRatio then
let
videoHeight =
Tuple.first model.screenSize * Tuple.second model.videoSize // Tuple.first model.videoSize
in
( ( 0, (Tuple.second model.screenSize - videoHeight) // 2 )
, ( Tuple.first model.screenSize, videoHeight )
)
else
let
videoWidth =
Tuple.second model.screenSize * Tuple.first model.videoSize // Tuple.second model.videoSize
in
( ( (Tuple.first model.screenSize - videoWidth) // 2, 0 )
, ( videoWidth, Tuple.second model.screenSize )
)
in
Element.el
(Element.inFront bar
:: Element.width Element.fill
:: Element.height Element.fill
:: Background.color (Element.rgb 0 0 0)
:: Element.htmlAttribute (Html.Attributes.id "full")
:: playerEvents
)
(Element.html
(Html.video
(Html.Attributes.style "position" "absolute"
:: Html.Attributes.width w
:: Html.Attributes.height h
:: Html.Attributes.style "top" (String.fromInt y ++ "px")
:: Html.Attributes.style "left" (String.fromInt x ++ "px")
:: videoEvents
)
[]
)
)
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
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
}
)
volumeButton : Float -> Bool -> Element Msg
volumeButton volume muted =
let
icon =
if muted then
Icons.volumeX
else if volume < 0.3 then
Icons.volume
else if volume < 0.6 then
Icons.volume1
else
Icons.volume2
in
Input.button []
{ label = icon True
, onPress = Just (SetVolume volume (not muted))
}
settingsButton : Element Msg
settingsButton =
Input.button []
{ label = Icons.settings False
, onPress = Just ToggleSettings
}
playerEvents : List (Element.Attribute Msg)
playerEvents =
List.map Element.htmlAttribute
[ Html.Events.on "fullscreenchange" decodeFullscreenChange
, Html.Events.on "mousemove" (Decode.succeed MouseMove)
]
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
, Html.Events.on "resize" decodeVideoResize
, Html.Events.on "ratechange" decodePlaybackRateChange
]
seekBarEvents : Model -> List (Element.Attribute Msg)
seekBarEvents model =
List.map Element.htmlAttribute
[ Html.Events.on "click" (decodeSeek model)
]
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 : Model -> Decode.Decoder Msg
decodeSeek model =
Decode.map2 (\x y -> Seek (toFloat x / toFloat y * model.duration))
(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 Tuple.pair
(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))
decodeVideoResize : Decode.Decoder Msg
decodeVideoResize =
Dom.target <|
Decode.map2 (\x y -> NowHasVideoSize ( x, y ))
(Decode.field "videoWidth" Decode.int)
(Decode.field "videoHeight" Decode.int)
decodePlaybackRateChange : Decode.Decoder Msg
decodePlaybackRateChange =
Dom.target <|
Decode.map NowHasPlaybackRate
(Decode.field "playbackRate" Decode.float)
decodeKeyDown : Model -> Decode.Decoder Msg
decodeKeyDown model =
Decode.field "keyCode" Decode.int
|> Decode.andThen
(\x ->
case x of
-- Enter key
32 ->
Decode.succeed PlayPause
-- J key
74 ->
Decode.succeed (Seek (max 0 (model.position - 10)))
-- L key
76 ->
Decode.succeed (Seek (min model.duration (model.position + 10)))
-- K key
75 ->
Decode.succeed PlayPause
-- Left arrow
37 ->
Decode.succeed (Seek (max 0 (model.position - 5)))
-- Right arrow
39 ->
Decode.succeed (Seek (min model.duration (model.position + 5)))
-- Down arrow
40 ->
Decode.succeed (SetVolume (max 0 (model.volume - 0.1)) model.muted)
-- Top arrow
38 ->
Decode.succeed (SetVolume (min 1 (model.volume + 0.1)) model.muted)
-- M key
77 ->
Decode.succeed (SetVolume model.volume (not model.muted))
-- F key
70 ->
Decode.succeed
(if model.isFullscreen then
ExitFullscreen
else
RequestFullscreen
)
_ ->
Decode.fail ("no shortcut for code " ++ String.fromInt x)
)
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 : String -> Cmd msg
port playPause : () -> Cmd msg
port seek : Float -> Cmd msg
port requestFullscreen : () -> Cmd msg
port exitFullscreen : () -> Cmd msg
port setPlaybackRate : Float -> Cmd msg
port setQuality : Quality.Quality -> Cmd msg
port setVolume : { volume : Float, muted : Bool } -> Cmd msg
port nowHasQualities : (List Int -> msg) -> Sub msg
port nowHasQuality : (Decode.Value -> msg) -> Sub msg
fadeIn : Animation
fadeIn =
Animation.fromTo
{ duration = 500
, options = []
}
[ P.opacity 0 ]
[ P.opacity 1 ]
fadeOut : Animation
fadeOut =
Animation.fromTo
{ duration = 500
, options = []
}
[ P.opacity 1 ]
[ P.opacity 0 ]
animatedEl : Animation -> List (Element.Attribute msg) -> Element msg -> Element msg
animatedEl =
animatedUi Element.el
animatedUi =
Animated.ui
{ behindContent = Element.behindContent
, htmlAttribute = Element.htmlAttribute
, html = Element.html
}

234
src/Video.elm Normal file
View File

@ -0,0 +1,234 @@
port module Video exposing (Msg(..), Settings(..), Video, fromUrl, init, nowHasQualities, nowHasQuality, update)
import Json.Decode as Decode
import Quality exposing (Quality)
type alias Video =
{ url : String
, playing : Bool
, position : Float
, duration : Float
, loaded : List ( Float, Float )
, volume : Float
, muted : Bool
, isFullscreen : Bool
, quality : Maybe Quality.Quality
, qualities : List Int
, showBar : Bool
, animationFrame : Float
, size : ( Int, Int )
, playbackRate : Float
, settings : Settings
, showSettings : Bool
}
fromUrl : String -> Video
fromUrl url =
{ url = url
, playing = False
, position = 0
, duration = 0
, loaded = []
, volume = 1
, muted = False
, isFullscreen = False
, quality = Nothing
, qualities = []
, showBar = True
, animationFrame = 0
, size = ( 0, 0 )
, playbackRate = 1
, settings = All
, showSettings = False
}
type Settings
= All
| Speed
| Quality
type Msg
= Noop
| PlayPause
| Seek Float
| ToggleSettings
| SetSettings Settings
| SetPlaybackRate Float
| SetQuality Quality.Quality
| SetVolume Float Bool
| RequestFullscreen
| ExitFullscreen
| AnimationFrameDelta Float
| MouseMove
| NowPlaying
| NowPaused
| NowHasDuration Float
| NowAtPosition Float
| NowAtVolume Float Bool
| NowLoaded (List ( Float, Float ))
| NowIsFullscreen Bool
| NowHasQualities (List Int)
| NowHasQuality Quality.Quality
| NowHasSize ( Int, Int )
| NowHasPlaybackRate Float
update : Msg -> Video -> ( Video, Cmd Msg )
update msg model =
case msg of
Noop ->
( model, Cmd.none )
PlayPause ->
( model, playPause )
Seek time ->
( model, seek time )
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 )
SetVolume v m ->
( model, setVolume { volume = v, muted = m } )
AnimationFrameDelta delta ->
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 )
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 )
NowHasQuality quality ->
( { model | quality = Just quality }, Cmd.none )
NowHasSize size ->
( { model | size = size }, Cmd.none )
NowHasPlaybackRate rate ->
( { model | playbackRate = rate }, Cmd.none )
port polymnyVideoInit : String -> Cmd msg
init : String -> Cmd msg
init =
polymnyVideoInit
port polymnyVideoPlayPause : () -> Cmd msg
playPause : Cmd msg
playPause =
polymnyVideoPlayPause ()
port polymnyVideoSeek : Float -> Cmd msg
seek : Float -> Cmd msg
seek =
polymnyVideoSeek
port polymnyVideoRequestFullscreen : () -> Cmd msg
requestFullscreen : Cmd msg
requestFullscreen =
polymnyVideoRequestFullscreen ()
port polymnyVideoExitFullscreen : () -> Cmd msg
exitFullscreen : Cmd msg
exitFullscreen =
polymnyVideoExitFullscreen ()
port polymnyVideoSetPlaybackRate : Float -> Cmd msg
setPlaybackRate : Float -> Cmd msg
setPlaybackRate =
polymnyVideoSetPlaybackRate
port polymnyVideoSetQuality : Quality -> Cmd msg
setQuality : Quality -> Cmd msg
setQuality =
polymnyVideoSetQuality
port polymnyVideoSetVolume : { volume : Float, muted : Bool } -> Cmd msg
setVolume : { volume : Float, muted : Bool } -> Cmd msg
setVolume =
polymnyVideoSetVolume
port polymnyVideoNowHasQualities : (List Int -> msg) -> Sub msg
nowHasQualities : (List Int -> msg) -> Sub msg
nowHasQualities =
polymnyVideoNowHasQualities
port polymnyVideoNowHasQuality : (Decode.Value -> msg) -> Sub msg
nowHasQuality : (Decode.Value -> msg) -> Sub msg
nowHasQuality =
polymnyVideoNowHasQuality

451
src/Views.elm Normal file
View File

@ -0,0 +1,451 @@
module Views exposing (..)
import Element exposing (Element)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input
import Events as Events
import Html
import Html.Attributes
import Icons
import Quality
import Simple.Animation as Animation exposing (Animation)
import Simple.Animation.Animated as Animated
import Simple.Animation.Property as P
import Video exposing (Video)
embed : ( Int, Int ) -> Video -> Element Video.Msg
embed screenSize 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 =
animatedEl
(if model.animationFrame < 3000 then
fadeIn
else
fadeOut
)
[ Element.width Element.fill, Element.height Element.fill ]
(Element.column
[ Element.width Element.fill
, Element.alignBottom
, Font.color (Element.rgba 1 1 1 0.85)
]
[ settings model
, Element.column
[ 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
:: Events.seekBar model
)
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
, volumeButton model.volume model.muted
, 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 ]
]
]
]
)
videoAspectRatio =
toFloat (Tuple.first model.size) / toFloat (Tuple.second model.size)
screenAspectRatio =
toFloat (Tuple.first screenSize) / toFloat (Tuple.second screenSize)
( ( x, y ), ( w, h ) ) =
if videoAspectRatio > screenAspectRatio then
let
videoHeight =
Tuple.first screenSize * Tuple.second model.size // Tuple.first model.size
in
( ( 0, (Tuple.second screenSize - videoHeight) // 2 )
, ( Tuple.first screenSize, videoHeight )
)
else
let
videoWidth =
Tuple.second screenSize * Tuple.first model.size // Tuple.second model.size
in
( ( (Tuple.first screenSize - videoWidth) // 2, 0 )
, ( videoWidth, Tuple.second screenSize )
)
in
Element.el
(Element.inFront bar
:: Element.width Element.fill
:: Element.height Element.fill
:: Background.color (Element.rgb 0 0 0)
:: Element.htmlAttribute (Html.Attributes.id "full")
:: Events.player
)
(Element.html
(Html.video
(Html.Attributes.style "position" "absolute"
:: Html.Attributes.width w
:: Html.Attributes.height h
:: Html.Attributes.style "top" (String.fromInt y ++ "px")
:: Html.Attributes.style "left" (String.fromInt x ++ "px")
:: Events.video
)
[]
)
)
settings : Video -> Element Video.Msg
settings model =
let
makeMenuButton : Video.Settings -> Element Video.Msg -> Element Video.Msg -> Element Video.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 (Video.SetSettings s)
}
speedButton =
makeMenuButton Video.Speed (Element.text "Speed") (Element.text ("x" ++ String.fromFloat model.playbackRate))
qualityButton =
case model.quality of
Just q ->
makeMenuButton Video.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 (Video.SetSettings Video.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 (Video.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 (Video.SetQuality { auto = x == 0, height = x })
}
)
|> (\x -> returnButton :: x)
buttons =
case model.settings of
Video.All ->
[ speedButton, qualityButton ]
Video.Speed ->
speedOptions
Video.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 Video.Msg
playPauseButton playing =
let
icon =
if playing then
Icons.pause True
else
Icons.play True
in
Input.button []
{ label = icon
, onPress = Just Video.PlayPause
}
fullscreenButton : Bool -> Element Video.Msg
fullscreenButton isFullscreen =
Input.button []
(if isFullscreen then
{ label = Icons.minimize False
, onPress = Just Video.ExitFullscreen
}
else
{ label = Icons.maximize False
, onPress = Just Video.RequestFullscreen
}
)
volumeButton : Float -> Bool -> Element Video.Msg
volumeButton volume muted =
let
icon =
if muted then
Icons.volumeX
else if volume < 0.3 then
Icons.volume
else if volume < 0.6 then
Icons.volume1
else
Icons.volume2
in
Input.button []
{ label = icon True
, onPress = Just (Video.SetVolume volume (not muted))
}
settingsButton : Element Video.Msg
settingsButton =
Input.button []
{ label = Icons.settings False
, onPress = Just Video.ToggleSettings
}
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
fadeIn : Animation
fadeIn =
Animation.fromTo
{ duration = 500
, options = []
}
[ P.opacity 0 ]
[ P.opacity 1 ]
fadeOut : Animation
fadeOut =
Animation.fromTo
{ duration = 500
, options = []
}
[ P.opacity 1 ]
[ P.opacity 0 ]
animatedEl : Animation -> List (Element.Attribute msg) -> Element msg -> Element msg
animatedEl =
animatedUi Element.el
animatedUi =
Animated.ui
{ behindContent = Element.behindContent
, htmlAttribute = Element.htmlAttribute
, html = Element.html
}