"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._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), 250); }, 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._onNewRemoveBackElement, 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); } }, _createPlayingInfrastructure: function() { this._ctx = new AudioContext(); this._decoder = new Mp3Decoder(); this._currentTime = 0; this._ctx.suspend(); }, _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); }, _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 { var src = this._ctx.createBufferSource(); src.buffer = sb; src.connect(this._ctx.destination); src.start(this._currentTime); 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) { this.progress.setPlayback(this._ctx.currentTime / this._audio.getDuration()); } }, _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; } }, _onNewRemoveBackElement: function(key) { switch (key) { case "image": this._removeView(ItemType.straight.picture); break; case "audio": this.removeForeignController(this._audio); this._audio.destructor(); this._audio = null; } }, _onStateChanged: function(e) { switch (e.newState) { case "initial": if (e.manipulation === "noController") { this.removeForeignController(this._audio); this._audio.destructor(); this._audio = null; this._destroyPlayingInfrastructure(); this._createPlayingInfrastructure(); } break; case "initialPlaying": if (e.manipulation === "noController") { 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); this._ctx.resume(); //todo temporal } else { this._fsm.manipulation("noMoreFrames"); } 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": this._ctx.resume(); break; } break; } }, _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: "paused", play: "hasControllerPlaying", noController: "initial" }, "hasControllerPlaying": { newFrames: "playing", pause: "hasController", noController: "initialPlaying" }, "paused": { play: "playing", noController: "initial", noMoreFrames: "pausedAllLoaded" }, "pausedAllLoaded": { play: "playingAllLoaded", noController: "initial" }, "playing": { pause: "paused", noMoreFrames: "playingAllLoaded", noController: "initialPlaying" }, "playingAllLoaded": { pause: "pausedAllLoaded", noController: "initialPlaying" } } var audioPortion = 1024 * 50; var ProgressModel = Model.inherit({ className: "ProgressModel", constructor: function(properties) { Model.fn.constructor.call(this, properties); this.load = 0; this.playback = 0; this.initialized = true; }, setLoad: function(l) { if (l !== this.load) { this.load = l; this.trigger("load", l); } }, setPlayback: function(p) { if (p !== this.playback) { this.playback = p; this.trigger("playback", p); } } }); module.exports = Player;