604 lines
20 KiB
JavaScript
604 lines
20 KiB
JavaScript
"use strict";
|
|
|
|
var Uint64 = require("../wType/uint64");
|
|
var Address = require("../wType/address");
|
|
|
|
var Controller = require("./controller");
|
|
var Button = require("./button");
|
|
var ImageById = require("./imageById");
|
|
var Vocabulary = require("./vocabulary");
|
|
var Audio = require("./file/audio");
|
|
var Model = require("./localModel");
|
|
|
|
var Enum = require("../utils/enum");
|
|
var StateMachine = require("../utils/stateMachine");
|
|
|
|
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();
|
|
this.volume = new Slider();
|
|
this.volume.setValue(1);
|
|
this.progress.on("seekingStart", this._onSeekingStart, this);
|
|
this.progress.on("seekingEnd", this._onSeekingEnd, this);
|
|
this.progress.enable(false);
|
|
this.volume.on("value", this._onVolume, this);
|
|
this._audio = null;
|
|
this._createStateMachine();
|
|
this._createPlayingInfrastructure();
|
|
|
|
this.addHandler("get");
|
|
this.addHandler("viewsChange");
|
|
this.addHandler("play");
|
|
this.addHandler("pause");
|
|
|
|
this._playbackInterval = setInterval(this._onInterval.bind(this), intervalPrecision);
|
|
},
|
|
destructor: function() {
|
|
this._clearInterval(this._playbackInterval);
|
|
this._destroyPlayingInfrastructure();
|
|
this._fsm.destructor();
|
|
this.progress.destructor();
|
|
|
|
Controller.fn.destructor.call(this);
|
|
},
|
|
_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:
|
|
var btn = new Button(address.clone());
|
|
btn.itemType = t;
|
|
this.controls[t] = btn;
|
|
this.addController(btn);
|
|
this.trigger("newElement", btn, t);
|
|
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);
|
|
}
|
|
} else {
|
|
this.trigger("serviceMessage", "An unrecgnized item ItemType in Player: " + t, 1);
|
|
}
|
|
},
|
|
_addView: function(type, address) {
|
|
var t = type.valueOf();
|
|
var ctrl;
|
|
var supported = false;
|
|
|
|
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);
|
|
break;
|
|
case ItemType.straight.currentPlayback:
|
|
ctrl = new Vocabulary(address.clone());
|
|
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
|
|
break;
|
|
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);
|
|
}
|
|
} else {
|
|
this.trigger("serviceMessage", "An unrecognized item ItemType in Player: " + t, 1);
|
|
}
|
|
|
|
if (supported) {
|
|
ctrl.ItemType = t;
|
|
this.views[t] = ctrl;
|
|
this.addController(ctrl);
|
|
|
|
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._gainNode = this._ctx.createGain();
|
|
this._gainNode.connect(this._ctx.destination);
|
|
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();
|
|
},
|
|
getCurrentPlaybackTime: function() {
|
|
return this._ctx.currentTime + this._seekingTime;
|
|
},
|
|
_h_get: function(ev) {
|
|
var data = ev.getData();
|
|
|
|
var controls = data.at("controls");
|
|
var views = data.at("views");
|
|
var mode = data.at("mode").valueOf();
|
|
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);
|
|
this._addView(vc.at("type"), vc.at("address"));
|
|
}
|
|
|
|
if (this.mode !== mode) {
|
|
if (PlayerMode.reversed[mode] === undefined) {
|
|
throw new Error("Unsupported mode of player: " + mode);
|
|
}
|
|
this.mode = mode;
|
|
}
|
|
|
|
this.initialized = true;
|
|
this.trigger("data");
|
|
},
|
|
_h_pause: function(ev) {
|
|
this._fsm.manipulation("pause");
|
|
},
|
|
_h_play: function(ev) {
|
|
this._fsm.manipulation("play");
|
|
},
|
|
_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());
|
|
}
|
|
|
|
size = add.length();
|
|
for (i = 0; i < size; ++i) {
|
|
vc = add.at(i);
|
|
this._addView(vc.at("type"), vc.at("address"));
|
|
}
|
|
},
|
|
_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 {
|
|
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._gainNode);
|
|
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._gainNode);
|
|
src.start(startTime);
|
|
this._sources.push(src);
|
|
}
|
|
|
|
this._currentTime += sb.duration;
|
|
}
|
|
}
|
|
|
|
this.progress.setLoad(this._currentTime / this._audio.getDuration());
|
|
|
|
this._fsm.manipulation("newFrames");
|
|
if (this._audio.hasMore()) {
|
|
this._audio.requestSlice(audioPortion);
|
|
} else {
|
|
this._fsm.manipulation("noMoreFrames");
|
|
}
|
|
},
|
|
_onControllerReady: function() {
|
|
this._fsm.manipulation("controllerReady");
|
|
},
|
|
_onInterval: function() {
|
|
if (this._audio && this._audio.initialized && seekingStates.indexOf(this._fsm.state()) === -1) {
|
|
var duration = this._audio.getDuration();
|
|
this.progress.setValue(this.getCurrentPlaybackTime() / duration);
|
|
this._checkIfEnough();
|
|
|
|
if (this.progress.value >= 0.9999) {
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_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;
|
|
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);
|
|
this._fsm.manipulation("controller");
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
_onRemovePlaybackElement: function(key) {
|
|
switch (key) {
|
|
case "image":
|
|
this._removeView(ItemType.straight.picture);
|
|
break;
|
|
case "audio":
|
|
this.removeForeignController(this._audio);
|
|
this._audio.destructor();
|
|
this._audio = null;
|
|
}
|
|
},
|
|
_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._gainNode);
|
|
src.start(0, Math.abs(startTime));
|
|
this._sources.push(src);
|
|
}
|
|
} else {
|
|
var src = this._ctx.createBufferSource();
|
|
src.buffer = buffer;
|
|
src.connect(this._gainNode);
|
|
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":
|
|
if (e.manipulation === "noController") {
|
|
this.progress.enable(false);
|
|
this.removeForeignController(this._audio);
|
|
this._audio.destructor();
|
|
this._audio = null;
|
|
this._destroyPlayingInfrastructure();
|
|
this._createPlayingInfrastructure();
|
|
}
|
|
break;
|
|
case "initialPlaying":
|
|
if (e.manipulation === "noController") {
|
|
this.progress.enable(false);
|
|
this._ctx.suspend();
|
|
|
|
this.removeForeignController(this._audio);
|
|
this._audio.destructor();
|
|
this._audio = null;
|
|
this._destroyPlayingInfrastructure();
|
|
this._createPlayingInfrastructure();
|
|
}
|
|
break;
|
|
case "controllerNotReady":
|
|
break
|
|
case "controllerNotReadyPlaying":
|
|
break
|
|
case "hasController":
|
|
break;
|
|
case "hasControllerPlaying":
|
|
if (this._audio.hasMore()) {
|
|
this._audio.requestSlice(audioPortion);
|
|
} else {
|
|
this._fsm.manipulation("noMoreFrames");
|
|
}
|
|
break;
|
|
case "buffering":
|
|
if (e.oldState === "hasController") {
|
|
this.progress.enable(true);
|
|
}
|
|
break;
|
|
case "bufferingPlaying":
|
|
if (e.oldState === "playing") {
|
|
this._ctx.suspend();
|
|
} else if (e.oldState === "hasControllerPlaying") {
|
|
this.progress.enable(true);
|
|
}
|
|
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":
|
|
this._ctx.suspend();
|
|
break;
|
|
}
|
|
break;
|
|
case "pausedAllLoaded":
|
|
switch (e.oldState) {
|
|
case "playingAllLoaded":
|
|
this._ctx.suspend();
|
|
break;
|
|
}
|
|
break;
|
|
case "playing":
|
|
this._ctx.resume();
|
|
break;
|
|
case "playingAllLoaded":
|
|
switch (e.oldState) {
|
|
case "pausedAllLoaded":
|
|
case "bufferingPlaying":
|
|
case "seekingPlayingAllLoaded":
|
|
this._ctx.resume();
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
_onVolume: function(volume) {
|
|
this._gainNode.gain.cancelScheduledValues(this._ctx.currentTime);
|
|
this._gainNode.gain.exponentialRampToValueAtTime(volume, this._ctx.currentTime + 0.01);
|
|
},
|
|
_removeControl: function(type) {
|
|
var ctrl = this.controls[type];
|
|
if (ctrl !== undefined) {
|
|
this.trigger("removeElement", type);
|
|
this.removeController(ctrl);
|
|
ctrl.destructor();
|
|
}
|
|
},
|
|
_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();
|
|
}
|
|
}
|
|
});
|
|
|
|
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");
|
|
|
|
Player.ItemType = ItemType;
|
|
|
|
var graphs = Object.create(null);
|
|
graphs[PlayerMode.straight.playback] = {
|
|
"initial": {
|
|
controller: "controllerNotReady",
|
|
play: "initialPlaying"
|
|
},
|
|
"initialPlaying": {
|
|
pause: "initial",
|
|
controller: "controllerNotReadyPlaying"
|
|
},
|
|
"controllerNotReady": {
|
|
play: "controllerNotReadyPlaying",
|
|
controllerReady: "hasController"
|
|
},
|
|
"controllerNotReadyPlaying": {
|
|
pause: "controllerNotReady",
|
|
controllerReady: "hasControllerPlaying"
|
|
},
|
|
"hasController": {
|
|
newFrames: "bufferingPlaying",
|
|
play: "hasControllerPlaying",
|
|
noController: "initial"
|
|
},
|
|
"hasControllerPlaying": {
|
|
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",
|
|
startSeeking: "seeking"
|
|
},
|
|
"pausedAllLoaded": {
|
|
play: "playingAllLoaded",
|
|
noController: "initial",
|
|
startSeeking: "seekingAllLoaded"
|
|
},
|
|
"playing": {
|
|
pause: "paused",
|
|
notEnough: "bufferingPlaying",
|
|
noMoreFrames: "playingAllLoaded",
|
|
noController: "initialPlaying",
|
|
startSeeking: "seekingPlaying"
|
|
},
|
|
"playingAllLoaded": {
|
|
pause: "pausedAllLoaded",
|
|
noController: "initialPlaying",
|
|
startSeeking: "seekingPlayingAllLoaded"
|
|
}
|
|
}
|
|
var seekingStates = ["seeking", "seekingPlaying", "seekingAllLoaded", "seekingPlayingAllLoaded"]
|
|
|
|
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 Slider = Model.inherit({
|
|
className: "Slider",
|
|
constructor: function(properties) {
|
|
Model.fn.constructor.call(this, properties);
|
|
|
|
this.enabled = true;
|
|
this.value = 0;
|
|
this.initialized = true;
|
|
},
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
|
|
module.exports = Player;
|