Initial commit

This commit is contained in:
Thomas Forgione 2021-06-09 11:00:36 +02:00
commit 9ca7927fd2
5 changed files with 436 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
video
elm-stuff
js

35
Makefile Normal file
View File

@ -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}

26
elm.json Normal file
View File

@ -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": {}
}
}

81
index.html Normal file
View File

@ -0,0 +1,81 @@
<!doctype HTML>
<html>
<head>
<title>twitch.tforgione.fr</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="container"></div>
<script src="https://cdn.rawgit.com/video-dev/hls.js/18bb552/dist/hls.min.js"></script>
<script src="js/main.js"></script>
<script>
Object.defineProperty(TimeRanges.prototype, "asArray", {
get: function() {
var ret = [];
for (var i = 0; i < this.length; i++) {
ret.push({start: this.start(i), end: this.end(i)});
}
return ret;
}
});
var app = Elm.Main.init({
node: document.getElementById('container'),
});
app.ports.initVideo.subscribe(function() {
const video = document.getElementById('video');
const hls = new Hls();
hls.loadSource('video/manifest.m3u8');
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
// Transform available levels into an array of integers (height values).
const availableQualities = hls.levels.map((l) => l.height);
availableQualities.unshift(0);
});
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;
// }
})
hls.attachMedia(video);
function updateQuality(newQuality) {
if (newQuality === 0) {
hls.currentLevel = -1;
} else {
hls.levels.forEach((level, levelIndex) => {
if (level.height === newQuality) {
hls.currentLevel = levelIndex;
}
});
}
}
});
app.ports.playPause.subscribe(function() {
const video = document.getElementById('video');
if (video.paused) {
video.play();
} else {
video.pause();
}
});
app.ports.seek.subscribe(function(arg) {
const video = document.getElementById('video');
video.currentTime = arg;
});
</script>
</body>
</html>

291
src/Main.elm Normal file
View File

@ -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