Initial commit
This commit is contained in:
commit
9ca7927fd2
|
@ -0,0 +1,3 @@
|
||||||
|
video
|
||||||
|
elm-stuff
|
||||||
|
js
|
|
@ -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}
|
||||||
|
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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
|
Loading…
Reference in New Issue