commit 9ca7927fd2aa091440f868cc05dff6b36108e946 Author: Thomas Forgione Date: Wed Jun 9 11:00:36 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da0d60e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +video +elm-stuff +js diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..33a9a66 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +ifeq ("$(ELM)","") + ELM=elm +endif + +ifeq ("$(ELMLIVE)", "") + ELMLIVE=elm-live +endif + +ifeq ("$(UGLIFYJS)", "") + UGLIFYJS=uglifyjs +endif + +BUILD_DIR=js + +all: dev + +dev: js/main.js + +release: js/main.min.js + +js/main.js: src/** + $(ELM) make src/Main.elm --output $(BUILD_DIR)/main.js + +js/main.min.js: js/main.tmp.js + @$(UGLIFYJS) $(BUILD_DIR)/main.tmp.js --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle > $(BUILD_DIR)/main.min.js + +js/main.tmp.js: src/** + @$(ELM) make src/Main.elm --optimize --output $(BUILD_DIR)/main.tmp.js + +watch: + @$(ELMLIVE) src/Main.elm -p 7000 -d . -- --output $(BUILD_DIR)/main.js + +clean: + @rm -rf $(BUILD_DIR)/{main.js,main.min.js} + diff --git a/elm.json b/elm.json new file mode 100644 index 0000000..90af5ca --- /dev/null +++ b/elm.json @@ -0,0 +1,26 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "K-Adam/elm-dom": "1.0.0", + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0", + "elm/json": "1.1.3", + "mdgriffith/elm-ui": "1.1.8" + }, + "indirect": { + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..2fdeef4 --- /dev/null +++ b/index.html @@ -0,0 +1,81 @@ + + + + twitch.tforgione.fr + + + + +
+ + + + + diff --git a/src/Main.elm b/src/Main.elm new file mode 100644 index 0000000..5da89fd --- /dev/null +++ b/src/Main.elm @@ -0,0 +1,291 @@ +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 Json.Decode as Decode + + +main : Program Decode.Value Model Msg +main = + Browser.application + { init = \_ _ _ -> init + , update = update + , view = view + , subscriptions = \_ -> Sub.none + , onUrlChange = \_ -> Noop + , onUrlRequest = \_ -> Noop + } + + +type alias Model = + { url : String + , playing : Bool + , position : Float + , duration : Float + , loaded : List ( Float, Float ) + , volume : Float + , muted : Bool + } + + +type Msg + = Noop + | PlayPause + | Seek Float + | NowPlaying + | NowPaused + | NowHasDuration Float + | NowAtPosition Float + | NowAtVolume Float Bool + | NowLoaded (List ( Float, Float )) + + +init : ( Model, Cmd Msg ) +init = + ( Model "video/manifest.m3u8" False 0.0 1.0 [] 1.0 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) ) + + 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 ) + + +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 = + 0 + + remaining = + round ((model.duration - model.position) * 1000) + + bar = + Element.column + [ Element.width Element.fill + , Element.padding 10 + , Element.alignBottom + , Font.color (Element.rgb 1 1 1) + , 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 0.4 0.4 0.4 0.75) + , Element.width Element.fill + , Element.height (Element.px 5) + , Element.centerY + , Border.rounded 5 + ] + Element.none + ) + :: seekBarEvents + ) + [ 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 + [ Background.color (Element.rgba 0.7 0.7 0.7 0.75) + , Element.width (Element.fillPortion loaded) + , Element.height Element.fill + , Element.height (Element.px 5) + , Element.centerY + ] + Element.none + , Element.el [ Element.width (Element.fillPortion remaining) ] Element.none + ] + , Element.row + [ Element.spacing 10 ] + [ playPauseButton model.playing + , Element.text (formatTime model.position ++ " / " ++ formatTime model.duration) + ] + ] + in + Element.el [ Element.inFront bar, Element.width (Element.px 1000) ] + (Element.html (Html.video videoEvents [])) + + +playPauseButton : Bool -> Element Msg +playPauseButton playing = + let + icon = + if playing then + "⏸" + + else + "▶" + in + Input.button [] + { label = Element.text icon + , onPress = Just PlayPause + } + + +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 = + Decode.map NowLoaded + (Dom.target <| + Decode.field "buffered" <| + Decode.field "asArray" <| + decodeTimeRanges + ) + + +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) + + +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