From 5f2a3b89eaa1199d9bc2ddd10622f9803cec983f Mon Sep 17 00:00:00 2001 From: RblSb Date: Fri, 3 Apr 2020 04:48:36 +0300 Subject: Sync playback rate --- .vscode/tasks.json | 21 ++++++ build-server.hxml | 1 + build/server.js | 47 +++++++++++- res/client.js | 66 +++++++++++++++++ src/Types.hx | 7 +- src/client/IPlayer.hx | 2 + src/client/Main.hx | 11 +++ src/client/Player.hx | 26 +++++++ src/client/players/Iframe.hx | 6 ++ src/client/players/Raw.hx | 9 +++ src/client/players/Youtube.hx | 13 +++- src/server/Main.hx | 21 ++++-- src/server/VideoTimer.hx | 36 ++++++++-- test/Main.hx | 15 ++++ test/tests/TestTimer.hx | 163 ++++++++++++++++++++++++++++++++++++++++++ tests.hxml | 6 ++ 16 files changed, 436 insertions(+), 14 deletions(-) create mode 100644 test/Main.hx create mode 100644 test/tests/TestTimer.hx create mode 100644 tests.hxml diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f68272e..0b315f6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -16,6 +16,27 @@ "group": { "kind": "build", "isDefault": true + }, + "presentation": { + "clear": true + } + }, + { + "label": "Run tests", + "type": "hxml", + "file": "tests.hxml", + "problemMatcher": [ + "$haxe-absolute", + "$haxe", + "$haxe-error", + "$haxe-trace" + ], + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "clear": true } } ] diff --git a/build-server.hxml b/build-server.hxml index 5f181aa..49def02 100644 --- a/build-server.hxml +++ b/build-server.hxml @@ -2,6 +2,7 @@ -lib hxnodejs-ws # Client libs for completion -lib youtubeIFramePlayer:git:https://github.com/okawa-h/youtubeIFramePlayer-externs.git +-lib utest -cp src --main server.Main -D analyzer-optimize diff --git a/build/server.js b/build/server.js index 5641bd5..5077240 100644 --- a/build/server.js +++ b/build/server.js @@ -1393,7 +1393,17 @@ server_Main.prototype = { this.onMessage(client,{ type : "SkipVideo", skipVideo : { url : this.videoList[this.itemPos].url}}); return; } - this.send(client,{ type : "GetTime", getTime : { time : this.videoTimer.getTime(), paused : this.videoTimer.isPaused()}}); + var obj = { type : "GetTime", getTime : { time : this.videoTimer.getTime()}}; + if(this.videoTimer.isPaused()) { + obj.getTime.paused = true; + } + if(this.videoTimer.getRate() != 1) { + if(!ClientTools.hasLeader(this.clients)) { + this.videoTimer.setRate(1); + } + obj.getTime.rate = this.videoTimer.getRate(); + } + this.send(client,obj); break; case "Login": var name = data.login.clientName; @@ -1551,6 +1561,7 @@ server_Main.prototype = { if(this.videoTimer.isPaused()) { this.videoTimer.play(); } + this.videoTimer.setRate(1); this.broadcast({ type : "Play", play : { time : this.videoTimer.getTime()}}); } break; @@ -1565,6 +1576,16 @@ server_Main.prototype = { _$VideoList_VideoList_$Impl_$.setNextItem(this.videoList,pos,this.itemPos); this.broadcast(data); break; + case "SetRate": + if(this.videoList.length == 0) { + return; + } + if((client.group & 2) == 0) { + return; + } + this.videoTimer.setRate(data.setRate.rate); + this.broadcastExcept(client,data); + break; case "SetTime": if(this.videoList.length == 0) { return; @@ -1775,6 +1796,8 @@ server_Utils.shuffle = function(arr) { } }; var server_VideoTimer = function() { + this.rate = 1.0; + this.rateStartTime = 0.0; this.pauseStartTime = 0.0; this.startTime = 0.0; this.isStarted = false; @@ -1785,6 +1808,7 @@ server_VideoTimer.prototype = { this.isStarted = true; this.startTime = Date.now() / 1000; this.pauseStartTime = 0; + this.rateStartTime = Date.now() / 1000; } ,stop: function() { this.isStarted = false; @@ -1792,20 +1816,23 @@ server_VideoTimer.prototype = { this.pauseStartTime = 0; } ,pause: function() { + this.startTime += this.rateTime() - this.rateTime() * this.rate; this.pauseStartTime = Date.now() / 1000; + this.rateStartTime = 0; } ,play: function() { if(!this.isStarted) { this.start(); } this.startTime += this.pauseTime(); + this.rateStartTime = Date.now() / 1000; this.pauseStartTime = 0; } ,getTime: function() { if(this.startTime == 0) { return 0; } - return Date.now() / 1000 - this.startTime - this.pauseTime(); + return Date.now() / 1000 - this.startTime - this.rateTime() + this.rateTime() * this.rate - this.pauseTime(); } ,setTime: function(secs) { this.startTime = Date.now() / 1000 - secs; @@ -1820,12 +1847,28 @@ server_VideoTimer.prototype = { return true; } } + ,getRate: function() { + return this.rate; + } + ,setRate: function(rate) { + if(!this.isPaused()) { + this.startTime += this.rateTime() - this.rateTime() * this.rate; + this.rateStartTime = Date.now() / 1000; + } + this.rate = rate; + } ,pauseTime: function() { if(this.pauseStartTime == 0) { return 0; } return Date.now() / 1000 - this.pauseStartTime; } + ,rateTime: function() { + if(this.rateStartTime == 0) { + return 0; + } + return Date.now() / 1000 - this.rateStartTime - this.pauseTime(); + } }; var sys_FileSystem = function() { }; sys_FileSystem.__name__ = true; diff --git a/res/client.js b/res/client.js index 0611166..6519d6c 100644 --- a/res/client.js +++ b/res/client.js @@ -1091,6 +1091,15 @@ client_Main.prototype = { this.onTimeGet.run(); break; case "GetTime": + if(data.getTime.paused == null) { + data.getTime.paused = false; + } + if(data.getTime.rate == null) { + data.getTime.rate = 1; + } + if(this.player.getPlaybackRate() != data.getTime.rate) { + this.player.setPlaybackRate(data.getTime.rate); + } var newTime = data.getTime.time; var time = this.player.getTime(); if((this.personal.group & 1 << ClientGroup.Leader._hx_index) != 0) { @@ -1175,6 +1184,12 @@ client_Main.prototype = { case "SetNextItem": this.player.setNextItem(data.setNextItem.pos); break; + case "SetRate": + if((this.personal.group & 1 << ClientGroup.Leader._hx_index) != 0) { + return; + } + this.player.setPlaybackRate(data.setRate.rate); + break; case "SetTime": var newTime1 = data.setTime.time; var time1 = this.player.getTime(); @@ -1543,6 +1558,7 @@ client_MobileView.init = function() { }; }; var client_Player = function(main) { + this.skipSetRate = false; this.skipSetTime = false; this.isLoaded = false; this.itemPos = 0; @@ -1653,6 +1669,16 @@ client_Player.prototype = { } this.main.send({ type : "SetTime", setTime : { time : this.getTime()}}); } + ,onRateChange: function() { + if(this.skipSetRate) { + this.skipSetRate = false; + return; + } + if((this.main.personal.group & 2) == 0) { + return; + } + this.main.send({ type : "SetRate", setRate : { rate : this.getPlaybackRate()}}); + } ,addVideoItem: function(item,atEnd) { var url = StringTools.htmlEscape(item.url,true); var itemEl = this.nodeFromString("
  • \n\t\t\t\t" + StringTools.htmlEscape(item.title) + "\n\t\t\t\t" + this.duration(item.duration) + "\n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t
  • "); @@ -1833,6 +1859,25 @@ client_Player.prototype = { this.skipSetTime = isLocal; this.player.setTime(time); } + ,getPlaybackRate: function() { + if(this.player == null) { + return 1; + } + return this.player.getPlaybackRate(); + } + ,setPlaybackRate: function(rate,isLocal) { + if(isLocal == null) { + isLocal = true; + } + if(!this.main.isSyncActive) { + return; + } + if(this.player == null) { + return; + } + this.skipSetRate = isLocal; + this.player.setPlaybackRate(rate); + } }; var client_Settings = function() { }; client_Settings.__name__ = true; @@ -1982,6 +2027,11 @@ client_players_Iframe.prototype = { } ,setTime: function(time) { } + ,getPlaybackRate: function() { + return 1; + } + ,setPlaybackRate: function(rate) { + } }; var client_players_Raw = function(main,player) { this.playAllowed = true; @@ -2047,6 +2097,7 @@ client_players_Raw.prototype = { return; }; this.video.onpause = ($_=this.player,$bind($_,$_.onPause)); + this.video.onratechange = ($_=this.player,$bind($_,$_.onRateChange)); this.playerEl.appendChild(this.video); } ,removeVideo: function() { @@ -2090,6 +2141,12 @@ client_players_Raw.prototype = { } this.video.currentTime = time; } + ,getPlaybackRate: function() { + return this.video.playbackRate; + } + ,setPlaybackRate: function(rate) { + this.video.playbackRate = rate; + } }; var client_players_Youtube = function(main,player) { this.matchSeconds = new EReg("([0-9]+)S",""); @@ -2282,6 +2339,9 @@ client_players_Youtube.prototype = { break; } return; + }, onPlaybackRateChange : function(e2) { + _gthis.player.onRateChange(); + return; }}}); } ,removeVideo: function() { @@ -2317,6 +2377,12 @@ client_players_Youtube.prototype = { } this.youtube.seekTo(time,true); } + ,getPlaybackRate: function() { + return this.youtube.getPlaybackRate(); + } + ,setPlaybackRate: function(rate) { + this.youtube.setPlaybackRate(rate); + } }; var haxe_Log = function() { }; haxe_Log.__name__ = true; diff --git a/src/Types.hx b/src/Types.hx index 2501cc4..366b2ed 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -134,11 +134,15 @@ typedef WsEvent = { }, ?getTime:{ time:Float, - paused:Bool + ?paused:Bool, + ?rate:Float }, ?setTime:{ time:Float }, + ?setRate:{ + rate:Float + }, ?rewind:{ time:Float }, @@ -181,6 +185,7 @@ enum abstract WsEventType(String) { var Play; var GetTime; var SetTime; + var SetRate; var Rewind; var SetLeader; var PlayItem; diff --git a/src/client/IPlayer.hx b/src/client/IPlayer.hx index 4f29512..e414af5 100644 --- a/src/client/IPlayer.hx +++ b/src/client/IPlayer.hx @@ -11,4 +11,6 @@ interface IPlayer { function pause():Void; function getTime():Float; function setTime(time:Float):Void; + function getPlaybackRate():Float; + function setPlaybackRate(rate:Float):Void; } diff --git a/src/client/Main.hx b/src/client/Main.hx index f65640a..780f005 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -366,6 +366,13 @@ class Main { player.play(); case GetTime: + if (data.getTime.paused == null) data.getTime.paused = false; + if (data.getTime.rate == null) data.getTime.rate = 1; + + if (player.getPlaybackRate() != data.getTime.rate) { + player.setPlaybackRate(data.getTime.rate); + } + final newTime = data.getTime.time; final time = player.getTime(); if (isLeader()) { @@ -386,6 +393,10 @@ class Main { if (Math.abs(time - newTime) < synchThreshold) return; player.setTime(newTime); + case SetRate: + if (isLeader()) return; + player.setPlaybackRate(data.setRate.rate); + case Rewind: player.setTime(data.rewind.time); diff --git a/src/client/Player.hx b/src/client/Player.hx index 8e55b25..53a1168 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -21,6 +21,7 @@ class Player { var itemPos = 0; var isLoaded = false; var skipSetTime = false; + var skipSetRate = false; public function new(main:Main):Void { this.main = main; @@ -158,6 +159,19 @@ class Player { }); } + public function onRateChange():Void { + if (skipSetRate) { + skipSetRate = false; + return; + } + if (!main.isLeader()) return; + main.send({ + type: SetRate, setRate: { + rate: getPlaybackRate() + } + }); + } + public function addVideoItem(item:VideoItem, atEnd:Bool):Void { final url = item.url.htmlEscape(true); final itemEl = nodeFromString( @@ -322,4 +336,16 @@ class Player { player.setTime(time); } + public function getPlaybackRate():Float { + if (player == null) return 1; + return player.getPlaybackRate(); + } + + public function setPlaybackRate(rate:Float, isLocal = true):Void { + if (!main.isSyncActive) return; + if (player == null) return; + skipSetRate = isLocal; + player.setPlaybackRate(rate); + } + } diff --git a/src/client/players/Iframe.hx b/src/client/players/Iframe.hx index f0a04c5..d79196b 100644 --- a/src/client/players/Iframe.hx +++ b/src/client/players/Iframe.hx @@ -59,4 +59,10 @@ class Iframe implements IPlayer { public function setTime(time:Float):Void {} + public function getPlaybackRate():Float { + return 1; + } + + public function setPlaybackRate(rate:Float):Void {} + } diff --git a/src/client/players/Raw.hx b/src/client/players/Raw.hx index c7fa980..cd01a42 100644 --- a/src/client/players/Raw.hx +++ b/src/client/players/Raw.hx @@ -67,6 +67,7 @@ class Raw implements IPlayer { player.onPlay(); } video.onpause = player.onPause; + video.onratechange = player.onRateChange; playerEl.appendChild(video); } @@ -102,4 +103,12 @@ class Raw implements IPlayer { video.currentTime = time; } + public function getPlaybackRate():Float { + return video.playbackRate; + } + + public function setPlaybackRate(rate:Float):Void { + video.playbackRate = rate; + } + } diff --git a/src/client/players/Youtube.hx b/src/client/players/Youtube.hx index f582ba7..eff407e 100644 --- a/src/client/players/Youtube.hx +++ b/src/client/players/Youtube.hx @@ -208,7 +208,10 @@ class Youtube implements IPlayer { player.onSetTime(); case CUED: } - } + }, + onPlaybackRateChange: e -> { + player.onRateChange(); + }, } }); } @@ -239,4 +242,12 @@ class Youtube implements IPlayer { youtube.seekTo(time, true); } + public function getPlaybackRate():Float { + return youtube.getPlaybackRate(); + } + + public function setPlaybackRate(rate:Float):Void { + youtube.setPlaybackRate(rate); + } + } diff --git a/src/server/Main.hx b/src/server/Main.hx index f056ac6..5142f48 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -438,11 +438,17 @@ class Main { }); return; } - send(client, { + final obj:WsEvent = { type: GetTime, getTime: { - time: videoTimer.getTime(), - paused: videoTimer.isPaused() - }}); + time: videoTimer.getTime() + } + }; + if (videoTimer.isPaused()) obj.getTime.paused = true; + if (videoTimer.getRate() != 1) { + if (!clients.hasLeader()) videoTimer.setRate(1); + obj.getTime.rate = videoTimer.getRate(); + } + send(client, obj); case SetTime: if (videoList.length == 0) return; @@ -450,6 +456,12 @@ class Main { videoTimer.setTime(data.setTime.time); broadcastExcept(client, data); + case SetRate: + if (videoList.length == 0) return; + if (!client.isLeader) return; + videoTimer.setRate(data.setRate.rate); + broadcastExcept(client, data); + case Rewind: if (!checkPermission(client, RewindPerm)) return; if (videoList.length == 0) return; @@ -474,6 +486,7 @@ class Main { if (videoList.length == 0) return; if (!clients.hasLeader()) { if (videoTimer.isPaused()) videoTimer.play(); + videoTimer.setRate(1); broadcast({ type: Play, play: { time: videoTimer.getTime() diff --git a/src/server/VideoTimer.hx b/src/server/VideoTimer.hx index 4bc29db..508b97b 100644 --- a/src/server/VideoTimer.hx +++ b/src/server/VideoTimer.hx @@ -1,19 +1,22 @@ package server; -import haxe.Timer; +import haxe.Timer.stamp; class VideoTimer { public var isStarted(default, null) = false; var startTime = 0.0; var pauseStartTime = 0.0; + var rateStartTime = 0.0; + var rate = 1.0; public function new() {} public function start():Void { isStarted = true; - startTime = Timer.stamp(); + startTime = stamp(); pauseStartTime = 0; + rateStartTime = stamp(); } public function stop():Void { @@ -23,22 +26,26 @@ class VideoTimer { } public function pause():Void { - pauseStartTime = Timer.stamp(); + startTime += rateTime() - rateTime() * this.rate; + pauseStartTime = stamp(); + rateStartTime = 0; } public function play():Void { if (!isStarted) start(); startTime += pauseTime(); + rateStartTime = stamp(); pauseStartTime = 0; } public function getTime():Float { if (startTime == 0) return 0; - return Timer.stamp() - startTime - pauseTime(); + final time = stamp() - startTime; + return time - rateTime() + rateTime() * rate - pauseTime(); } public function setTime(secs:Float):Void { - startTime = Timer.stamp() - secs; + startTime = stamp() - secs; if (isPaused()) pause(); } @@ -46,9 +53,26 @@ class VideoTimer { return !isStarted || pauseStartTime != 0; } + public function getRate():Float { + return rate; + } + + public function setRate(rate:Float):Void { + if (!isPaused()) { + startTime += rateTime() - rateTime() * this.rate; + rateStartTime = stamp(); + } + this.rate = rate; + } + function pauseTime():Float { if (pauseStartTime == 0) return 0; - return Timer.stamp() - pauseStartTime; + return stamp() - pauseStartTime; + } + + function rateTime():Float { + if (rateStartTime == 0) return 0; + return stamp() - rateStartTime - pauseTime(); } } diff --git a/test/Main.hx b/test/Main.hx new file mode 100644 index 0000000..d2ea286 --- /dev/null +++ b/test/Main.hx @@ -0,0 +1,15 @@ +package; + +import utest.Runner; +import utest.ui.Report; + +class Main { + + static function main() { + final runner = new Runner(); + runner.addCases(test.tests); + Report.create(runner); + runner.run(); + } + +} diff --git a/test/tests/TestTimer.hx b/test/tests/TestTimer.hx new file mode 100644 index 0000000..9403b75 --- /dev/null +++ b/test/tests/TestTimer.hx @@ -0,0 +1,163 @@ +package test.tests; + +import haxe.PosInfos; +import utest.Assert; +import haxe.Timer; +import server.VideoTimer; + +class TestTimer extends utest.Test { + + @:timeout(500) + function testMain(async:utest.Async) { + final timer = new VideoTimer(); + timer.start(); + Timer.delay(() -> { + almostEq(0.1, timer.getTime()); + timer.setTime(1); + almostEq(1, timer.getTime()); + }, 100); + Timer.delay(() -> { + almostEq(1.1, timer.getTime()); + timer.setTime(0.1); + almostEq(0.1, timer.getTime()); + }, 200); + Timer.delay(() -> { + almostEq(0.2, timer.getTime()); + Assert.equals(false, timer.isPaused()); + Assert.equals(true, timer.isStarted); + timer.stop(); + Assert.equals(0, timer.getTime()); + Assert.equals(true, timer.isPaused()); + Assert.equals(false, timer.isStarted); + async.done(); + }, 300); + } + + @:timeout(500) + function testRate(async:utest.Async) { + final timer = new VideoTimer(); + timer.start(); + timer.setRate(2); + almostEq(0, timer.getTime()); + Timer.delay(() -> { + almostEq(0.2, timer.getTime()); + timer.setRate(1); + almostEq(0.2, timer.getTime()); + }, 100); + Timer.delay(() -> { + almostEq(0.3, timer.getTime()); + timer.setRate(2); + almostEq(0.3, timer.getTime()); + }, 200); + Timer.delay(() -> { + almostEq(0.5, timer.getTime()); + timer.pause(); + almostEq(0.5, timer.getTime()); + Assert.equals(true, timer.isPaused()); + Assert.equals(true, timer.isStarted); + }, 300); + Timer.delay(() -> { + almostEq(0.5, timer.getTime()); + Assert.equals(true, timer.isPaused()); + Assert.equals(true, timer.isStarted); + async.done(); + }, 400); + } + + @:timeout(500) + function testRatePause(async:utest.Async) { + final timer = new VideoTimer(); + timer.start(); + timer.setRate(2); + timer.setTime(1); + almostEq(1, timer.getTime()); + Timer.delay(() -> { + almostEq(1.2, timer.getTime()); + timer.pause(); + almostEq(1.2, timer.getTime()); + }, 100); + Timer.delay(() -> { + almostEq(1.2, timer.getTime()); + timer.play(); + almostEq(1.2, timer.getTime()); + }, 200); + Timer.delay(() -> { + almostEq(1.4, timer.getTime()); + timer.pause(); + almostEq(1.4, timer.getTime()); + timer.setRate(3); + }, 300); + Timer.delay(() -> { + almostEq(1.4, timer.getTime()); + timer.play(); + almostEq(1.4, timer.getTime()); + timer.setRate(1); + almostEq(1.4, timer.getTime()); + async.done(); + }, 400); + } + + @:timeout(500) + function testPauseRate(async:utest.Async) { + final timer = new VideoTimer(); + timer.start(); + timer.setTime(100); + timer.pause(); + Timer.delay(() -> { + almostEq(100, timer.getTime()); + timer.setRate(2); + almostEq(100, timer.getTime()); + }, 100); + Timer.delay(() -> { + almostEq(100, timer.getTime()); + timer.setRate(1); + almostEq(100, timer.getTime()); + }, 200); + Timer.delay(() -> { + almostEq(100, timer.getTime()); + timer.setRate(2); + almostEq(100, timer.getTime()); + timer.play(); + almostEq(100, timer.getTime()); + }, 300); + Timer.delay(() -> { + almostEq(100.2, timer.getTime()); + timer.setRate(1); + almostEq(100.2, timer.getTime()); + async.done(); + }, 400); + } + + @:timeout(500) + function testBigRate(async:utest.Async) { + final timer = new VideoTimer(); + timer.start(); + timer.setRate(3); + timer.setTime(10); + almostEq(10, timer.getTime()); + Timer.delay(() -> { + almostEq(10.3, timer.getTime()); + }, 100); + Timer.delay(() -> { + almostEq(10.6, timer.getTime()); + timer.pause(); + almostEq(10.6, timer.getTime()); + }, 200); + Timer.delay(() -> { + almostEq(10.6, timer.getTime()); + timer.play(); + almostEq(10.6, timer.getTime()); + }, 300); + Timer.delay(() -> { + almostEq(10.9, timer.getTime()); + timer.setRate(1); + almostEq(10.9, timer.getTime()); + async.done(); + }, 400); + } + + function almostEq(a:Float, b:Float, ?p:PosInfos):Void { + Assert.equals(Math.round(a * 10) / 10, Math.round(b * 10) / 10, p); + } + +} diff --git a/tests.hxml b/tests.hxml new file mode 100644 index 0000000..c95f9d8 --- /dev/null +++ b/tests.hxml @@ -0,0 +1,6 @@ +-cp src +-cp test +--main Main +-lib utest +--interp +#-D test=9133 -- cgit v1.2.3