diff options
| author | RblSb <msrblsb@gmail.com> | 2025-01-26 23:22:33 +0300 |
|---|---|---|
| committer | RblSb <msrblsb@gmail.com> | 2025-01-28 15:46:30 +0300 |
| commit | 0592564264fff57ccfd9677957196951f9f1c6cf (patch) | |
| tree | c360c2e5d45d9ac8706f836b0466b88221e1f10d /src/client | |
| parent | c7518e58788c17ad2ca8340ab5c7633489aa9518 (diff) | |
Video upload feature
And you can play video as soon as upload starts! This is pretty useful for thicc ones. Video will be keeped in cache and will comply cache size limit.
I'm also implemented system storage check to change cache limit if it's lower than config value, so upload will be blocked if there is lower than 10MiB available on disk.
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/Buttons.hx | 61 | ||||
| -rw-r--r-- | src/client/JsApi.hx | 24 | ||||
| -rw-r--r-- | src/client/Main.hx | 45 | ||||
| -rw-r--r-- | src/client/Utils.hx | 56 | ||||
| -rw-r--r-- | src/client/players/Raw.hx | 2 |
5 files changed, 163 insertions, 25 deletions
diff --git a/src/client/Buttons.hx b/src/client/Buttons.hx index fb8337d..334debc 100644 --- a/src/client/Buttons.hx +++ b/src/client/Buttons.hx @@ -1,7 +1,10 @@ package client; +import Types.UploadResponse; +import Types.WsEvent; import client.Main.ge; import haxe.Timer; +import haxe.io.Path; import js.Browser.document; import js.Browser.window; import js.html.Element; @@ -239,6 +242,60 @@ class Buttons { mediaUrl.focus(); } + ge("#mediaurl-upload").onclick = e -> { + Utils.browseFile((buffer, name) -> { + if (name == null || name.length == 0) name = "video"; + + // send last chunk separately to allow server file streaming while uploading + final chunkSize = 1024 * 1024 * 5; // 5 MB + if (buffer.byteLength > chunkSize) { + final lastChunk = buffer.slice(buffer.byteLength - chunkSize); + window.fetch("/upload-last-chunk", { + method: "POST", + headers: { + "content-name": Path.withoutExtension(name), + "client-name": main.getName(), + }, + body: lastChunk, + }); + } + + // send full file + final request = window.fetch("/upload", { + method: "POST", + headers: { + "content-name": Path.withoutExtension(name), + "client-name": main.getName(), + }, + body: buffer, + }); + request.then(e -> { + e.json().then((data:UploadResponse) -> { + trace(data.info); + if (data.errorId == null) return; + main.serverMessage(data.info, true, false); + }); + }).catchError(err -> { + trace(err); + Timer.delay(() -> { + main.hideDynamicChin(); + }, 500); + }); + + // set file url to input after upload starts + function onStartUpload(event:WsEvent):Void { + if (event.type != Progress) return; + final data = event.progress; + if (data.type != Uploading) return; + if (data.data == null) return; + final input:InputElement = ge("#mediaurl"); + input.value = data.data; + JsApi.off(Progress, onStartUpload); + } + JsApi.on(Progress, onStartUpload); + }); + } + final showOptions = ge("#showoptions"); showOptions.onclick = e -> { final isActive = toggleGroup(showOptions); @@ -362,7 +419,7 @@ class Buttons { } final selectLocalVideoBtn = ge("#selectLocalVideoBtn"); selectLocalVideoBtn.onclick = e -> { - Utils.browseFileUrl((url:String, name:String) -> { + Utils.browseFileUrl((url, name) -> { JsApi.setVideoSrc(url); }); } @@ -374,7 +431,7 @@ class Buttons { ge("#getplaylist").title += " (Alt-C)"; ge("#fullscreenbtn").title += " (Alt-F)"; ge("#leader_btn").title += " (Alt-L)"; - window.onkeydown = function(e:KeyboardEvent) { + window.onkeydown = (e:KeyboardEvent) -> { if (!settings.hotkeysEnabled) return; final target:Element = cast e.target; if (isElementEditable(target)) return; diff --git a/src/client/JsApi.hx b/src/client/JsApi.hx index defcc4d..b576b47 100644 --- a/src/client/JsApi.hx +++ b/src/client/JsApi.hx @@ -8,7 +8,7 @@ import js.Browser.window; import js.Syntax; private typedef VideoChangeFunc = (item:VideoItem) -> Void; -private typedef OnceEventFunc = (event:WsEvent) -> Void; +private typedef EventCallback = (event:WsEvent) -> Void; class JsApi { static var main:Main; @@ -16,7 +16,8 @@ class JsApi { static final subtitleFormats = []; static final videoChange:Array<VideoChangeFunc> = []; static final videoRemove:Array<VideoChangeFunc> = []; - static final onceListeners:Array<{type:WsEventType, callback:OnceEventFunc}> = []; + static final onListeners:Array<{type:WsEventType, callback:EventCallback}> = []; + static final onceListeners:Array<{type:WsEventType, callback:EventCallback}> = []; public static function init(main:Main, player:Player):Void { JsApi.main = main; @@ -147,11 +148,26 @@ class JsApi { * `});` */ @:expose - public static function once(type:WsEventType, callback:OnceEventFunc):Void { + public static function once(type:WsEventType, callback:EventCallback):Void { onceListeners.unshift({type: type, callback: callback}); } - public static function fireOnceEvent(event:WsEvent):Void { + public static function on(type:WsEventType, callback:EventCallback):Void { + onListeners.unshift({type: type, callback: callback}); + } + + public static function off(type:WsEventType, callback:EventCallback):Void { + final listener = onListeners.find(item -> { + return item.type == type && item.callback == callback; + }); + onListeners.remove(listener); + } + + public static function fireEvents(event:WsEvent):Void { + for (listener in onListeners.reversed()) { + if (listener.type != event.type) continue; + listener.callback(event); + } for (listener in onceListeners.reversed()) { if (listener.type != event.type) continue; listener.callback(event); diff --git a/src/client/Main.hx b/src/client/Main.hx index 6ec8727..61b3c3a 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -25,8 +25,6 @@ import js.html.URL; import js.html.VideoElement; import js.html.WebSocket; -using ClientTools; - class Main { public static var instance(default, null):Main; static inline var SETTINGS_VERSION = 5; @@ -457,7 +455,7 @@ class Main { final t = t.charAt(0).toLowerCase() + t.substr(1); trace('Event: ${data.type}', Reflect.field(data, t)); } - JsApi.fireOnceEvent(data); + JsApi.fireEvents(data); switch (data.type) { case Connected: onConnected(data); @@ -509,6 +507,26 @@ class Main { } serverMessage(text); + case Progress: + final data = data.progress; + final text = switch data.type { + case Caching: + final caching = Lang.get("caching"); + final name = data.data; + '$caching $name'; + case Downloading: Lang.get("downloading"); + case Uploading: Lang.get("uploading"); + } + final percent = (data.ratio * 100).toFixed(1); + var text = '$text...'; + if (percent > 0) text += ' $percent%'; + showProgressInfo(text); + if (data.ratio == 1) { + Timer.delay(() -> { + hideDynamicChin(); + }, 500); + } + case AddVideo: player.addVideoItem(data.addVideo.item, data.addVideo.atEnd); if (player.itemsLength() == 1) player.setVideo(0); @@ -934,7 +952,7 @@ class Main { return msgBuf.lastElementChild?.className.startsWith("server-msg"); } - public function serverMessage(text:String, isText = true, withTimestamp = true):Void { + public function serverMessage(text:String, isText = true, withTimestamp = true):Element { final div = document.createDivElement(); final time = Date.now().toString().split(" ")[1]; div.className = "server-whisper"; @@ -947,6 +965,7 @@ class Main { else textDiv.innerHTML = text; addMessageDiv(div); scrollChatToEnd(); + return div; } public function serverHtmlMessage(el:Element):Void { @@ -1071,6 +1090,18 @@ class Main { }, {once: true}); } + public function showProgressInfo(text:String):Void { + final chin = ge("#dynamic-chin"); + var div = chin.querySelector("#progress-info"); + if (div == null) { + div = document.createDivElement(); + div.id = "progress-info"; + chin.prepend(div); + } + div.textContent = text; + showDynamicChin(); + } + public function showServerUnpause():Void { if (showingServerPause) return; showingServerPause = true; @@ -1096,6 +1127,12 @@ class Main { JsApi.once(SetLeader, event -> removeLeader()); } + showDynamicChin(); + } + + function showDynamicChin():Void { + final chin = ge("#dynamic-chin"); + if (chin.style.display == "") return; chin.style.display = ""; chin.style.transition = "none"; chin.classList.remove("collapsed"); diff --git a/src/client/Utils.hx b/src/client/Utils.hx index 4d85697..a120166 100644 --- a/src/client/Utils.hx +++ b/src/client/Utils.hx @@ -5,7 +5,9 @@ import js.Browser.document; import js.Browser.navigator; import js.Browser.window; import js.html.Element; +import js.html.FileReader; import js.html.URL; +import js.lib.ArrayBuffer; class Utils { public static function nativeTrace(msg:Dynamic, ?infos:haxe.PosInfos):Void { @@ -127,25 +129,51 @@ class Utils { #end } + public static function browseFile( + onFileLoad:(buffer:ArrayBuffer, name:String) -> Void + ):Void { + browseFileImpl(onFileLoad, true, false); + } + public static function browseFileUrl( onFileLoad:(url:String, name:String) -> Void, - isBinary = true, revoke = false ):Void { - final input = document.createElement("input"); + browseFileImpl(onFileLoad, false, revoke); + } + + static function browseFileImpl( + onFileLoad:(data:Dynamic, name:String) -> Void, + isBinary:Bool, + revokeAfterLoad:Bool + ):Void { + final input = document.createInputElement(); input.style.visibility = "hidden"; - input.setAttribute("type", "file"); + input.type = "file"; input.id = "browse"; - input.onclick = function(e) { + input.onclick = e -> { e.cancelBubble = true; e.stopPropagation(); } - input.onchange = function() { - final file:Dynamic = (input : Dynamic).files[0]; - final url = URL.createObjectURL(file); - onFileLoad(url, file.name); - document.body.removeChild(input); - if (revoke) URL.revokeObjectURL(url); + input.onchange = e -> { + final file = input.files[0] ?? return; + if (!isBinary) { + final url = URL.createObjectURL(file); + onFileLoad(url, file.name); + document.body.removeChild(input); + if (revokeAfterLoad) URL.revokeObjectURL(url); + return; + } + final reader = new FileReader(); + reader.onload = e -> { + final result:ArrayBuffer = reader.result; + onFileLoad(result, file.name); + document.body.removeChild(input); + } + reader.onerror = e -> { + document.body.removeChild(input); + } + reader.readAsArrayBuffer(file); } document.body.appendChild(input); input.click(); @@ -156,10 +184,10 @@ class Utils { type: mime }); final url = URL.createObjectURL(blob); - final a = document.createElement("a"); - untyped a.download = name; - untyped a.href = url; - a.onclick = function(e) { + final a = document.createAnchorElement(); + a.download = name; + a.href = url; + a.onclick = e -> { e.cancelBubble = true; e.stopPropagation(); } diff --git a/src/client/players/Raw.hx b/src/client/players/Raw.hx index f054f14..10a54e8 100644 --- a/src/client/players/Raw.hx +++ b/src/client/players/Raw.hx @@ -97,7 +97,7 @@ class Raw implements IPlayer { public function loadVideo(item:VideoItem):Void { final url = main.tryLocalIp(item.url); - final isHls = item.url.contains("m3u8") || item.title.endsWith("m3u8"); + final isHls = url.contains("m3u8") || item.title.endsWith("m3u8"); if (isHls && !isHlsLoaded) { loadHlsPlugin(() -> loadVideo(item)); return; |
