diff --git a/README.md b/README.md
index 09a3a47..da68f4a 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,40 @@
-## SyncTube
+#
SyncTube
Synchronized video viewing with chat and other features.
Lightweight modern implementation and very easy way to run locally.
Default channel example: https://synctube.onrender.com/
-### Features
-- Multi-Language support
-- Hotkeys (`Alt-P` for global play/pause, [etc](https://github.com/RblSb/SyncTube/blob/d3f6d4e6434527569d13f211a0eb074c5a11992e/src/client/Buttons.hx#L303-L314))
-- Mobile view with page fullscreen
-- Way to play local videos for network users (without NAT loopback feature)
+## Features
+- Control video playback for all users with active `Leader` button
+- Start watching local videos while uploading them on server, before upload completes
+- External `vtt`/`srt`/`ass` subtitles support
+- External audiotrack / voiceover support
+- `/30`, `/-21`, etc chat commands to rewind video playback in seconds
+- Hotkeys (`Alt-P` for global play/pause, [etc](https://github.com/RblSb/SyncTube/blob/382f9b2ebedca905028341825350a0fa69d88673/src/client/Buttons.hx#L416-L427))
+- Compact view button with page fullscreen on Android
- Playback rate synchronization (with leader)
-- `/30`, `/-21`, etc to rewind video playback in seconds
- Links mask: `foo.com/bar${1-4}.mp4` to add multiple items
- Override every front-end file you want (`user/res` folder)
- [Native mobile client](https://github.com/RblSb/SyncTubeApp)
-### Supported players
+### Easier playback controls for smaller groups
+- Enable `requestLeaderOnPause` to allow global pause by anybody, without `Leader` button
+- Enable `unpauseWithoutLeader` to allow global unpause for non-leaders
+
+## Supported players
- Youtube (videos, shorts, streams and playlists)
- [Streamable](https://streamable.com)
- [VK](https://vk.com/video)
- Raw mp4 videos and m3u8 playlists (or any other media format supported in browser)
- Iframes (without sync)
-### Setup
+## Setup
- Open `4200` port in your router settings (port is customizable)
- `npm ci` in this project folder ([NodeJS 14+](https://nodejs.org) required)
- Run `node build/server.js`
- Open showed "Local" link for yourself and send "Global" link to friends
-### Setup (Docker)
+## Setup (Docker)
As alternative, you can install Docker and run:
> ```shell
> docker build -t synctube .
@@ -44,40 +50,54 @@ or
- (Docker container hides real local/global ips, so you need to checkout it manually)
-### Optional dependencies
+## Optional dependencies
If you want to enable `Cache on server` feature for Youtube player, you can also run:
```shell
npm i @distube/ytdl-core@latest
```
-And install `ffmpeg` on your server system. Default cache size is 3.0 GiB.
+And install `ffmpeg` on your server system, it's only used to build single mp4 from downloaded audio/video tracks. Default cache size is 3.0 GiB.
-### Configuration
+## Configuration
It's just works, but you can also check [user/ folder](/user/README.md) for server settings and additional customization.
-### Plugins
-- [octosubs](https://github.com/RblSb/SyncTube-octosubs) - `ASS`/`SSA` subtitles support
-- [qswitcher](https://github.com/aNNiMON/SyncTube-QSwitcher) - raw video quality switcher
-
-### How to use
+## How to use
- Login with any nickname
- Add your video url with "plus" button below (youtube or direct link to mp4 for example)
- Now it plays and syncs for all page users, well done
-- You can click "leader" button to get access to global video controls (play/pause, time setting, playback speed)
+- You can click "leader" button to get access to global video controls (play/pause, seeking, playback speed)
- If you want to restrict permissions or add admins/emotes, see `Configuration` above
-### Intergations
-#### Heroku:
+## Chat commands
+- `/-1h9m54` - Command format to rewind video **back** by `1 hour 9 minutes 54 seconds`
+- `/ad` - Rewind sponsored block in active YouTube video
+- `/fb` (`/flashback`) - rewind video to a prev time if someone rewinded/restarted video accidentally
+- `/clear` - Clear chat. Admin clears chat globally
+- `/help` - Show initial tutorial message
+
+### Admins only:
+
+- `/ban Guest 1 2h` - Ban user `Guest 1` ip for `2 hours`
+- `/unban Foo` (`/removeBan`) - Unban user `Foo`
+- `/kick Foo` - Force `Foo` disconnection until page reload
+- `/dump` - Download state dump to report issues
+
+## Plugins
+- [octosubs](https://github.com/RblSb/SyncTube-octosubs) - More colorful `ASS`/`SSA` subtitles support
+- [qswitcher](https://github.com/aNNiMON/SyncTube-QSwitcher) - Raw video quality switcher
+
+## Intergations
+### Heroku:
- Create app and commit repo to get build
- Remove `user/` folder from `.gitignore` and commit it to change default configuration
- Add `APP_URL` config var with `your-app-link.herokuapp.com` value to prevent sleeping when clients online
-### Development
+## Development
- Install [Haxe 4.3](https://haxe.org/download/), [VSCode](https://code.visualstudio.com) and [Haxe extension](https://marketplace.visualstudio.com/items?itemName=nadako.vshaxe)
- `haxelib install all` to install extern libs
- If you skipped `Setup` section before: `npm ci`
- Open project in VSCode and press `F5` for client+server build and run
-### About
-- Layout and design by [Austin Riddell](https://github.com/aus-tin)
+## About
+- [Redesign](https://github.com/RblSb/SyncTube/pull/5) by Austin Riddell
- [Original idea](https://github.com/calzoneman/sync) by Calvin Montgomery
- Default emotes by [emlan](https://www.deviantart.com/emlan)
diff --git a/build/server.js b/build/server.js
index 28d861d..d2062b0 100644
--- a/build/server.js
+++ b/build/server.js
@@ -2447,6 +2447,9 @@ VideoList.prototype = {
}
this.pos = i;
}
+ ,hasItem: function(i) {
+ return this.items[i] != null;
+ }
,exists: function(f) {
return Lambda.exists(this.items,f);
}
@@ -3685,6 +3688,9 @@ var server_Cache = function(main,cacheDir) {
this.cacheDir = cacheDir;
server_Utils.ensureDir(cacheDir);
this.isYtReady = this.checkYtDeps();
+ if(this.isYtReady) {
+ this.cleanYtInputFiles();
+ }
};
server_Cache.__name__ = true;
server_Cache.prototype = {
@@ -3702,14 +3708,26 @@ server_Cache.prototype = {
return false;
}
}
+ ,cleanYtInputFiles: function() {
+ var names = js_node_Fs.readdirSync(this.cacheDir);
+ var _g = 0;
+ while(_g < names.length) {
+ var name = names[_g];
+ ++_g;
+ if(!StringTools.startsWith(name,"__tmp")) {
+ continue;
+ }
+ this.remove(name);
+ }
+ }
,log: function(client,msg) {
this.main.serverMessage(client,msg);
- haxe_Log.trace(msg,{ fileName : "src/server/Cache.hx", lineNumber : 46, className : "server.Cache", methodName : "log"});
+ haxe_Log.trace(msg,{ fileName : "src/server/Cache.hx", lineNumber : 56, className : "server.Cache", methodName : "log"});
}
,cacheYoutubeVideo: function(client,url,callback) {
var _gthis = this;
if(!this.isYtReady) {
- haxe_Log.trace("Do `npm i @distube/ytdl-core@latest` to use cache feature (you also need to install `ffmpeg` to build mp4 from downloaded audio/video tracks).",{ fileName : "src/server/Cache.hx", lineNumber : 51, className : "server.Cache", methodName : "cacheYoutubeVideo"});
+ haxe_Log.trace("Do `npm i @distube/ytdl-core@latest` to use cache feature (you also need to install `ffmpeg` to build mp4 from downloaded audio/video tracks).",{ fileName : "src/server/Cache.hx", lineNumber : 61, className : "server.Cache", methodName : "cacheYoutubeVideo"});
return;
}
var videoId = utils_YoutubeUtils.extractVideoId(url);
@@ -3722,12 +3740,18 @@ server_Cache.prototype = {
callback(outName);
return;
}
+ var inVideoName = "__tmp-video-" + videoId;
+ var inAudioName = "__tmp-audio-" + videoId;
+ if(this.isFileExists(inVideoName)) {
+ this.log(client,"Caching " + outName + " already in progress");
+ return;
+ }
var ytdl = require("@distube/ytdl-core");
- haxe_Log.trace("Caching " + url + " to " + outName + "...",{ fileName : "src/server/Cache.hx", lineNumber : 65, className : "server.Cache", methodName : "cacheYoutubeVideo"});
+ haxe_Log.trace("Caching " + url + " to " + outName + "...",{ fileName : "src/server/Cache.hx", lineNumber : 85, className : "server.Cache", methodName : "cacheYoutubeVideo"});
this.main.send(client,{ type : "Progress", progress : { type : "Caching", ratio : 0, data : outName}});
var promise = ytdl.getInfo(url);
promise.then(function(info) {
- haxe_Log.trace("Get info with " + info.formats.length + " formats",{ fileName : "src/server/Cache.hx", lineNumber : 76, className : "server.Cache", methodName : "cacheYoutubeVideo"});
+ haxe_Log.trace("Get info with " + info.formats.length + " formats",{ fileName : "src/server/Cache.hx", lineNumber : 96, className : "server.Cache", methodName : "cacheYoutubeVideo"});
var audioFormat;
try {
var ytdl1 = ytdl.chooseFormat;
@@ -3746,7 +3770,7 @@ server_Cache.prototype = {
} catch( _g ) {
var e = haxe_Exception.caught(_g);
_gthis.log(client,"Error: audio format not found");
- haxe_Log.trace(e,{ fileName : "src/server/Cache.hx", lineNumber : 83, className : "server.Cache", methodName : "cacheYoutubeVideo"});
+ haxe_Log.trace(e,{ fileName : "src/server/Cache.hx", lineNumber : 103, className : "server.Cache", methodName : "cacheYoutubeVideo"});
var _g1 = [];
var _g2 = 0;
var _g3 = info.formats;
@@ -3757,7 +3781,7 @@ server_Cache.prototype = {
_g1.push(v);
}
}
- haxe_Log.trace(_g1,{ fileName : "src/server/Cache.hx", lineNumber : 84, className : "server.Cache", methodName : "cacheYoutubeVideo"});
+ haxe_Log.trace(_g1,{ fileName : "src/server/Cache.hx", lineNumber : 104, className : "server.Cache", methodName : "cacheYoutubeVideo"});
return;
}
var videoFormat;
@@ -3776,40 +3800,47 @@ server_Cache.prototype = {
_g.push(v);
}
}
- haxe_Log.trace(_g,{ fileName : "src/server/Cache.hx", lineNumber : 89, className : "server.Cache", methodName : "cacheYoutubeVideo"});
+ haxe_Log.trace(_g,{ fileName : "src/server/Cache.hx", lineNumber : 109, className : "server.Cache", methodName : "cacheYoutubeVideo"});
return;
}
var dlVideo = ytdl(url,{ format : videoFormat});
- dlVideo.pipe(js_node_Fs.createWriteStream("" + _gthis.cacheDir + "/input-video"));
+ dlVideo.pipe(js_node_Fs.createWriteStream("" + _gthis.cacheDir + "/" + inVideoName));
dlVideo.on("error",function(err) {
_gthis.log(client,"Error during video download: " + err);
+ _gthis.remove(inVideoName);
+ _gthis.remove(inAudioName);
});
var dlAudio = ytdl(url,{ format : audioFormat});
- dlAudio.pipe(js_node_Fs.createWriteStream("" + _gthis.cacheDir + "/input-audio"));
+ dlAudio.pipe(js_node_Fs.createWriteStream("" + _gthis.cacheDir + "/" + inAudioName));
dlAudio.on("error",function(err) {
_gthis.log(client,"Error during audio download: " + err);
+ _gthis.remove(inVideoName);
+ _gthis.remove(inAudioName);
});
var count = 0;
var onComplete = function(type) {
count += 1;
- haxe_Log.trace("" + type + " track downloaded (" + count + "/2)",{ fileName : "src/server/Cache.hx", lineNumber : 108, className : "server.Cache", methodName : "cacheYoutubeVideo"});
+ haxe_Log.trace("" + type + " track downloaded (" + count + "/2)",{ fileName : "src/server/Cache.hx", lineNumber : 134, className : "server.Cache", methodName : "cacheYoutubeVideo"});
if(count < 2) {
return;
}
- var size = js_node_Fs.statSync("" + _gthis.cacheDir + "/input-video").size;
- size += js_node_Fs.statSync("" + _gthis.cacheDir + "/input-audio").size;
+ if(!_gthis.isFileExists(inVideoName) || !_gthis.isFileExists(inAudioName)) {
+ _gthis.remove(inVideoName);
+ _gthis.remove(inAudioName);
+ return;
+ }
+ var size = js_node_Fs.statSync("" + _gthis.cacheDir + "/" + inVideoName).size;
+ size += js_node_Fs.statSync("" + _gthis.cacheDir + "/" + inAudioName).size;
_gthis.removeOlderCache(size + _gthis.freeSpaceBlock);
- var args = ("-y -i input-video -i input-audio -c copy -map 0:v -map 1:a ./" + outName).split(" ");
+ var args = ("-y -i ./" + inVideoName + " -i ./" + inAudioName + " -c copy -map 0:v -map 1:a ./" + outName).split(" ");
var $process = js_node_ChildProcess.spawn("ffmpeg",args,{ cwd : _gthis.cacheDir, stdio : "ignore"});
$process.on("close",function(code) {
+ _gthis.remove(inVideoName);
+ _gthis.remove(inAudioName);
if(code != 0) {
_gthis.log(client,"Error: ffmpeg closed with code " + code);
return;
}
- var inVideo = "" + _gthis.cacheDir + "/input-video";
- var inAudio = "" + _gthis.cacheDir + "/input-audio";
- js_node_Fs.unlinkSync(inVideo);
- js_node_Fs.unlinkSync(inAudio);
_gthis.add(outName);
callback(outName);
});
@@ -3838,6 +3869,8 @@ server_Cache.prototype = {
_gthis.main.send(client,{ type : "Progress", progress : { type : "Downloading", ratio : ratio}});
});
}).catch(function(err) {
+ _gthis.remove(inVideoName);
+ _gthis.remove(inAudioName);
_gthis.log(client,"" + err);
});
}
@@ -3852,7 +3885,7 @@ server_Cache.prototype = {
}
tmp("/",function(err,stats) {
if(err != null) {
- haxe_Log.trace(err,{ fileName : "src/server/Cache.hx", lineNumber : 173, className : "server.Cache", methodName : "setStorageLimit"});
+ haxe_Log.trace(err,{ fileName : "src/server/Cache.hx", lineNumber : 200, className : "server.Cache", methodName : "setStorageLimit"});
return;
}
var a = stats.bsize * stats.bavail - _gthis.freeSpaceBlock;
@@ -3871,6 +3904,10 @@ server_Cache.prototype = {
this.cachedFiles.unshift(name);
}
}
+ ,remove: function(name) {
+ HxOverrides.remove(this.cachedFiles,name);
+ this.removeFile(name);
+ }
,removeOlderCache: function(addFileSize) {
if(addFileSize == null) {
addFileSize = 0;
@@ -3881,20 +3918,25 @@ server_Cache.prototype = {
if(tmp == null) {
break;
}
- var path = this.getFilePath(tmp);
- if(sys_FileSystem.exists(path)) {
- js_node_Fs.unlinkSync(path);
- }
+ this.removeFile(tmp);
space = this.getUsedSpace(addFileSize);
}
}
- ,getFreeFileName: function(baseName) {
- if(baseName == null) {
- baseName = "video";
+ ,removeFile: function(name) {
+ var path = this.getFilePath(name);
+ if(sys_FileSystem.exists(path)) {
+ js_node_Fs.unlinkSync(path);
+ }
+ }
+ ,getFreeFileName: function(fullName) {
+ if(fullName == null) {
+ fullName = "video.mp4";
}
+ var baseName = haxe_io_Path.withoutDirectory(haxe_io_Path.withoutExtension(fullName));
+ var ext = haxe_io_Path.extension(fullName);
var i = 1;
while(true) {
- var name = "" + baseName + (i == 1 ? "" : "" + i) + ".mp4";
+ var name = "" + baseName + (i == 1 ? "" : "" + i) + "." + ext;
if(!this.isFileExists(name)) {
return name;
}
@@ -4131,7 +4173,7 @@ var server_HttpServer = function(main,config) {
this.matchVarString = new EReg("\\${([A-z_]+)}","g");
this.matchLang = new EReg("^[A-z]+","");
this.uploadingFilesLastChunks = new haxe_ds_StringMap();
- this.uploadingFiles = new haxe_ds_StringMap();
+ this.uploadingFilesSizes = new haxe_ds_StringMap();
this.CHUNK_SIZE = 5242880;
this.cache = null;
this.allowLocalRequests = false;
@@ -4158,8 +4200,8 @@ server_HttpServer.prototype = {
}
var filePath = this.getPath(this.dir,url);
var ext = haxe_io_Path.extension(filePath).toLowerCase();
- res.setHeader("Accept-Ranges","bytes");
- res.setHeader("Content-Type",this.getMimeType(ext));
+ res.setHeader("accept-ranges","bytes");
+ res.setHeader("content-type",this.getMimeType(ext));
if(this.cache != null && req.method == "POST") {
switch(url.pathname) {
case "/upload":
@@ -4225,96 +4267,86 @@ server_HttpServer.prototype = {
req.on("end",function() {
var buffer = js_node_buffer_Buffer.concat(body);
_gthis.uploadingFilesLastChunks.h[filePath] = buffer;
- res.writeHead(200,{ "Content-Type" : "application/json"});
- res.end(JSON.stringify({ info : "File last chunk uploaded"}));
+ res.writeHead(200,{ "content-type" : _gthis.getMimeType("json")});
+ var json = { info : "File last chunk uploaded", url : _gthis.cache.getFileUrl(name)};
+ res.end(JSON.stringify(json));
});
}
,uploadFile: function(req,res) {
var _gthis = this;
var name = this.cache.getFreeFileName(req.headers["content-name"]);
- var clientName = req.headers["client-name"];
var filePath = this.cache.getFilePath(name);
- var size;
var tmp = Std.parseInt(req.headers["content-length"]);
- if(tmp != null) {
- size = tmp;
- } else {
+ if(tmp == null) {
return;
}
- var written = 0;
- if(this.cache.getFreeSpace() < size) {
- var _this = _gthis.uploadingFiles;
- if(Object.prototype.hasOwnProperty.call(_this.h,name)) {
- delete(_this.h[name]);
+ if(tmp < this.cache.storageLimit) {
+ this.cache.removeOlderCache(tmp);
+ }
+ if(this.cache.getFreeSpace() < tmp) {
+ res.statusCode = 413;
+ res.end(JSON.stringify({ info : "Error: Not enough free space on server or file size is out of cache storage limit.", errorId : "freeSpace"}));
+ var _this = _gthis.uploadingFilesSizes;
+ if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) {
+ delete(_this.h[filePath]);
}
var _this = _gthis.uploadingFilesLastChunks;
- if(Object.prototype.hasOwnProperty.call(_this.h,name)) {
- delete(_this.h[name]);
+ if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) {
+ delete(_this.h[filePath]);
}
- res.statusCode = 200;
- res.end(JSON.stringify({ info : "Error: Not enough free space on server or file size is out of cache storage limit.", errorId : "freeSpace"}));
+ this.cache.remove(name);
+ req.destroy();
+ var tmp1 = ClientTools.getByName(this.main.clients,name);
+ if(tmp1 == null) {
+ return;
+ }
+ this.main.serverMessage(tmp1,"Error: Not enough free space on server or file size is out of cache storage limit.");
return;
}
var stream = js_node_Fs.createWriteStream(filePath);
req.pipe(stream);
- var isStart = true;
- req.on("data",function(chunk) {
- var url = null;
- if(isStart) {
- isStart = false;
- _gthis.cache.removeOlderCache(size);
- _gthis.cache.add(name);
- _gthis.uploadingFiles.h[filePath] = size;
- url = _gthis.cache.getFileUrl(name);
- }
- written += chunk.length;
- var v = written / size;
- var ratio = v < 0 ? 0 : v > 1 ? 1 : v;
- tools_MathTools.toFixed(ratio * 100,2);
- var tmp = ClientTools.getByName(_gthis.main.clients,clientName);
- if(tmp == null) {
- return;
- }
- _gthis.main.send(tmp,{ type : "Progress", progress : { type : "Uploading", ratio : ratio, data : url}});
- });
+ this.cache.add(name);
+ this.uploadingFilesSizes.h[filePath] = tmp;
stream.on("close",function() {
- var _this = _gthis.uploadingFiles;
- if(Object.prototype.hasOwnProperty.call(_this.h,name)) {
- delete(_this.h[name]);
+ res.statusCode = 200;
+ res.end(JSON.stringify({ info : "File write stream closed."}));
+ var _this = _gthis.uploadingFilesSizes;
+ if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) {
+ delete(_this.h[filePath]);
}
var _this = _gthis.uploadingFilesLastChunks;
- if(Object.prototype.hasOwnProperty.call(_this.h,name)) {
- delete(_this.h[name]);
+ if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) {
+ delete(_this.h[filePath]);
}
- res.statusCode = 200;
- res.end(JSON.stringify({ info : "File write stream closed."}));
});
stream.on("error",function(err) {
- haxe_Log.trace(err,{ fileName : "src/server/HttpServer.hx", lineNumber : 210, className : "server.HttpServer", methodName : "uploadFile"});
- var _this = _gthis.uploadingFiles;
- if(Object.prototype.hasOwnProperty.call(_this.h,name)) {
- delete(_this.h[name]);
+ haxe_Log.trace(err,{ fileName : "src/server/HttpServer.hx", lineNumber : 197, className : "server.HttpServer", methodName : "uploadFile"});
+ res.statusCode = 500;
+ res.end(JSON.stringify({ info : "File write stream error."}));
+ var _this = _gthis.uploadingFilesSizes;
+ if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) {
+ delete(_this.h[filePath]);
}
var _this = _gthis.uploadingFilesLastChunks;
- if(Object.prototype.hasOwnProperty.call(_this.h,name)) {
- delete(_this.h[name]);
+ if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) {
+ delete(_this.h[filePath]);
}
- res.statusCode = 200;
- res.end(JSON.stringify({ info : "File write stream error."}));
+ _gthis.cache.remove(name);
});
req.on("error",function(err) {
- haxe_Log.trace("Request Error:",{ fileName : "src/server/HttpServer.hx", lineNumber : 216, className : "server.HttpServer", methodName : "uploadFile", customParams : [err]});
+ haxe_Log.trace("Request Error:",{ fileName : "src/server/HttpServer.hx", lineNumber : 204, className : "server.HttpServer", methodName : "uploadFile", customParams : [err]});
stream.destroy();
- var _this = _gthis.uploadingFiles;
- if(Object.prototype.hasOwnProperty.call(_this.h,name)) {
- delete(_this.h[name]);
+ res.statusCode = 500;
+ res.end(JSON.stringify({ info : "File request error."}));
+ var _this = _gthis.uploadingFilesSizes;
+ if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) {
+ delete(_this.h[filePath]);
}
var _this = _gthis.uploadingFilesLastChunks;
- if(Object.prototype.hasOwnProperty.call(_this.h,name)) {
- delete(_this.h[name]);
+ if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) {
+ delete(_this.h[filePath]);
}
- res.statusCode = 200;
- res.end(JSON.stringify({ info : "File request error."}));
+ _gthis.cache.remove(name);
});
}
,getPath: function(dir,url) {
@@ -4326,7 +4358,7 @@ server_HttpServer.prototype = {
return haxe_io_Path.addTrailingSlash(filePath) + "index.html";
}
,readFileError: function(err,res,filePath) {
- res.setHeader("Content-Type",this.getMimeType("html"));
+ res.setHeader("content-type",this.getMimeType("html"));
if(err.code == "ENOENT") {
res.statusCode = 404;
var rel = js_node_Path.relative(this.dir,filePath);
@@ -4341,13 +4373,13 @@ server_HttpServer.prototype = {
return false;
}
var videoSize = js_node_Fs.statSync(filePath).size;
- if(Object.prototype.hasOwnProperty.call(this.uploadingFiles.h,filePath)) {
- videoSize = this.uploadingFiles.h[filePath];
+ if(Object.prototype.hasOwnProperty.call(this.uploadingFilesSizes.h,filePath)) {
+ videoSize = this.uploadingFilesSizes.h[filePath];
}
var rangeHeader = req.headers["range"];
if(rangeHeader == null) {
res.statusCode = 200;
- res.setHeader("Content-Length","" + videoSize);
+ res.setHeader("content-length","" + videoSize);
var videoStream = js_node_Fs.createReadStream(filePath);
videoStream.pipe(res);
res.on("error",function() {
@@ -4362,8 +4394,8 @@ server_HttpServer.prototype = {
var start = range.start;
var end = range.end;
var 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;
var buffer = this.uploadingFilesLastChunks.h[filePath];
if(buffer != null && end == videoSize - 1 && contentLength < buffer.byteLength) {
@@ -5209,6 +5241,7 @@ server_Main.prototype = {
result[i] = { name : client1.name, id : client1.id, ip : _gthis.clientIp(client1.req), isBanned : (client1.group & 1) != 0, isAdmin : (client1.group & 8) != 0, isLeader : (client1.group & 4) != 0, isUser : (client1.group & 2) != 0};
}
var json = server_Main.jsonStringify({ state : data1, clients : result, logs : this.logger.getLogs()},"\t");
+ this.send(client,{ type : "ServerMessage", serverMessage : { textId : "Free space: " + tools_MathTools.toFixed(this.cache.getFreeSpace() / 1024) + "KiB"}});
this.send(client,{ type : "Dump", dump : { data : json}});
break;
case "Flashback":
@@ -5298,7 +5331,7 @@ server_Main.prototype = {
this.send(client,{ type : "LoginError"});
return;
}
- haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 607, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " logged as " + name]});
+ haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 606, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " logged as " + name]});
client.name = name;
client.setGroupFlag(ClientGroup.User,true);
this.checkBan(client);
@@ -5311,7 +5344,7 @@ server_Main.prototype = {
var oldName = client.name;
client.name = "Guest " + (this.clients.indexOf(client) + 1);
client.setGroupFlag(ClientGroup.User,false);
- haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 628, className : "server.Main", methodName : "onMessage", customParams : ["Client " + oldName + " logout to " + client.name]});
+ haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 627, className : "server.Main", methodName : "onMessage", customParams : ["Client " + oldName + " logout to " + client.name]});
this.send(client,{ type : data.type, logout : { oldClientName : oldName, clientName : client.name, clients : this.clientList()}});
this.sendClientListExcept(client);
break;
@@ -5373,11 +5406,15 @@ server_Main.prototype = {
if(!this.checkPermission(client,"changeOrder")) {
return;
}
+ var pos = data.playItem.pos;
+ if(!this.videoList.hasItem(pos)) {
+ return;
+ }
if(this.videoTimer.getTime() > 30) {
var _this = this.videoList;
this.saveFlashbackTime(_this.items[_this.pos]);
}
- this.videoList.setPos(data.playItem.pos);
+ this.videoList.setPos(pos);
data.playItem.pos = this.videoList.pos;
this.restartWaitTimer();
this.broadcast(data);
@@ -5408,11 +5445,9 @@ server_Main.prototype = {
this.saveFlashbackTime(_this.items[_this.pos]);
}
this.videoList.removeItem(index);
+ this.broadcast(data);
if(isCurrent && this.videoList.items.length > 0) {
- this.broadcast(data);
this.restartWaitTimer();
- } else {
- this.broadcast(data);
}
break;
case "Rewind":
@@ -5466,6 +5501,9 @@ server_Main.prototype = {
return;
}
var pos = data.setNextItem.pos;
+ if(!this.videoList.hasItem(pos)) {
+ return;
+ }
if(pos == this.videoList.pos || pos == this.videoList.pos + 1) {
return;
}
@@ -5522,7 +5560,11 @@ server_Main.prototype = {
if(!this.checkPermission(client,"toggleItemType")) {
return;
}
- this.videoList.toggleItemType(data.toggleItemType.pos);
+ var pos = data.toggleItemType.pos;
+ if(!this.videoList.hasItem(pos)) {
+ return;
+ }
+ this.videoList.toggleItemType(pos);
this.broadcast(data);
break;
case "TogglePlaylistLock":
@@ -5539,6 +5581,9 @@ server_Main.prototype = {
this.broadcast({ type : "UpdatePlaylist", updatePlaylist : { videoList : this.videoList.items}});
break;
case "VideoLoaded":
+ if(this.isServerPause) {
+ return;
+ }
this.prepareVideoPlayback();
break;
}
@@ -5618,11 +5663,10 @@ server_Main.prototype = {
}
var ip = this.clientIp(client.req);
var currentTime = new Date().getTime();
- var _g = 0;
- var _g1 = this.userList.bans;
- while(_g < _g1.length) {
- var ban = _g1[_g];
- ++_g;
+ var arr = this.userList.bans;
+ var _g_i = arr.length - 1;
+ while(_g_i > -1) {
+ var ban = arr[_g_i--];
if(ban.ip != ip) {
continue;
}
@@ -5630,7 +5674,7 @@ server_Main.prototype = {
client.setGroupFlag(ClientGroup.Banned,!isOutdated);
if(isOutdated) {
HxOverrides.remove(this.userList.bans,ban);
- haxe_Log.trace("" + client.name + " ban removed",{ fileName : "src/server/Main.hx", lineNumber : 1056, className : "server.Main", methodName : "checkBan"});
+ haxe_Log.trace("" + client.name + " ban removed",{ fileName : "src/server/Main.hx", lineNumber : 1062, className : "server.Main", methodName : "checkBan"});
this.sendClientList();
}
break;
@@ -5869,6 +5913,12 @@ server_VideoTimer.prototype = {
this.pauseStartTime = 0;
}
,pause: function() {
+ if(this.isPaused()) {
+ return;
+ }
+ this.updatePauseTime();
+ }
+ ,updatePauseTime: function() {
this.startTime += this.rateTime() - this.rateTime() * this.rate;
var hrtime = process.hrtime();
this.pauseStartTime = hrtime[0] + hrtime[1] / 1e9;
@@ -5896,7 +5946,7 @@ server_VideoTimer.prototype = {
var hrtime = process.hrtime();
this.rateStartTime = hrtime[0] + hrtime[1] / 1e9;
if(this.isPaused()) {
- this.pause();
+ this.updatePauseTime();
}
}
,isPaused: function() {
diff --git a/res/client.js b/res/client.js
index 9629980..a7560d6 100644
--- a/res/client.js
+++ b/res/client.js
@@ -798,41 +798,49 @@ client_Buttons.init = function(main) {
if(name == null || name.length == 0) {
name = "video";
}
- if(buffer.byteLength > 5242880) {
- var lastChunk = buffer.slice(buffer.byteLength - 5242880);
- window.fetch("/upload-last-chunk",{ method : "POST", headers : { "content-name" : haxe_io_Path.withoutExtension(name), "client-name" : main.personal.name}, body : lastChunk});
- }
- var request = window.fetch("/upload",{ method : "POST", headers : { "content-name" : haxe_io_Path.withoutExtension(name), "client-name" : main.personal.name}, body : buffer});
- request.then(function(e) {
+ var a = buffer.byteLength - 5242880;
+ var lastChunk = buffer.slice(a < 0 ? 0 : a);
+ var chunkReq = window.fetch("/upload-last-chunk",{ method : "POST", headers : { "content-name" : name, "client-name" : main.personal.name}, body : lastChunk});
+ chunkReq.then(function(e) {
return e.json().then(function(data) {
- haxe_Log.trace(data.info,{ fileName : "src/client/Buttons.hx", lineNumber : 276, className : "client.Buttons", methodName : "init"});
- if(data.errorId == null) {
+ if(data.errorId != null) {
+ main.serverMessage(data.info,true,false);
return;
}
- main.serverMessage(data.info,true,false);
+ window.document.querySelector("#mediaurl").value = data.url;
});
- }).catch(function(err) {
- haxe_Log.trace(err,{ fileName : "src/client/Buttons.hx", lineNumber : 281, className : "client.Buttons", methodName : "init"});
- return haxe_Timer.delay(function() {
- main.hideDynamicChin();
- },500);
});
- var onStartUpload = null;
- onStartUpload = function(event) {
- if(event.type != "Progress") {
- return;
+ var request = new XMLHttpRequest();
+ request.open("POST","/upload",true);
+ request.setRequestHeader("content-name",name);
+ request.setRequestHeader("client-name",main.personal.name);
+ request.upload.onprogress = function(event) {
+ var ratio = 0.0;
+ if(event.lengthComputable) {
+ var v = event.loaded / event.total;
+ ratio = v < 0 ? 0 : v > 1 ? 1 : v;
}
- var data = event.progress;
- if(data.type != "Uploading") {
+ main.onProgressEvent({ type : "Progress", progress : { type : "Uploading", ratio : ratio}});
+ };
+ request.onload = function(e) {
+ var data;
+ try {
+ data = JSON.parse(request.responseText);
+ } catch( _g ) {
+ haxe_Log.trace(haxe_Exception.caught(_g),{ fileName : "src/client/Buttons.hx", lineNumber : 299, className : "client.Buttons", methodName : "init"});
return;
}
- if(data.data == null) {
+ if(data.errorId == null) {
return;
}
- window.document.querySelector("#mediaurl").value = data.data;
- client_JsApi.off("Progress",onStartUpload);
+ main.serverMessage(data.info,true,false);
+ };
+ request.onloadend = function() {
+ return haxe_Timer.delay(function() {
+ main.hideDynamicChin();
+ },500);
};
- client_JsApi.on("Progress",onStartUpload);
+ request.send(new Blob([buffer]));
});
};
var showOptions = window.document.querySelector("#showoptions");
@@ -1267,18 +1275,6 @@ client_JsApi.hasSubtitleSupport = $hx_exports["client"]["JsApi"]["hasSubtitleSup
client_JsApi.once = $hx_exports["client"]["JsApi"]["once"] = function(type,callback) {
client_JsApi.onceListeners.unshift({ type : type, callback : callback});
};
-client_JsApi.on = function(type,callback) {
- client_JsApi.onListeners.unshift({ type : type, callback : callback});
-};
-client_JsApi.off = function(type,callback) {
- HxOverrides.remove(client_JsApi.onListeners,Lambda.find(client_JsApi.onListeners,function(item) {
- if(item.type == type) {
- return item.callback == callback;
- } else {
- return false;
- }
- }));
-};
client_JsApi.fireEvents = function(event) {
var _g_arr = client_JsApi.onListeners;
var _g_i = _g_arr.length - 1;
@@ -1385,7 +1381,7 @@ client_Main.prototype = {
}
this.gotFirstPageInteraction = true;
this.player.unmute();
- if(!this.hasLeader() && !this.showingServerPause) {
+ if(!this.hasLeader() && !this.showingServerPause && !this.player.inUserInteraction) {
this.player.play();
}
window.document.removeEventListener("click",$bind(this,this.onFirstInteraction));
@@ -1678,7 +1674,6 @@ client_Main.prototype = {
}
}
,onMessage: function(e) {
- var _gthis = this;
var data = JSON.parse(e.data);
if(this.config != null && this.config.isVerbose) {
var t = data.type;
@@ -1830,30 +1825,7 @@ client_Main.prototype = {
this.player.setVideo(data.playItem.pos);
break;
case "Progress":
- var data1 = data.progress;
- var text;
- switch(data1.type) {
- case "Caching":
- text = "" + Lang.get("caching") + " " + data1.data;
- break;
- case "Downloading":
- text = Lang.get("downloading");
- break;
- case "Uploading":
- text = Lang.get("uploading");
- break;
- }
- var percent = tools_MathTools.toFixed(data1.ratio * 100,1);
- var text1 = "" + text + "...";
- if(percent > 0) {
- text1 += " " + percent + "%";
- }
- this.showProgressInfo(text1);
- if(data1.ratio == 1) {
- haxe_Timer.delay(function() {
- _gthis.hideDynamicChin();
- },500);
- }
+ this.onProgressEvent(data);
break;
case "RemoveVideo":
this.player.removeItem(data.removeVideo.url);
@@ -1935,6 +1907,33 @@ client_Main.prototype = {
break;
}
}
+ ,onProgressEvent: function(data) {
+ var _gthis = this;
+ var data1 = data.progress;
+ var text;
+ switch(data1.type) {
+ case "Caching":
+ text = "" + Lang.get("caching") + " " + data1.data;
+ break;
+ case "Downloading":
+ text = Lang.get("downloading");
+ break;
+ case "Uploading":
+ text = Lang.get("uploading");
+ break;
+ }
+ var percent = tools_MathTools.toFixed(data1.ratio * 100,1);
+ var text1 = "" + text + "...";
+ if(percent > 0) {
+ text1 += " " + percent + "%";
+ }
+ this.showProgressInfo(text1);
+ if(data1.ratio == 1) {
+ haxe_Timer.delay(function() {
+ _gthis.hideDynamicChin();
+ },500);
+ }
+ }
,updateLastStateTime: function() {
if(this.lastStateTimeStamp == 0) {
this.lastStateTimeStamp = HxOverrides.now() / 1000;
@@ -2918,7 +2917,14 @@ client_Player.prototype = {
}
var hasAutoPause = this.main.hasLeaderOnPauseRequest();
if((this.main.personal.group & 4) == 0) {
- if(hasAutoPause && this.inUserInteraction || this.main.hasUnpauseWithoutLeader()) {
+ var allowUnpause = hasAutoPause && this.inUserInteraction;
+ if(!allowUnpause) {
+ allowUnpause = this.main.hasUnpauseWithoutLeader();
+ }
+ if(this.getPlaybackRate() != 1) {
+ allowUnpause = false;
+ }
+ if(allowUnpause) {
this.main.removeLeader();
} else if(this.main.lastState.paused) {
this.pause();
@@ -2928,8 +2934,8 @@ client_Player.prototype = {
}
this.main.send({ type : "Play", play : { time : this.getTime()}});
if(hasAutoPause) {
- if(this.main.hasPermission("requestLeader")) {
- this.main.toggleLeader();
+ if(this.main.hasPermission("requestLeader") && this.getPlaybackRate() == 1) {
+ this.main.removeLeader();
}
}
}
@@ -3330,7 +3336,7 @@ client_Player.prototype = {
}
};
http.onError = function(msg) {
- haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 656, className : "client.Player", methodName : "skipAd"});
+ haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 661, className : "client.Player", methodName : "skipAd"});
};
http.request();
}
diff --git a/src/VideoList.hx b/src/VideoList.hx
index c5e92c8..eb3e67d 100644
--- a/src/VideoList.hx
+++ b/src/VideoList.hx
@@ -44,6 +44,10 @@ class VideoList {
pos = i;
}
+ public function hasItem(i:Int):Bool {
+ return items[i] != null;
+ }
+
public function exists(f:(item:VideoItem) -> Bool):Bool {
return items.exists(f);
}
diff --git a/src/client/Buttons.hx b/src/client/Buttons.hx
index 99e12cb..513133a 100644
--- a/src/client/Buttons.hx
+++ b/src/client/Buttons.hx
@@ -1,18 +1,20 @@
package client;
import Types.UploadResponse;
-import Types.WsEvent;
import client.Main.getEl;
+import haxe.Json;
import haxe.Timer;
-import haxe.io.Path;
import js.Browser.document;
import js.Browser.window;
+import js.html.Blob;
import js.html.Element;
import js.html.ImageElement;
import js.html.InputElement;
import js.html.KeyboardEvent;
+import js.html.ProgressEvent;
import js.html.TransitionEvent;
import js.html.VisualViewport;
+import js.html.XMLHttpRequest;
class Buttons {
static var split:Split;
@@ -250,51 +252,63 @@ class Buttons {
// 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", {
+ final bufferOffset = (buffer.byteLength - chunkSize).limitMin(0);
+ final lastChunk = buffer.slice(bufferOffset);
+ final chunkReq = window.fetch("/upload-last-chunk", {
method: "POST",
headers: {
- "content-name": Path.withoutExtension(name),
+ "content-name": name,
"client-name": main.getName(),
},
- body: buffer,
+ body: lastChunk,
});
- request.then(e -> {
+ chunkReq.then(e -> {
e.json().then((data:UploadResponse) -> {
- trace(data.info);
- if (data.errorId == null) return;
- main.serverMessage(data.info, true, false);
+ if (data.errorId != null) {
+ main.serverMessage(data.info, true, false);
+ return;
+ }
+ final input:InputElement = getEl("#mediaurl");
+ input.value = data.url;
+ });
+ });
+
+ final request = new XMLHttpRequest();
+ request.open("POST", "/upload", true);
+ request.setRequestHeader("content-name", name);
+ request.setRequestHeader("client-name", main.getName());
+
+ request.upload.onprogress = (event:ProgressEvent) -> {
+ var ratio = 0.0;
+ if (event.lengthComputable) {
+ ratio = (event.loaded / event.total).clamp(0, 1);
+ }
+ main.onProgressEvent({
+ type: Progress,
+ progress: {
+ type: Uploading,
+ ratio: ratio
+ }
});
- }).catchError(err -> {
- trace(err);
+ }
+
+ request.onload = (e:ProgressEvent) -> {
+ final data:UploadResponse = try {
+ Json.parse(request.responseText);
+ } catch (e) {
+ trace(e);
+ return;
+ }
+ if (data.errorId == null) return;
+ main.serverMessage(data.info, true, false);
+ }
+ request.onloadend = () -> {
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 = getEl("#mediaurl");
- input.value = data.data;
- JsApi.off(Progress, onStartUpload);
}
- JsApi.on(Progress, onStartUpload);
+
+ request.send(new Blob([buffer]));
});
}
diff --git a/src/client/Main.hx b/src/client/Main.hx
index 84f6838..5c4b28d 100644
--- a/src/client/Main.hx
+++ b/src/client/Main.hx
@@ -120,7 +120,7 @@ class Main {
if (!player.isVideoLoaded()) return;
gotFirstPageInteraction = true;
player.unmute();
- if (!hasLeader() && !showingServerPause) player.play();
+ if (!hasLeader() && !showingServerPause && !player.inUserInteraction) player.play();
document.removeEventListener("click", onFirstInteraction);
}
@@ -508,24 +508,7 @@ 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);
- }
+ onProgressEvent(data);
case AddVideo:
player.addVideoItem(data.addVideo.item, data.addVideo.atEnd);
@@ -675,6 +658,27 @@ class Main {
}
}
+ public function onProgressEvent(data:WsEvent):Void {
+ 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);
+ }
+ }
+
function updateLastStateTime():Void {
if (lastStateTimeStamp == 0) {
lastStateTimeStamp = Timer.stamp();
diff --git a/src/client/Player.hx b/src/client/Player.hx
index e5ea87c..64248fe 100644
--- a/src/client/Player.hx
+++ b/src/client/Player.hx
@@ -279,8 +279,11 @@ class Player {
if (!main.isLeader()) {
// user click, so we can unpause by removing leader
// (doesn't work in Firefox because of no video click propagation)
- final allowUnpause = (hasAutoPause && inUserInteraction);
- if (allowUnpause || main.hasUnpauseWithoutLeader()) {
+ var allowUnpause = hasAutoPause && inUserInteraction;
+ if (!allowUnpause) allowUnpause = main.hasUnpauseWithoutLeader();
+ // do not remove leader with custom rate
+ if (getPlaybackRate() != 1) allowUnpause = false;
+ if (allowUnpause) {
main.removeLeader();
} else {
// paused and no leader - instant pause
@@ -299,7 +302,9 @@ class Player {
});
if (hasAutoPause) {
// do not remove leader if user cannot request it back
- if (main.hasPermission(RequestLeaderPerm)) main.toggleLeader();
+ if (main.hasPermission(RequestLeaderPerm) && getPlaybackRate() == 1) {
+ main.removeLeader();
+ }
}
}
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 = 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 = 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 = [];
+ // temp media data while file is uploading to allow instant streaming
+ final uploadingFilesSizes:Map = [];
final uploadingFilesLastChunks:Map = [];
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);
@@ -941,6 +941,12 @@ class Main {
logs: logger.getLogs()
}
final json = jsonStringify(data, "\t");
+ send(client, {
+ type: ServerMessage,
+ serverMessage: {
+ textId: "Free space: " + (cache.getFreeSpace() / 1024).toFixed() + "KiB"
+ }
+ });
send(client, {
type: Dump,
dump: {
@@ -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 {
diff --git a/test/tests/TestServer.hx b/test/tests/TestServer.hx
index abdbf8f..b02bb90 100644
--- a/test/tests/TestServer.hx
+++ b/test/tests/TestServer.hx
@@ -67,10 +67,6 @@ class TestServer extends Test {
server.onServerInited = () -> {
final client = new FakeClient(server.localIp, server.port);
var client2:FakeClient = null;
- // client.ws.on("message", data -> {
- // final data:WsEvent = Json.parse(data);
- // trace(data.type);
- // });
client.message().then((data) -> {
Assert.equals(Connected, data.type);
diff --git a/test/tests/TestTimer.hx b/test/tests/TestTimer.hx
index 1261553..2e6e9e8 100644
--- a/test/tests/TestTimer.hx
+++ b/test/tests/TestTimer.hx
@@ -136,6 +136,18 @@ class TestTimer extends Test {
}, 400);
}
+ @:timeout(500)
+ function testDoublePause(async:Async) {
+ final timer = new VideoTimer();
+ timer.start();
+ timer.pause();
+ Timer.delay(() -> {
+ timer.pause();
+ almostEq(0, timer.getTime());
+ async.done();
+ }, 300);
+ }
+
@:timeout(500)
function testBigRate(async:Async) {
final timer = new VideoTimer();