public/src/SAGE2_AudioManager.js
// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2014-15
/* global createjs */
"use strict";
/**
* SAGE2 Audio Manager, renders the audio streams for a given site
*
* @module client
* @submodule SAGE2_AudioManager
* @class SAGE2_AudioManager
*/
// Global variable (needed by runtime)
var hostAlias = {};
// Websocket handle
var wsio;
// default settings for applications
var autoplay = false;
var initialVolume = 8;
// WebAudio API variables
var audioCtx;
var audioGainNodes = {};
var audioPannerNodes = {};
// Number of sound instances being played at once
var numberOfSounds = 0;
// Max number of sound played at once
var maxNumberOfSounds = 5;
// Explicitely close web socket when web browser is closed
window.onbeforeunload = function() {
if (wsio !== undefined) {
wsio.close();
}
};
/**
* When the page loads, starts the audio manager
*
*/
window.addEventListener('load', function(event) {
SAGE2_init();
});
/**
* Entry point of the application
*
* @method SAGE2_init
*/
function SAGE2_init() {
// SoundJS library
//
// Load the SoundJS library and plugins
if (!createjs.Sound.initializeDefaultPlugins()) {
console.log('SoundJS> cannot load library');
return;
} else {
console.log('SoundJS> library loaded - version', createjs.SoundJS.version);
}
///////////
// Detect which browser is being used
SAGE2_browser();
wsio = new WebsocketIO();
wsio.open(function() {
console.log("Websocket opened");
setupListeners();
// Get the cookie for the session, if there's one
var session = getCookie("session");
var clientDescription = {
clientType: "audioManager",
requests: {
config: true,
version: false,
time: false,
console: false
},
session: session
};
wsio.emit('addClient', clientDescription);
});
// Socket close event (ie server crashed)
wsio.on('close', function(evt) {
var i, tracks;
// Pause all video tracks
tracks = document.getElementsByTagName('video');
for (i = 0; i < tracks.length; i++) {
if (tracks[i].parentNode) {
tracks[i].pause();
// tracks[i].parentNode.removeChild(tracks[i]);
}
}
// Pause all audio tracks
tracks = document.getElementsByTagName('audio');
for (i = 0; i < tracks.length; i++) {
if (tracks[i].parentNode) {
tracks[i].pause();
// tracks[i].parentNode.removeChild(tracks[i]);
}
}
// Play an audio blop
createjs.Sound.play("down");
// Try to reload
var refresh = setInterval(function() {
// make a dummy request to test the server every 2 sec
var xhr = new XMLHttpRequest();
xhr.open("GET", "/", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log("server ready");
// when server ready, clear the interval callback
clearInterval(refresh);
// and reload the page
if (__SAGE2__.browser.isFirefox) {
var main = document.getElementById('main');
while (main.firstChild) {
main.removeChild(main.firstChild);
}
window.open(window.location, '_blank');
} else {
window.location.reload();
}
}
};
xhr.send();
}, 2000);
});
}
function setupListeners() {
// wall values
var totalWidth;
wsio.on('initialize', function(data) {
// Reset the counter for number sounds
numberOfSounds = 0;
});
wsio.on('setupDisplayConfiguration', function(json_cfg) {
var i;
var http_port;
var https_port;
http_port = json_cfg.port === 80 ? "" : ":" + json_cfg.port;
https_port = json_cfg.secure_port === 443 ? "" : ":" + json_cfg.secure_port;
hostAlias["http://" + json_cfg.host + http_port] = window.location.origin;
hostAlias["https://" + json_cfg.host + https_port] = window.location.origin;
for (i = 0; i < json_cfg.alternate_hosts.length; i++) {
hostAlias["http://" + json_cfg.alternate_hosts[i] + http_port] = window.location.origin;
hostAlias["https://" + json_cfg.alternate_hosts[i] + https_port] = window.location.origin;
}
// Load the initial volume value from the configuration object
if (json_cfg.audio && json_cfg.audio.initialVolume !== undefined) {
// Making sure the value is between 0 and 10
initialVolume = parseInt(json_cfg.audio.initialVolume, 10);
initialVolume = Math.max(Math.min(initialVolume, 10), 0);
console.log("Configuration> initialVolume = ", initialVolume);
}
// Select the jinggle sound (default or configuration file)
// var jingle = "sage2_jinggle.mp3";
// var jingle = "kola-startup.mp3";
// var jingle = "blues_lick_in_a.mp3";
var jingle = "waipio-jingle.mp3";
if (json_cfg.ui.startup_sound) {
// use the jingle file if specificied in configuration file
jingle = json_cfg.ui.startup_sound;
}
// folder for audio files (relative to public/)
var audioPath = "sounds/";
// Default settings
var defaults = {
volume: initialVolume / 20, // volume [0:1] - value 0-10 and half volume for special effects
delay: 0, // amount of time to delay the start of audio playback, in milliseconds
loop: 0, // times the audio loops when it reaches the end of playback, 0 no loops, -1 infinite
offset: 0, // offset from the start of the audio to begin playback, in milliseconds
pan: 0 // left-right pan of the sound, -1 (left) and 1 (right).
};
var lowdefaults = {
volume: initialVolume / 80, // low volume for some events
delay: 0, loop: 0,
offset: 0, pan: 0
};
// Array of assets to preload
var soundAssets = [
{id: "startup", src: jingle, defaultPlayProps: defaults},
{id: "newapp", src: "newapp2.mp3", defaultPlayProps: defaults},
{id: "deleteapp", src: "deleteapp.mp3", defaultPlayProps: defaults},
{id: "remote", src: "remote.mp3", defaultPlayProps: lowdefaults},
{id: "send", src: "send.mp3", defaultPlayProps: defaults},
{id: "down", src: "down.mp3", defaultPlayProps: lowdefaults}
];
// If the file cannot load, try other formats (need the files)
createjs.Sound.alternateExtensions = ["ogg", "mp3"];
// Callback when the assets are loaded
createjs.Sound.on("fileload", handleSoundJSLoad);
// Load the assets (will trigger the callbac when done)
createjs.Sound.registerSounds(soundAssets, audioPath);
// Main audio context (for low-level operations)
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
audioCtx.listener.setPosition(0, 0, 0);
totalWidth = json_cfg.totalWidth;
});
wsio.on('createAppWindow', function(data) {
// Limit the number of sounds
if (numberOfSounds < maxNumberOfSounds) {
numberOfSounds = numberOfSounds + 1;
// Play an audio blip
var newAppSound = createjs.Sound.play("newapp");
// Callback when sound is done playing
newAppSound.on('complete', function(evt) {
numberOfSounds = numberOfSounds - 1;
});
}
if (data.application === "movie_player") {
var main = document.getElementById('main');
var videosTable = document.getElementById('videos');
var vid;
if (__SAGE2__.browser.isFirefox) {
// Firefox seems to crash with audio elements
vid = document.createElement('video');
} else {
vid = document.createElement('audio');
}
vid.id = data.id;
// vid.volume = initialVolume / 10;
vid.firstPlay = true;
vid.startPaused = data.data.paused;
vid.controls = false;
vid.style.display = "none";
vid.addEventListener('canplaythrough', function() {
// Video is loaded and can be played
if (vid.firstPlay && vid.sessionTime) {
// Update the local time
vid.currentTime = vid.sessionTime;
}
vid.firstPlay = false;
if (autoplay === true) {
playVideo(data.id);
}
}, false);
vid.addEventListener('ended', function() {
if (autoplay === true) {
vid.currentTime = 0;
}
}, false);
vid.addEventListener('timeupdate', function() {
var vid_time = document.getElementById(data.id + "_time");
if (vid_time) {
// time second, converted to millis, and to string
vid_time.textContent = formatHHMMSS(1000.0 * vid.currentTime);
}
}, false);
var url = cleanURL(data.data.audio_url);
var source = document.createElement('source');
var param = url.indexOf('?');
if (param >= 0) {
source.src = url + "&clientID=audio";
} else {
source.src = url + "?clientID=audio";
}
source.type = data.data.audio_type;
vid.appendChild(source);
// WebAudio API
var audioSource = audioCtx.createMediaElementSource(vid);
var gainNode = audioCtx.createGain();
audioGainNodes[vid.id] = gainNode;
var panNode = audioCtx.createPanner();
panNode.panningModel = 'equalpower';
audioPannerNodes[vid.id] = panNode;
// source -> gain -> pan -> speakers
audioSource.connect(gainNode);
gainNode.connect(panNode);
panNode.connect(audioCtx.destination);
// webaudio end
var videoRow = document.createElement('tr');
videoRow.className = "rowNoBorder";
videoRow.id = data.id + "_row";
var icon = document.createElement('td');
icon.setAttribute("rowspan", 2);
var link = document.createElement("img");
if (data.icon) {
link.src = data.icon + '_256.jpg';
} else {
link.src = "images/unknownapp_256.jpg";
}
link.width = 120;
icon.appendChild(link);
var title = document.createElement('td');
title.id = data.id + "_title";
title.className = "videoTitle";
title.textContent = data.title;
var time = document.createElement('td');
time.id = data.id + "_time";
time.className = "videoTime";
time.textContent = "00:00:00";
var play = document.createElement('td');
play.id = data.id + "_play";
play.className = "videoPlay";
play.textContent = "Paused";
var volumeMute = document.createElement('td');
volumeMute.id = data.id + "_mute";
volumeMute.className = "videoVolumeMute";
volumeMute.innerHTML = "✓";
videoRow.appendChild(icon);
videoRow.appendChild(title);
videoRow.appendChild(time);
videoRow.appendChild(volumeMute);
videoRow.appendChild(play);
var videoRow2 = document.createElement('tr');
videoRow2.className = "rowWithBorder";
videoRow2.id = data.id + "_row2";
var volume = document.createElement('td');
volume.setAttribute("colspan", 4);
volume.id = data.id + "_volume";
volume.className = "videoVolume";
var volumeSlider = document.createElement('input');
volumeSlider.id = data.id + "_volumeSlider";
volumeSlider.className = "videoVolumeSlider";
volumeSlider.type = "range";
volumeSlider.appid = data.id;
volumeSlider.min = 0;
volumeSlider.max = 1;
volumeSlider.step = 0.05;
// Set the initial value
volumeSlider.value = initialVolume / 10;
// Setup a callback for slider
volumeSlider.addEventListener('input', changeVolume, false);
// set the initial volume
changeVolume({target: volumeSlider});
// Add slider to the DOM
volume.appendChild(volumeSlider);
videoRow2.appendChild(volume);
main.appendChild(vid);
videosTable.appendChild(videoRow);
videosTable.appendChild(videoRow2);
}
});
wsio.on('setItemPosition', function (data) {
if (audioPannerNodes[data.elemId]) {
// Calculate center of the application window
var halfTotalWidth = totalWidth / 2;
var centerX = data.elemLeft + data.elemWidth / 2 - halfTotalWidth;
if (centerX < -totalWidth) {
centerX = -totalWidth;
}
if (centerX > totalWidth) {
centerX = totalWidth;
}
// Update the panner position
var panX = centerX / totalWidth;
var panY = 0;
var panZ = 1 - Math.abs(panX);
var panNode = audioPannerNodes[data.elemId];
panNode.setPosition(panX, panY, panZ);
}
});
wsio.on('setItemPositionAndSize', function (data) {
if (audioPannerNodes[data.elemId]) {
// Calculate center of the application window
var halfTotalWidth = totalWidth / 2;
var centerX = data.elemLeft + data.elemWidth / 2 - halfTotalWidth;
if (centerX < -totalWidth) {
centerX = -totalWidth;
}
if (centerX > totalWidth) {
centerX = totalWidth;
}
// Update the panner position
var panX = centerX / totalWidth;
var panY = 0;
var panZ = 1 - Math.abs(panX);
var panNode = audioPannerNodes[data.elemId];
panNode.setPosition(panX, panY, panZ);
}
});
wsio.on('setVolume', function(data) {
// Get the slider element
var slider = document.getElementById(data.id + "_volumeSlider");
// Change its value
slider.value = data.level;
// Go change the actual volume (gain)
changeVideoVolume(data.id, data.level);
});
wsio.on('videoPlaying', function(data) {
var vid = document.getElementById(data.id);
if (vid) {
vid.play();
}
var vid_play = document.getElementById(data.id + "_play");
if (vid_play) {
vid_play.textContent = "Playing";
}
});
wsio.on('videoPaused', function(data) {
var vid = document.getElementById(data.id);
var vid_play = document.getElementById(data.id + "_play");
if (vid) {
vid.pause();
}
if (vid_play) {
vid_play.textContent = "Paused";
}
});
wsio.on('videoEnded', function(data) {
var vid = document.getElementById(data.id);
var vid_play = document.getElementById(data.id + "_play");
if (vid) {
vid.pause();
}
if (vid_play) {
vid_play.textContent = "Paused";
}
});
wsio.on('videoMuted', function(data) {
var vid = document.getElementById(data.id);
var vid_mute = document.getElementById(data.id + "_mute");
if (vid) {
vid.muted = true;
}
if (vid_mute) {
vid_mute.innerHTML = "✗";
}
});
wsio.on('videoUnmuted', function(data) {
var vid = document.getElementById(data.id);
var vid_mute = document.getElementById(data.id + "_mute");
if (vid) {
vid.muted = false;
}
if (vid_mute) {
vid_mute.innerHTML = "✓";
}
});
wsio.on('updateVideoItemTime', function(data) {
var vid = document.getElementById(data.id);
if (vid) {
if (vid.firstPlay) {
// if not fully loaded, just store the time
vid.sessionTime = data.timestamp;
} else {
vid.currentTime = data.timestamp;
}
}
});
wsio.on('deleteElement', function(data) {
// Limit the number of sounds
if (numberOfSounds < maxNumberOfSounds) {
numberOfSounds = numberOfSounds + 1;
// Play an audio blop
var deleteSound = createjs.Sound.play("deleteapp");
// Callback when sound is done playing
deleteSound.on('complete', function(evt) {
numberOfSounds = numberOfSounds - 1;
});
}
// Stop video
var vid = document.getElementById(data.elemId);
if (vid) {
vid.pause();
}
// Clean up the DOM
deleteElement(data.elemId);
deleteElement(data.elemId + "_row");
deleteElement(data.elemId + "_row2");
// Delete also the webaudio nodes
delete audioPannerNodes[data.elemId];
delete audioGainNodes[data.elemId];
});
wsio.on('connectedToRemoteSite', function(data) {
// Play an audio blop when a remote site comes up or down
if (data.connected === "on") {
createjs.Sound.play("remote");
}
if (data.connected === "off") {
createjs.Sound.play("down");
}
});
wsio.on('setAppSharingFlag', function(data) {
// Play an audio blop when sending an app to a remote site
if (data.sharing) {
createjs.Sound.play("send");
}
});
}
/**
* Callback for Soundjs library when all audio assets loaded
*
* @method handleSoundJSLoad
* @param event {Event} event data
*/
function handleSoundJSLoad(event) {
if (event.id === "startup") {
// Play the startup jingle at load
var instance = createjs.Sound.play(event.src);
// Set the volume
instance.volume = initialVolume / 10;
}
console.log('SoundJS> asset loaded', event.id);
}
/**
* Handler for the volume slider
*
* @method changeVolume
* @param event {Event} event data
*/
function changeVolume(event) {
var volumeSlider = event.target;
var vol = volumeSlider.value;
wsio.emit("setVolume", {id: volumeSlider.appid, level: vol});
// Dont need to change volume since the server will send message
// changeVideoVolume(volumeSlider.appid, vol);
}
/**
* Change the volume of a video
*
* @method changeVideoVolume
* @param videoId {String} id of the video
* @param volume {Number} new volume value
*/
function changeVideoVolume(videoId, volume) {
// Change the gain (0 to 1)
audioGainNodes[videoId].gain.value = volume;
}
/**
* Play a video
*
* @method playVideo
* @param videoId {String} id of the video
*/
function playVideo(videoId) {
wsio.emit('playVideo', {id: videoId});
}
/**
* Uptime video time
*
* @method updateVideotime
* @param videoId {String} id of the video
*/
function updateVideotime(videoId, timestamp, play) {
wsio.emit('updateVideoTime', {id: videoId, timestamp: timestamp, play: play});
}