diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/Cache.hx | 72 | ||||
| -rw-r--r-- | src/server/HttpServer.hx | 89 | ||||
| -rw-r--r-- | src/server/Main.hx | 24 | ||||
| -rw-r--r-- | src/server/VideoTimer.hx | 7 |
4 files changed, 115 insertions, 77 deletions
diff --git a/src/server/Cache.hx b/src/server/Cache.hx index fa5889c..d772648 100644 --- a/src/server/Cache.hx +++ b/src/server/Cache.hx @@ -17,7 +17,8 @@ class Cache { public final isYtReady = false; /** In bytes **/ - var storageLimit = 3 * 1024 * 1024 * 1024; + public var storageLimit(default, null) = 3 * 1024 * 1024 * 1024; + final freeSpaceBlock = 10 * 1024 * 1024; // 10MB public function new(main:Main, cacheDir:String) { @@ -25,6 +26,7 @@ class Cache { this.cacheDir = cacheDir; Utils.ensureDir(cacheDir); isYtReady = checkYtDeps(); + if (isYtReady) cleanYtInputFiles(); } function checkYtDeps():Bool { @@ -41,6 +43,14 @@ class Cache { } } + function cleanYtInputFiles():Void { + final names = FileSystem.readDirectory(cacheDir); + for (name in names) { + if (!name.startsWith("__tmp")) continue; + remove(name); + } + } + function log(client:Client, msg:String):Void { main.serverMessage(client, msg); trace(msg); @@ -61,6 +71,16 @@ class Cache { callback(outName); return; } + final inVideoName = '__tmp-video-$videoId'; + final inAudioName = '__tmp-audio-$videoId'; + inline function removeInputFiles():Void { + remove(inVideoName); + remove(inAudioName); + } + if (isFileExists(inVideoName)) { + log(client, 'Caching $outName already in progress'); + return; + } final ytdl:Dynamic = untyped require("@distube/ytdl-core"); trace('Caching $url to $outName...'); main.send(client, { @@ -93,26 +113,36 @@ class Cache { final dlVideo:Readable<Dynamic> = ytdl(url, { format: videoFormat, }); - dlVideo.pipe(Fs.createWriteStream('$cacheDir/input-video')); - dlVideo.on("error", err -> log(client, "Error during video download: " + err)); + dlVideo.pipe(Fs.createWriteStream('$cacheDir/$inVideoName')); + dlVideo.on("error", err -> { + log(client, "Error during video download: " + err); + removeInputFiles(); + }); final dlAudio:Readable<Dynamic> = ytdl(url, { format: audioFormat, }); - dlAudio.pipe(Fs.createWriteStream('$cacheDir/input-audio')); - dlAudio.on("error", err -> log(client, "Error during audio download: " + err)); + dlAudio.pipe(Fs.createWriteStream('$cacheDir/$inAudioName')); + dlAudio.on("error", err -> { + log(client, "Error during audio download: " + err); + removeInputFiles(); + }); var count = 0; function onComplete(type:String):Void { count++; trace('$type track downloaded ($count/2)'); if (count < 2) return; - var size = FileSystem.stat('$cacheDir/input-video').size; - size += FileSystem.stat('$cacheDir/input-audio').size; + if (!isFileExists(inVideoName) || !isFileExists(inAudioName)) { + removeInputFiles(); + return; + } + var size = FileSystem.stat('$cacheDir/$inVideoName').size; + size += FileSystem.stat('$cacheDir/$inAudioName').size; // clean some space for full mp4 removeOlderCache(size + freeSpaceBlock); - final args = '-y -i input-video -i input-audio -c copy -map 0:v -map 1:a ./$outName'.split(" "); + final args = '-y -i ./$inVideoName -i ./$inAudioName -c copy -map 0:v -map 1:a ./$outName'.split(" "); final process = ChildProcess.spawn("ffmpeg", args, { cwd: cacheDir, stdio: "ignore" @@ -121,15 +151,11 @@ class Cache { // trace('FFmpeg stderr: ${data}'); // }); process.on("close", (code:Int) -> { + removeInputFiles(); if (code != 0) { log(client, 'Error: ffmpeg closed with code $code'); return; } - final inVideo = '$cacheDir/input-video'; - final inAudio = '$cacheDir/input-audio'; - FileSystem.deleteFile(inVideo); - FileSystem.deleteFile(inAudio); - add(outName); callback(outName); @@ -160,6 +186,7 @@ class Cache { }); }); }).catchError(err -> { + removeInputFiles(); log(client, "" + err); }); } @@ -191,21 +218,32 @@ class Cache { } } + public function remove(name:String):Void { + cachedFiles.remove(name); + removeFile(name); + } + public function removeOlderCache(addFileSize = 0):Void { var space = getUsedSpace(addFileSize); while (space > storageLimit) { final name = cachedFiles.pop() ?? break; - final path = getFilePath(name); - if (FileSystem.exists(path)) FileSystem.deleteFile(path); + removeFile(name); space = getUsedSpace(addFileSize); } } - public function getFreeFileName(baseName = "video"):String { + function removeFile(name:String):Void { + final path = getFilePath(name); + if (FileSystem.exists(path)) FileSystem.deleteFile(path); + } + + public function getFreeFileName(fullName = "video.mp4"):String { + final baseName = Path.withoutDirectory(Path.withoutExtension(fullName)); + final ext = Path.extension(fullName); var i = 1; while (true) { final n = i == 1 ? "" : '$i'; - final name = '$baseName$n.mp4'; + final name = '$baseName$n.$ext'; if (!isFileExists(name)) return name; i++; } diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx index 6602e86..346aac1 100644 --- a/src/server/HttpServer.hx +++ b/src/server/HttpServer.hx @@ -54,7 +54,8 @@ class HttpServer { final allowLocalRequests = false; final cache:Cache = null; final CHUNK_SIZE = 1024 * 1024 * 5; // 5 MB - final uploadingFiles:Map<String, Int> = []; + // temp media data while file is uploading to allow instant streaming + final uploadingFilesSizes:Map<String, Int> = []; final uploadingFilesLastChunks:Map<String, Buffer> = []; public function new(main:Main, config:HttpServerConfig):Void { @@ -76,8 +77,8 @@ class HttpServer { var filePath = getPath(dir, url); final ext = Path.extension(filePath).toLowerCase(); - res.setHeader("Accept-Ranges", "bytes"); - res.setHeader("Content-Type", getMimeType(ext)); + res.setHeader("accept-ranges", "bytes"); + res.setHeader("content-type", getMimeType(ext)); if (cache != null && req.method == "POST") { switch url.pathname { @@ -140,10 +141,11 @@ class HttpServer { final buffer = Buffer.concat(body); uploadingFilesLastChunks[filePath] = buffer; res.writeHead(200, { - 'Content-Type': 'application/json', + "content-type": getMimeType("json"), }); final json:UploadResponse = { - info: "File last chunk uploaded" + info: "File last chunk uploaded", + url: cache.getFileUrl(name) } res.end(Json.stringify(json)); }); @@ -154,70 +156,57 @@ class HttpServer { final clientName = req.headers["client-name"]; final filePath = cache.getFilePath(name); final size = Std.parseInt(req.headers["content-length"]) ?? return; - var written = 0; - inline function end(json:UploadResponse):Void { - uploadingFiles.remove(name); - uploadingFilesLastChunks.remove(name); - - res.statusCode = 200; + inline function end(code:Int, json:UploadResponse):Void { + res.statusCode = code; res.end(Json.stringify(json)); + + uploadingFilesSizes.remove(filePath); + uploadingFilesLastChunks.remove(filePath); } + if (size < cache.storageLimit) { + // do not remove older cache if file is out of limit anyway + cache.removeOlderCache(size); + } if (cache.getFreeSpace() < size) { - end({ - info: "Error: Not enough free space on server or file size is out of cache storage limit.", - errorId: "freeSpace" + final errText = "Error: Not enough free space on server or file size is out of cache storage limit."; + end(413, { // Payload Too Large + info: errText, + errorId: "freeSpace", }); + cache.remove(name); + req.destroy(); + final client = main.clients.getByName(name) ?? return; + main.serverMessage(client, errText); return; } final stream = Fs.createWriteStream(filePath); req.pipe(stream); - inline function onStart() { - cache.removeOlderCache(size); - cache.add(name); - uploadingFiles[filePath] = size; - } - var isStart = true; - req.on("data", chunk -> { - var url:String = null; - if (isStart) { - isStart = false; - onStart(); - url = cache.getFileUrl(name); - } - written += chunk.length; - final ratio = (written / size).clamp(0, 1); - final percent = (ratio * 100).toFixed(2); - final client = main.clients.getByName(clientName) ?? return; - main.send(client, { - type: Progress, - progress: { - type: Uploading, - ratio: ratio, - data: url - } - }); - }); + cache.add(name); + uploadingFilesSizes[filePath] = size; + stream.on("close", () -> { - end({ + end(200, { info: "File write stream closed.", }); }); stream.on("error", err -> { trace(err); - end({ + end(500, { info: "File write stream error.", }); + cache.remove(name); }); req.on("error", err -> { trace("Request Error:", err); stream.destroy(); - end({ + end(500, { info: "File request error.", }); + cache.remove(name); }); } @@ -229,7 +218,7 @@ class HttpServer { } function readFileError(err:Dynamic, res:ServerResponse, filePath:String):Void { - res.setHeader("Content-Type", getMimeType("html")); + res.setHeader("content-type", getMimeType("html")); if (err.code == "ENOENT") { res.statusCode = 404; var rel = JsPath.relative(dir, filePath); @@ -244,13 +233,13 @@ class HttpServer { if (!Fs.existsSync(filePath)) return false; var videoSize:Int = cast Fs.statSync(filePath).size; // use future content length to start playing it before uploaded - if (uploadingFiles.exists(filePath)) { - videoSize = uploadingFiles[filePath]; + if (uploadingFilesSizes.exists(filePath)) { + videoSize = uploadingFilesSizes[filePath]; } final rangeHeader:String = req.headers["range"]; if (rangeHeader == null) { res.statusCode = 200; - res.setHeader("Content-Length", '$videoSize'); + res.setHeader("content-length", '$videoSize'); final videoStream = Fs.createReadStream(filePath); videoStream.pipe(res); res.on("error", () -> videoStream.destroy()); @@ -262,8 +251,8 @@ class HttpServer { final end = range.end; final contentLength = end - start + 1; - res.setHeader("Content-Range", 'bytes $start-$end/$videoSize'); - res.setHeader("Content-Length", '$contentLength'); + res.setHeader("content-range", 'bytes $start-$end/$videoSize'); + res.setHeader("content-length", '$contentLength'); res.statusCode = 206; // partial content // check for last chunk cache for instant play while uploading @@ -368,7 +357,7 @@ class HttpServer { function isChildOf(parent:String, child:String):Bool { final rel = JsPath.relative(parent, child); - return rel.length > 0 && !rel.startsWith('..') && !JsPath.isAbsolute(rel); + return rel.length > 0 && !rel.startsWith("..") && !JsPath.isAbsolute(rel); } function getMimeType(ext:String):String { diff --git a/src/server/Main.hx b/src/server/Main.hx index d5c0800..644a8b1 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -517,7 +517,6 @@ class Main { clients.remove(client); sendClientList(); if (client.isLeader) { - // if (videoTimer.isPaused()) videoTimer.play(); if (videoList.length > 0) { videoTimer.pause(); isServerPause = true; @@ -700,6 +699,7 @@ class Main { case VideoLoaded: // Called if client loads next video and can play it + if (isServerPause) return; prepareVideoPlayback(); case RemoveVideo: @@ -715,12 +715,8 @@ class Main { saveFlashbackTime(videoList.currentItem); } videoList.removeItem(index); - if (isCurrent && videoList.length > 0) { - broadcast(data); - restartWaitTimer(); - } else { - broadcast(data); - } + broadcast(data); + if (isCurrent && videoList.length > 0) restartWaitTimer(); case SkipVideo: if (!checkPermission(client, RemoveVideoPerm)) return; @@ -857,10 +853,12 @@ class Main { case PlayItem: if (!checkPermission(client, ChangeOrderPerm)) return; + final pos = data.playItem.pos; + if (!videoList.hasItem(pos)) return; if (videoTimer.getTime() > FLASHBACK_DIST) { saveFlashbackTime(videoList.currentItem); } - videoList.setPos(data.playItem.pos); + videoList.setPos(pos); data.playItem.pos = videoList.pos; restartWaitTimer(); broadcast(data); @@ -869,6 +867,7 @@ class Main { if (isPlaylistLockedFor(client)) return; if (!checkPermission(client, ChangeOrderPerm)) return; final pos = data.setNextItem.pos; + if (!videoList.hasItem(pos)) return; if (pos == videoList.pos || pos == videoList.pos + 1) return; videoList.setNextItem(pos); broadcast(data); @@ -877,6 +876,7 @@ class Main { if (isPlaylistLockedFor(client)) return; if (!checkPermission(client, ToggleItemTypePerm)) return; final pos = data.toggleItemType.pos; + if (!videoList.hasItem(pos)) return; videoList.toggleItemType(pos); broadcast(data); @@ -942,6 +942,12 @@ class Main { } final json = jsonStringify(data, "\t"); send(client, { + type: ServerMessage, + serverMessage: { + textId: "Free space: " + (cache.getFreeSpace() / 1024).toFixed() + "KiB" + } + }); + send(client, { type: Dump, dump: { data: json @@ -1047,7 +1053,7 @@ class Main { } final ip = clientIp(client.req); final currentTime = Date.now().getTime(); - for (ban in userList.bans) { + for (ban in userList.bans.reversed()) { if (ban.ip != ip) continue; final isOutdated = ban.toDate.getTime() < currentTime; client.isBanned = !isOutdated; diff --git a/src/server/VideoTimer.hx b/src/server/VideoTimer.hx index fcbb461..311e7f4 100644 --- a/src/server/VideoTimer.hx +++ b/src/server/VideoTimer.hx @@ -26,6 +26,11 @@ class VideoTimer { } public function pause():Void { + if (isPaused()) return; + updatePauseTime(); + } + + function updatePauseTime():Void { startTime += rateTime() - rateTime() * this.rate; pauseStartTime = stamp(); rateStartTime = 0; @@ -47,7 +52,7 @@ class VideoTimer { public function setTime(secs:Float):Void { startTime = stamp() - secs; rateStartTime = stamp(); - if (isPaused()) pause(); + if (isPaused()) updatePauseTime(); } public function isPaused():Bool { |
