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