"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._onSeek(0); 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;