radio/libjs/wController/player.js

604 lines
20 KiB
JavaScript
Raw Permalink Normal View History

2018-09-02 19:22:28 +00:00
"use strict";
var Uint64 = require("../wType/uint64");
var Address = require("../wType/address");
2018-09-02 19:22:28 +00:00
var Controller = require("./controller");
var Button = require("./button");
var ImageById = require("./imageById");
2018-09-02 19:22:28 +00:00
var Vocabulary = require("./vocabulary");
2018-12-17 17:15:58 +00:00
var Audio = require("./file/audio");
var Model = require("./localModel");
2018-12-17 17:15:58 +00:00
2018-09-02 19:22:28 +00:00
var Enum = require("../utils/enum");
2018-12-17 17:15:58 +00:00
var StateMachine = require("../utils/stateMachine");
2018-09-02 19:22:28 +00:00
var Player = Controller.inherit({
className: "Player",
constructor: function(addr) {
Controller.fn.constructor.call(this, addr);
this.controls = Object.create(null);
this.views = Object.create(null);
this.mode = PlayerMode.straight.playback;
this.progress = new ProgressModel();
2019-01-30 20:36:33 +00:00
this.volume = new Slider();
this.volume.setValue(1);
2019-01-26 18:54:22 +00:00
this.progress.on("seekingStart", this._onSeekingStart, this);
this.progress.on("seekingEnd", this._onSeekingEnd, this);
2019-01-30 20:36:33 +00:00
this.progress.enable(false);
this.volume.on("value", this._onVolume, this);
2018-12-17 17:15:58 +00:00
this._audio = null;
this._createStateMachine();
this._createPlayingInfrastructure();
2018-09-02 19:22:28 +00:00
this.addHandler("get");
this.addHandler("viewsChange");
2018-12-17 17:15:58 +00:00
this.addHandler("play");
this.addHandler("pause");
2019-01-26 18:54:22 +00:00
this._playbackInterval = setInterval(this._onInterval.bind(this), intervalPrecision);
2018-12-17 17:15:58 +00:00
},
destructor: function() {
this._clearInterval(this._playbackInterval);
this._destroyPlayingInfrastructure();
2018-12-17 17:15:58 +00:00
this._fsm.destructor();
this.progress.destructor();
2018-12-17 17:15:58 +00:00
Controller.fn.destructor.call(this);
2018-09-02 19:22:28 +00:00
},
_addControl: function(type, address) {
var t = type.valueOf();
if (this.controls[t] !== undefined) {
throw new Error("An attempt to add multiple instances of " + ItemType.reversed[t] + " into Player");
}
if (ItemType.reversed[t] !== undefined) {
switch (t) {
case ItemType.straight.playPause:
case ItemType.straight.prev:
case ItemType.straight.next:
2018-09-02 19:22:28 +00:00
var btn = new Button(address.clone());
btn.itemType = t;
this.controls[t] = btn;
this.addController(btn);
2018-10-28 21:32:44 +00:00
this.trigger("newElement", btn, t);
2018-09-02 19:22:28 +00:00
break;
default:
this.trigger("serviceMessage", "An attempt to add ItemType " + ItemType.reversed[t] + " to controls of the Player, but it's not qualified to be a control", 1);
2018-09-02 19:22:28 +00:00
}
} else {
this.trigger("serviceMessage", "An unrecgnized item ItemType in Player: " + t, 1);
2018-09-02 19:22:28 +00:00
}
},
_addView: function(type, address) {
var t = type.valueOf();
var ctrl;
var supported = false;
2018-09-02 19:22:28 +00:00
if (this.views[t] !== undefined) {
throw new Error("An attempt to add multiple instances of " + ItemType.reversed[t] + " into Player");
}
if (ItemType.reversed[t] !== undefined) {
switch (t) {
case ItemType.straight.queue:
this.trigger("serviceMessage", "Queue is not supported yet in Player", 1);
2018-09-02 19:22:28 +00:00
break;
case ItemType.straight.currentPlayback:
ctrl = new Vocabulary(address.clone());
2019-01-26 18:54:22 +00:00
ctrl.on("newElement", this._onNewPlaybackElement, this);
ctrl.on("removeElement", this._onRemovePlaybackElement, this);
supported = true;
break;
case ItemType.straight.picture:
ctrl = new ImageById(null, address.back());
ctrl.ItemType = t;
this.views[t] = ctrl;
this.trigger("newElement", ctrl, t);
supported = false; //just to avoid adding with addController, since ImageById is not a controller
2018-10-28 21:32:44 +00:00
break;
2018-09-02 19:22:28 +00:00
default:
this.trigger("serviceMessage", "An attempt to add ItemType " + ItemType.reversed[t] + " to views of the Player, but it's not qualified to be a view", 1);
2018-09-02 19:22:28 +00:00
}
} else {
this.trigger("serviceMessage", "An unrecognized item ItemType in Player: " + t, 1);
2018-09-02 19:22:28 +00:00
}
if (supported) {
ctrl.ItemType = t;
this.views[t] = ctrl;
this.addController(ctrl);
this.trigger("newElement", ctrl, t);
}
2018-09-02 19:22:28 +00:00
},
2019-01-26 18:54:22 +00:00
_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();
2019-01-30 20:36:33 +00:00
this._gainNode = this._ctx.createGain();
this._gainNode.connect(this._ctx.destination);
this._currentTime = 0;
2019-01-26 18:54:22 +00:00
this._seekingTime = 0;
this._buffers = [];
this._sources = [];
this._ctx.suspend();
},
2019-01-26 18:54:22 +00:00
_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();
},
2019-01-26 18:54:22 +00:00
getCurrentPlaybackTime: function() {
return this._ctx.currentTime + this._seekingTime;
2018-12-17 17:15:58 +00:00
},
2018-09-02 19:22:28 +00:00
_h_get: function(ev) {
var data = ev.getData();
var controls = data.at("controls");
var views = data.at("views");
var mode = data.at("mode").valueOf();
2018-09-02 19:22:28 +00:00
var size, i, vc;
size = controls.length();
for (i = 0; i < size; ++i) {
vc = controls.at(i);
this._addControl(vc.at("type"), vc.at("address"));
}
size = views.length();
for (i = 0; i < size; ++i) {
vc = views.at(i);
2018-10-28 21:32:44 +00:00
this._addView(vc.at("type"), vc.at("address"));
2018-09-02 19:22:28 +00:00
}
2018-10-28 21:32:44 +00:00
if (this.mode !== mode) {
if (PlayerMode.reversed[mode] === undefined) {
throw new Error("Unsupported mode of player: " + mode);
}
this.mode = mode;
}
2018-10-28 21:32:44 +00:00
this.initialized = true;
this.trigger("data");
2018-09-02 19:22:28 +00:00
},
2018-12-17 17:15:58 +00:00
_h_pause: function(ev) {
this._fsm.manipulation("pause");
2018-12-17 17:15:58 +00:00
},
_h_play: function(ev) {
this._fsm.manipulation("play");
},
2018-09-02 19:22:28 +00:00
_h_viewsChange: function(ev) {
var data = ev.getData();
var add = data.at("add");
var remove = data.at("remove");
var size, i, vc;
size = remove.length();
for (i = 0; i < size; ++i) {
this._removeView(remove.at(i).valueOf());
2018-09-02 19:22:28 +00:00
}
size = add.length();
for (i = 0; i < size; ++i) {
vc = add.at(i);
2018-10-28 21:32:44 +00:00
this._addView(vc.at("type"), vc.at("address"));
2018-09-02 19:22:28 +00:00
}
},
_onAudioNewSlice: function(frames) {
var arr = new Uint8Array(frames.valueOf());
this._decoder.addFragment(arr);
while (this._decoder.hasMore()) {
var sb = this._decoder.decode(9999);
if (sb === undefined) {
break;
} else {
2019-01-26 18:54:22 +00:00
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;
2019-01-30 20:36:33 +00:00
src.connect(this._gainNode);
2019-01-26 18:54:22 +00:00
src.start(0, Math.abs(startTime - this._ctx.currentTime));
this._sources.push(src);
}
} else {
var src = this._ctx.createBufferSource();
src.buffer = sb;
2019-01-30 20:36:33 +00:00
src.connect(this._gainNode);
2019-01-26 18:54:22 +00:00
src.start(startTime);
this._sources.push(src);
}
this._currentTime += sb.duration;
}
}
this.progress.setLoad(this._currentTime / this._audio.getDuration());
2018-12-17 17:15:58 +00:00
this._fsm.manipulation("newFrames");
if (this._audio.hasMore()) {
this._audio.requestSlice(audioPortion);
2018-12-17 17:15:58 +00:00
} else {
this._fsm.manipulation("noMoreFrames");
}
},
_onControllerReady: function() {
this._fsm.manipulation("controllerReady");
},
_onInterval: function() {
2019-01-26 18:54:22 +00:00
if (this._audio && this._audio.initialized && seekingStates.indexOf(this._fsm.state()) === -1) {
var duration = this._audio.getDuration();
2019-01-30 20:36:33 +00:00
this.progress.setValue(this.getCurrentPlaybackTime() / duration);
2019-01-26 18:54:22 +00:00
this._checkIfEnough();
2019-01-30 20:36:33 +00:00
if (this.progress.value >= 0.9999) {
2019-01-26 18:54:22 +00:00
var next = this.controls[ItemType.straight.next];
if (next && next.enabled) {
next.activate();
} else {
this._fsm.manipulation("pause");
this._onSeekingStart();
this._onSeekingEnd(0);
this.controls[ItemType.straight.playPause].activate();
2019-01-26 18:54:22 +00:00
}
}
}
},
2019-01-26 18:54:22 +00:00
_onNewPlaybackElement: function(key, element) {
switch (key) {
case "image":
var address = new Address(["images", element.toString()]);
this._addView(new Uint64(ItemType.straight.picture), address);
address.destructor();
break;
2018-12-17 17:15:58 +00:00
case "audio":
if (this.mode === PlayerMode.straight.playback) {
this._audio = new Audio(new Address(["music", element.toString()]));
this.addForeignController("Corax", this._audio);
this._audio.on("slice", this._onAudioNewSlice, this);
this._audio.on("ready", this._onControllerReady, this);
2018-12-17 17:15:58 +00:00
this._fsm.manipulation("controller");
}
break;
}
},
2019-01-26 18:54:22 +00:00
_onRemovePlaybackElement: function(key) {
switch (key) {
case "image":
this._removeView(ItemType.straight.picture);
break;
2018-12-17 17:15:58 +00:00
case "audio":
this.removeForeignController(this._audio);
this._audio.destructor();
this._audio = null;
}
},
2019-01-26 18:54:22 +00:00
_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;
2019-01-30 20:36:33 +00:00
src.connect(this._gainNode);
2019-01-26 18:54:22 +00:00
src.start(0, Math.abs(startTime));
this._sources.push(src);
}
} else {
var src = this._ctx.createBufferSource();
src.buffer = buffer;
2019-01-30 20:36:33 +00:00
src.connect(this._gainNode);
2019-01-26 18:54:22 +00:00
src.start(this._ctx.currentTime + startTime);
this._sources.push(src);
}
nc += buffer.duration;
}
}
this._fsm.manipulation("stopSeeking");
},
2018-12-17 17:15:58 +00:00
_onStateChanged: function(e) {
switch (e.newState) {
case "initial":
if (e.manipulation === "noController") {
2019-01-30 20:36:33 +00:00
this.progress.enable(false);
this.removeForeignController(this._audio);
this._audio.destructor();
this._audio = null;
this._destroyPlayingInfrastructure();
this._createPlayingInfrastructure();
}
2018-12-17 17:15:58 +00:00
break;
case "initialPlaying":
if (e.manipulation === "noController") {
2019-01-30 20:36:33 +00:00
this.progress.enable(false);
this._ctx.suspend();
this.removeForeignController(this._audio);
this._audio.destructor();
this._audio = null;
this._destroyPlayingInfrastructure();
this._createPlayingInfrastructure();
}
2018-12-17 17:15:58 +00:00
break;
case "controllerNotReady":
break
case "controllerNotReadyPlaying":
break
2018-12-17 17:15:58 +00:00
case "hasController":
break;
case "hasControllerPlaying":
if (this._audio.hasMore()) {
this._audio.requestSlice(audioPortion);
2018-12-17 17:15:58 +00:00
} else {
this._fsm.manipulation("noMoreFrames");
}
break;
2019-01-26 18:54:22 +00:00
case "buffering":
2019-01-30 20:36:33 +00:00
if (e.oldState === "hasController") {
this.progress.enable(true);
}
2019-01-26 18:54:22 +00:00
break;
case "bufferingPlaying":
if (e.oldState === "playing") {
this._ctx.suspend();
2019-01-30 20:36:33 +00:00
} else if (e.oldState === "hasControllerPlaying") {
this.progress.enable(true);
2019-01-26 18:54:22 +00:00
}
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;
2018-12-17 17:15:58 +00:00
case "paused":
switch (e.oldState) {
case "playing":
this._ctx.suspend();
2018-12-17 17:15:58 +00:00
break;
}
break;
case "pausedAllLoaded":
switch (e.oldState) {
case "playingAllLoaded":
this._ctx.suspend();
2018-12-17 17:15:58 +00:00
break;
}
break;
case "playing":
this._ctx.resume();
2018-12-17 17:15:58 +00:00
break;
case "playingAllLoaded":
switch (e.oldState) {
case "pausedAllLoaded":
2019-01-26 18:54:22 +00:00
case "bufferingPlaying":
case "seekingPlayingAllLoaded":
this._ctx.resume();
2018-12-17 17:15:58 +00:00
break;
}
break;
}
},
2019-01-30 20:36:33 +00:00
_onVolume: function(volume) {
this._gainNode.gain.cancelScheduledValues(this._ctx.currentTime);
this._gainNode.gain.exponentialRampToValueAtTime(volume, this._ctx.currentTime + 0.01);
},
2018-09-02 19:22:28 +00:00
_removeControl: function(type) {
var ctrl = this.controls[type];
if (ctrl !== undefined) {
this.trigger("removeElement", type);
this.removeController(ctrl);
ctrl.destructor();
}
2018-09-02 19:22:28 +00:00
},
_removeView: function(type) {
var view = this.views[type];
if (view !== undefined) {
this.trigger("removeElement", type);
if (type !== ItemType.straight.picture) {
this.removeController(view);
}
if (type === ItemType.straight.currentPlayback) {
if (this.views[ItemType.straight.picture]) {
this._removeView(ItemType.straight.picture);
}
this._fsm.manipulation("noController");
}
delete this.views[type];
view.destructor();
}
2018-09-02 19:22:28 +00:00
}
});
var ItemType = new Enum("ItemType");
ItemType.add("playPause");
ItemType.add("currentPlayback");
ItemType.add("queue");
ItemType.add("picture");
ItemType.add("prev");
ItemType.add("next");
var PlayerMode = new Enum("PlayerMode");
PlayerMode.add("playback");
2018-09-02 19:22:28 +00:00
Player.ItemType = ItemType;
2018-12-17 17:15:58 +00:00
var graphs = Object.create(null);
graphs[PlayerMode.straight.playback] = {
"initial": {
controller: "controllerNotReady",
2018-12-17 17:15:58 +00:00
play: "initialPlaying"
},
"initialPlaying": {
pause: "initial",
controller: "controllerNotReadyPlaying"
},
"controllerNotReady": {
play: "controllerNotReadyPlaying",
controllerReady: "hasController"
},
"controllerNotReadyPlaying": {
pause: "controllerNotReady",
controllerReady: "hasControllerPlaying"
2018-12-17 17:15:58 +00:00
},
"hasController": {
2019-01-26 18:54:22 +00:00
newFrames: "bufferingPlaying",
play: "hasControllerPlaying",
noController: "initial"
2018-12-17 17:15:58 +00:00
},
"hasControllerPlaying": {
2019-01-26 18:54:22 +00:00
newFrames: "bufferingPlaying",
pause: "hasController",
noController: "initialPlaying"
2018-12-17 17:15:58 +00:00
},
2019-01-26 18:54:22 +00:00
"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"
},
2018-12-17 17:15:58 +00:00
"paused": {
play: "playing",
2019-01-26 18:54:22 +00:00
notEnough: "buffering",
noController: "initial",
2019-01-26 18:54:22 +00:00
noMoreFrames: "pausedAllLoaded",
startSeeking: "seeking"
2018-12-17 17:15:58 +00:00
},
"pausedAllLoaded": {
play: "playingAllLoaded",
2019-01-26 18:54:22 +00:00
noController: "initial",
startSeeking: "seekingAllLoaded"
2018-12-17 17:15:58 +00:00
},
"playing": {
pause: "paused",
2019-01-26 18:54:22 +00:00
notEnough: "bufferingPlaying",
noMoreFrames: "playingAllLoaded",
2019-01-26 18:54:22 +00:00
noController: "initialPlaying",
startSeeking: "seekingPlaying"
2018-12-17 17:15:58 +00:00
},
"playingAllLoaded": {
pause: "pausedAllLoaded",
2019-01-26 18:54:22 +00:00
noController: "initialPlaying",
startSeeking: "seekingPlayingAllLoaded"
2018-12-17 17:15:58 +00:00
}
}
2019-01-26 18:54:22 +00:00
var seekingStates = ["seeking", "seekingPlaying", "seekingAllLoaded", "seekingPlayingAllLoaded"]
2018-12-17 17:15:58 +00:00
2019-01-26 18:54:22 +00:00
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
2019-01-30 20:36:33 +00:00
var Slider = Model.inherit({
className: "Slider",
constructor: function(properties) {
Model.fn.constructor.call(this, properties);
2019-01-30 20:36:33 +00:00
this.enabled = true;
this.value = 0;
this.initialized = true;
},
2019-01-30 20:36:33 +00:00
enable: function(en) {
if (en !== this.enabled) {
this.enabled = en;
this.trigger("enabled", en);
}
},
setValue: function(p) {
if (p !== this.value) {
this.value = p;
this.trigger("value", p);
}
}
});
var ProgressModel = Slider.inherit({
className: "ProgressModel",
constructor: function(properties) {
Slider.fn.constructor.call(this, properties);
this.value = 0;
},
setLoad: function(l) {
if (l !== this.load) {
this.load = l;
this.trigger("load", l);
}
}
});
2018-09-02 19:22:28 +00:00
module.exports = Player;