From 3f660239ec4651a297a19e7fa80393925ed56df0 Mon Sep 17 00:00:00 2001 From: blue Date: Sat, 26 Jan 2019 21:54:22 +0300 Subject: [PATCH] seeking, autonext song --- lib/wModel/file/audio.cpp | 2 +- libjs/utils/globalMethods.js | 35 ++++++ libjs/wController/file/audio.js | 2 +- libjs/wController/player.js | 199 ++++++++++++++++++++++++++---- lorgar/views/helpers/draggable.js | 47 +------ lorgar/views/songProgress.js | 59 +++++++++ lorgar/views/view.js | 16 +++ 7 files changed, 294 insertions(+), 66 deletions(-) diff --git a/lib/wModel/file/audio.cpp b/lib/wModel/file/audio.cpp index a46d754..d018231 100644 --- a/lib/wModel/file/audio.cpp +++ b/lib/wModel/file/audio.cpp @@ -40,6 +40,6 @@ void M::Audio::initAdditional(const W::String& p_mime) } } - additional.insert(u"duration", new W::Uint64(length / MAD_TIMER_RESOLUTION)); + additional.insert(u"duration", new W::Uint64(length * 1000 / MAD_TIMER_RESOLUTION)); additional.insert(u"bitrate", new W::Uint64(tBits / amount)); } diff --git a/libjs/utils/globalMethods.js b/libjs/utils/globalMethods.js index c104d9a..da47d1e 100644 --- a/libjs/utils/globalMethods.js +++ b/libjs/utils/globalMethods.js @@ -65,5 +65,40 @@ global.W = { // Return the modified object return lTarget; + }, + touchToMouse: function (e) { + e.preventDefault(); + if (e.touches.length > 1 || (e.type == "touchend" && e.touches.length > 0)) + return; + + var type = null; + var touch = null; + var src = e.currentTarget; + switch (e.type) { + case "touchstart": + type = "mousedown"; + touch = e.changedTouches[0]; + + break; + case "touchmove": + type = "mousemove"; + touch = e.changedTouches[0]; + src = window; + break; + case "touchend": + type = "mouseup"; + touch = e.changedTouches[0]; + src = window; + break; + } + + var event = new MouseEvent(type, { + button: 0, + screenX: touch.screenX, + screenY: touch.screenY, + clientX: touch.clientX, + clientY: touch.clientY + }); + src.dispatchEvent(event); } }; diff --git a/libjs/wController/file/audio.js b/libjs/wController/file/audio.js index 9d8474e..77b4d1b 100644 --- a/libjs/wController/file/audio.js +++ b/libjs/wController/file/audio.js @@ -17,7 +17,7 @@ var Audio = File.inherit({ return this._additional.at("bitrate").valueOf(); }, getDuration: function() { - return this._additional.at("duration").valueOf(); + return this._additional.at("duration").valueOf() / 1000; } }); diff --git a/libjs/wController/player.js b/libjs/wController/player.js index e434890..f4f3e8c 100644 --- a/libjs/wController/player.js +++ b/libjs/wController/player.js @@ -22,6 +22,9 @@ var Player = Controller.inherit({ this.views = Object.create(null); this.mode = PlayerMode.straight.playback; this.progress = new ProgressModel(); + this.progress.on("seekingStart", this._onSeekingStart, this); + this.progress.on("seekingEnd", this._onSeekingEnd, this); + this.progress.on("seek", this._onSeek, this); this._audio = null; this._createStateMachine(); this._createPlayingInfrastructure(); @@ -31,7 +34,7 @@ var Player = Controller.inherit({ this.addHandler("play"); this.addHandler("pause"); - this._playbackInterval = setInterval(this._onInterval.bind(this), 250); + this._playbackInterval = setInterval(this._onInterval.bind(this), intervalPrecision); }, destructor: function() { this._clearInterval(this._playbackInterval); @@ -82,8 +85,8 @@ var Player = Controller.inherit({ break; case ItemType.straight.currentPlayback: ctrl = new Vocabulary(address.clone()); - ctrl.on("newElement", this._onNewPlayBackElement, this); - ctrl.on("removeElement", this._onNewRemoveBackElement, this); + ctrl.on("newElement", this._onNewPlaybackElement, this); + ctrl.on("removeElement", this._onRemovePlaybackElement, this); supported = true; break; case ItemType.straight.picture: @@ -109,20 +112,34 @@ var Player = Controller.inherit({ this.trigger("newElement", ctrl, t); } }, + _checkIfEnough: function() { + var diff = this._currentTime - this._seekingTime - this._ctx.currentTime; + if (diff > threshold) { + this._fsm.manipulation("enough"); + } else { + this._fsm.manipulation("notEnough"); + } + }, _createPlayingInfrastructure: function() { this._ctx = new AudioContext(); this._decoder = new Mp3Decoder(); this._currentTime = 0; + this._seekingTime = 0; + this._buffers = []; + this._sources = []; this._ctx.suspend(); }, + _createStateMachine: function() { + this._fsm = new StateMachine("initial", graphs[this.mode]); + this._fsm.on("stateChanged", this._onStateChanged, this); + }, _destroyPlayingInfrastructure: function() { this._ctx.close(); this._decoder.delete(); }, - _createStateMachine: function() { - this._fsm = new StateMachine("initial", graphs[this.mode]); - this._fsm.on("stateChanged", this._onStateChanged, this); + getCurrentPlaybackTime: function() { + return this._ctx.currentTime + this._seekingTime; }, _h_get: function(ev) { var data = ev.getData(); @@ -187,10 +204,26 @@ var Player = Controller.inherit({ if (sb === undefined) { break; } else { - var src = this._ctx.createBufferSource(); - src.buffer = sb; - src.connect(this._ctx.destination); - src.start(this._currentTime); + this._buffers.push(sb); + + var startTime = this._currentTime - this._seekingTime; + if (startTime < this._ctx.currentTime) { + var offset = startTime - this._ctx.currentTime + sb.duration; + if (offset > 0) { + var src = this._ctx.createBufferSource(); + src.buffer = sb; + src.connect(this._ctx.destination); + src.start(0, Math.abs(startTime - this._ctx.currentTime)); + this._sources.push(src); + } + } else { + var src = this._ctx.createBufferSource(); + src.buffer = sb; + src.connect(this._ctx.destination); + src.start(startTime); + this._sources.push(src); + } + this._currentTime += sb.duration; } } @@ -208,11 +241,22 @@ var Player = Controller.inherit({ this._fsm.manipulation("controllerReady"); }, _onInterval: function() { - if (this._audio && this._audio.initialized) { - this.progress.setPlayback(this._ctx.currentTime / this._audio.getDuration()); + if (this._audio && this._audio.initialized && seekingStates.indexOf(this._fsm.state()) === -1) { + var duration = this._audio.getDuration(); + this.progress.setPlayback(this.getCurrentPlaybackTime() / duration); + this._checkIfEnough(); + + if (this.progress.playback >= 0.9999) { + var next = this.controls[ItemType.straight.next]; + if (next && next.enabled) { + next.activate(); + } else { + //todo kinda stop state? + } + } } }, - _onNewPlayBackElement: function(key, element) { + _onNewPlaybackElement: function(key, element) { switch (key) { case "image": var address = new Address(["images", element.toString()]); @@ -230,7 +274,7 @@ var Player = Controller.inherit({ break; } }, - _onNewRemoveBackElement: function(key) { + _onRemovePlaybackElement: function(key) { switch (key) { case "image": this._removeView(ItemType.straight.picture); @@ -241,6 +285,53 @@ var Player = Controller.inherit({ this._audio = null; } }, + _onSeek: function(progress) { + if (seekingStates.indexOf(this._fsm.state()) !== -1) { + this.progress.setPlayback(progress); + } + }, + _onSeekingStart: function() { + this._fsm.manipulation("startSeeking"); + }, + _onSeekingEnd: function(progress) { + if (seekingStates.indexOf(this._fsm.state()) !== -1) { + for (var i = 0; i < this._sources.length; ++i) { + this._sources[i].stop(); + } + this._sources = []; + + var ct = this.getCurrentPlaybackTime(); + var duration = this._audio.getDuration(); + var targetTime = duration * progress; + this._seekingTime += targetTime - ct; + + var nc = 0; + + for (var i = 0; i < this._buffers.length; ++i) { + var buffer = this._buffers[i]; + var startTime = nc - targetTime; + if (startTime < 0) { + var offset = startTime + buffer.duration; + if (offset > 0) { + var src = this._ctx.createBufferSource(); + src.buffer = buffer; + src.connect(this._ctx.destination); + src.start(0, Math.abs(startTime)); + this._sources.push(src); + } + } else { + var src = this._ctx.createBufferSource(); + src.buffer = buffer; + src.connect(this._ctx.destination); + src.start(this._ctx.currentTime + startTime); + this._sources.push(src); + } + + nc += buffer.duration; + } + } + this._fsm.manipulation("stopSeeking"); + }, _onStateChanged: function(e) { switch (e.newState) { case "initial": @@ -273,12 +364,31 @@ var Player = Controller.inherit({ case "hasControllerPlaying": if (this._audio.hasMore()) { this._audio.requestSlice(audioPortion); - - this._ctx.resume(); //todo temporal } else { this._fsm.manipulation("noMoreFrames"); } break; + case "buffering": + break; + case "bufferingPlaying": + if (e.oldState === "playing") { + this._ctx.suspend(); + } + break; + case "seeking": + break; + case "seekingPlaying": + if (e.oldState === "playing") { + this._ctx.suspend(); + } + break; + case "seekingAllLoaded": + break; + case "seekingPlayingAllLoaded": + if (e.oldState === "playingAllLoaded") { + this._ctx.suspend(); + } + break; case "paused": switch (e.oldState) { case "playing": @@ -299,6 +409,8 @@ var Player = Controller.inherit({ case "playingAllLoaded": switch (e.oldState) { case "pausedAllLoaded": + case "bufferingPlaying": + case "seekingPlayingAllLoaded": this._ctx.resume(); break; } @@ -365,36 +477,77 @@ graphs[PlayerMode.straight.playback] = { controllerReady: "hasControllerPlaying" }, "hasController": { - newFrames: "paused", + newFrames: "bufferingPlaying", play: "hasControllerPlaying", noController: "initial" }, "hasControllerPlaying": { - newFrames: "playing", + newFrames: "bufferingPlaying", pause: "hasController", noController: "initialPlaying" }, + "buffering": { + play: "bufferingPlaying", + enough: "paused", + noMoreFrames: "pausedAllLoaded", + startSeeking: "seeking", + noController: "initial" + }, + "bufferingPlaying": { + pause: "buffering", + enough: "playing", + noMoreFrames: "playingAllLoaded", + startSeeking: "seekingPlaying", + noController: "initialPlaying" + }, + "seeking": { + stopSeeking: "buffering", + noMoreFrames: "seekingAllLoaded", + noController: "initial" + }, + "seekingPlaying": { + stopSeeking: "bufferingPlaying", + noMoreFrames: "playingAllLoaded", + noController: "initialPlaying" + }, + "seekingAllLoaded": { + stopSeeking: "pausedAllLoaded", + noController: "initial" + }, + "seekingPlayingAllLoaded": { + stopSeeking: "playingAllLoaded", + noController: "initialPlaying" + }, "paused": { play: "playing", + notEnough: "buffering", noController: "initial", - noMoreFrames: "pausedAllLoaded" + noMoreFrames: "pausedAllLoaded", + startSeeking: "seeking" }, "pausedAllLoaded": { play: "playingAllLoaded", - noController: "initial" + noController: "initial", + startSeeking: "seekingAllLoaded" }, "playing": { pause: "paused", + notEnough: "bufferingPlaying", noMoreFrames: "playingAllLoaded", - noController: "initialPlaying" + noController: "initialPlaying", + startSeeking: "seekingPlaying" }, "playingAllLoaded": { pause: "pausedAllLoaded", - noController: "initialPlaying" + noController: "initialPlaying", + startSeeking: "seekingPlayingAllLoaded" } } +var seekingStates = ["seeking", "seekingPlaying", "seekingAllLoaded", "seekingPlayingAllLoaded"] -var audioPortion = 1024 * 50; +var audioPortion = 1024 * 50; //bytes to download for each portion +var threshold = 2; //seconds to buffer before playing +var intervalPrecision = 100; //millisecond of how often to check the playback var ProgressModel = Model.inherit({ className: "ProgressModel", diff --git a/lorgar/views/helpers/draggable.js b/lorgar/views/helpers/draggable.js index 173b4ad..f55eca2 100644 --- a/lorgar/views/helpers/draggable.js +++ b/lorgar/views/helpers/draggable.js @@ -30,9 +30,9 @@ this._y = 0; this._e.addEventListener("mousedown", this._proxy.onMouseDown, false); - this._e.addEventListener("touchstart", this._touch, false); - this._e.addEventListener("touchmove", this._touch, false); - this._e.addEventListener("touchend", this._touch, false); + this._e.addEventListener("touchstart", W.touchToMouse, false); + this._e.addEventListener("touchmove", W.touchToMouse, false); + this._e.addEventListener("touchend", W.touchToMouse, false); }, "destructor": function () { if (this._dragging) { @@ -45,9 +45,9 @@ } this._e.removeEventListener("mousedown", this._proxy.onMouseDown); - this._e.removeEventListener("touchstart", this._touch); - this._e.removeEventListener("touchmove", this._touch); - this._e.removeEventListener("touchend", this._touch); + this._e.removeEventListener("touchstart", W.touchToMouse); + this._e.removeEventListener("touchmove", W.touchToMouse); + this._e.removeEventListener("touchend", W.touchToMouse); Subscribable.fn.destructor.call(this); }, @@ -105,41 +105,6 @@ this._v.trigger("dragEnd"); return false; } - }, - "_touch": function (e) { - e.preventDefault(); - if (e.touches.length > 1 || (e.type == "touchend" && e.touches.length > 0)) - return; - - var type = null; - var touch = null; - var src = e.currentTarget; - switch (e.type) { - case "touchstart": - type = "mousedown"; - touch = e.changedTouches[0]; - - break; - case "touchmove": - type = "mousemove"; - touch = e.changedTouches[0]; - src = window; - break; - case "touchend": - type = "mouseup"; - touch = e.changedTouches[0]; - src = window; - break; - } - - var event = new MouseEvent(type, { - button: 0, - screenX: touch.screenX, - screenY: touch.screenY, - clientX: touch.clientX, - clientY: touch.clientY - }); - src.dispatchEvent(event); } }); diff --git a/lorgar/views/songProgress.js b/lorgar/views/songProgress.js index 02c153d..2f4fc44 100644 --- a/lorgar/views/songProgress.js +++ b/lorgar/views/songProgress.js @@ -20,14 +20,32 @@ this._createBars(); View.fn.constructor.call(this, controller, base, el); + this._seeking = false; + this._createProxy(); + this._f.on("load", this._onLoad, this); this._f.on("playback", this._onPlayback, this); this._e.style.backgroundColor = "#eeeeee"; this._e.appendChild(this._loadProgress); this._e.appendChild(this._playbackProgress); + + this._e.addEventListener("mousedown", this._proxy.onMouseDown, false); + this._e.addEventListener("touchstart", W.touchToMouse, false); + this._e.addEventListener("touchmove", W.touchToMouse, false); + this._e.addEventListener("touchend", W.touchToMouse, false); }, destructor: function() { + if (this._seeking) { + window.removeEventListener("mouseup", this._proxy.onMouseUp); + window.removeEventListener("mousemove", this._proxy.onMouseMove); + } + + this._e.removeEventListener("mousedown", this._proxy.onMouseDown); + this._e.removeEventListener("touchstart", W.touchToMouse); + this._e.removeEventListener("touchmove", W.touchToMouse); + this._e.removeEventListener("touchend", W.touchToMouse); + this._f.off("load", this._onLoad, this); this._f.off("playback", this._onPlayback, this); @@ -52,6 +70,13 @@ this._playbackProgress.style.top = "0"; this._playbackProgress.style.left = "0"; }, + _createProxy: function () { + this._proxy = { + onMouseDown: this._onMouseDown.bind(this), + onMouseUp: this._onMouseUp.bind(this), + onMouseMove: this._onMouseMove.bind(this) + } + }, _onData: function() { this._onLoad(this._f.load); this._onPlayback(this._f.playback); @@ -59,6 +84,40 @@ _onLoad: function(load) { this._loadProgress.style.width = load * 100 + "%"; }, + _onMouseDown: function(e) { + if (e.which === 1) { + window.addEventListener("mouseup", this._proxy.onMouseUp); + window.addEventListener("mousemove", this._proxy.onMouseMove); + this._seeking = true; + + this._f.trigger("seekingStart"); + + this._ap = this.getAbsolutePosition(); + var seek = Math.max(Math.min(e.pageX - this._ap.x, this._w), 0); + var nSeek = seek / this._w; + if (this._seek !== nSeek) { + this._seek = nSeek; + this._f.trigger("seek", this._seek); + } + } + }, + _onMouseMove: function(e) { + var seek = Math.max(Math.min(e.pageX - this._ap.x, this._w), 0); + var nSeek = seek / this._w; + if (this._seek !== nSeek) { + this._seek = nSeek; + this._f.trigger("seek", this._seek); + } + }, + _onMouseUp: function() { + delete this._ap; + delete this._seek; + this._f.trigger("seekingEnd", this._f.playback); + + this._seeking = false; + window.removeEventListener("mouseup", this._proxy.onMouseUp); + window.removeEventListener("mousemove", this._proxy.onMouseMove); + }, _onPlayback: function(pb) { this._playbackProgress.style.width = pb * 100 + "%"; }, diff --git a/lorgar/views/view.js b/lorgar/views/view.js index 8016b1f..cc3e745 100644 --- a/lorgar/views/view.js +++ b/lorgar/views/view.js @@ -98,6 +98,21 @@ return w; }, + "getAbsolutePosition": function() { + var pp; + if (this._p) { + pp = this._p.getAbsolutePosition(); + } else { + pp = Object.create(null); + pp.x = 0; + pp.y = 0; + } + + pp.x += this._x; + pp.y += this._y; + + return pp; + }, "_initDraggable": function() { this._dg = new Draggable(this, { snapDistance: this._o.snapDistance @@ -134,6 +149,7 @@ "remove": function() { if (this._p) { this._p.removeChild(this); + delete this._p; //just to make sure } }, "removeClass": function(className) {