public/src/movie_player.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
"use strict";
/**
* @module client
* @submodule image_viewer
*/
/**
* Movie player application, inherits from SAGE2_BlockStreamingApp
*
* @class movie_player
*/
var movie_player = SAGE2_BlockStreamingApp.extend({
/**
* Init method, creates an 'div' tag in the DOM
*
* @method init
* @param data {Object} contains initialization values (id, width, height, ...)
*/
init: function(data) {
this.blockStreamInit(data);
this.firstLoad();
this.initWidgets();
// Keep a copy of the title string
this.title = data.title;
// command variables
this.shouldSendCommands = false;
this.shouldReceiveCommands = false;
},
/**
* Builds the widgets to control the movie player
*
* @method initWidgets
*/
initWidgets: function() {
var _this = this;
this.loopBtn = this.controls.addButton({
identifier: "Loop",
type: "loop",
position: 11
});
this.muteBtn = this.controls.addButton({
identifier: "Mute",
type: "mute",
position: 9
});
this.playPauseBtn = this.controls.addButton({
identifier: "PlayPause",
type: "play-pause",
position: 5
});
this.stopBtn = this.controls.addButton({
identifier: "Stop",
type: "rewind",
position: 3
});
this.controls.addSlider({
identifier: "Seek",
minimum: 0,
maximum: this.state.numframes - 1,
increments: 1,
property: "this.state.frame",
labelFormatFunction: function(value, end) {
var duration = parseInt(1000 * (value / _this.state.framerate), 10);
return formatHHMMSS(duration);
}
});
this.controls.finishedAddingControls();
// Calculate human readable string for the length of the video
var clipLength = this.state.numframes / this.state.framerate;
this.lengthString = formatHHMMSS(1000 * clipLength);
setTimeout(function() {
_this.muteBtn.state = _this.state.muted ? 0 : 1;
_this.loopBtn.state = _this.state.looped ? 0 : 1;
_this.playPauseBtn.state = _this.state.paused ? 0 : 1;
}, 500);
},
/**
* Set to movie player to a given frame
*
* @method setVideoFrame
* @param frameIdx {Number} change the current frame number
*/
setVideoFrame: function(frameIdx) {
this.state.frame = frameIdx;
this.SAGE2Sync(false);
},
/**
* Pause the movie if not in loop mode
*
* @method videoEnded
*/
videoEnded: function() {
if (this.state.looped === false) {
this.stopVideo();
} else if (this.shouldSendCommands) {
wsio.emit("serverDataSetValue", {
nameOfValue: "videoSyncCommandVariable",
value: {
command: "seek",
timestamp: 0,
frame: 0,
framerate: this.state.framerate,
play: false
},
description: "This variable contains the last send command"
});
}
},
/**
* Load the app from a previous state and builds the widgets
*
* @method load
* @param date {Date} time from the server
*/
load: function(date) {
},
/**
* Overloading the postDraw call to update the title
*
* @method postDraw
* @param date {Date} current time from the server
*/
postDraw: function(date) {
this.prevDate = date;
this.frame++;
// new code: put current time in title bar
var duration = parseInt(1000 * (this.state.frame / this.state.framerate), 10);
var current = formatHHMMSS(duration);
// modified to have (frame) [(Sending / Receiving Commands)]
if (this.shouldSendCommands) {
this.updateTitle(this.title + " - " + current + "(f:" + this.state.frame + ")(Sending Commands)");
} else if (this.shouldReceiveCommands) {
this.updateTitle(this.title + " - " + current + "(f:" + this.state.frame + ")(Receiving Commands)");
} else {
// Default mode: show current time and duration
this.updateTitle(this.title + " - " + current + " / " + this.lengthString);
// var currentFrame = Math.floor(this.state.frame % this.state.framerate) + 1;
}
},
/**
* Toggle between play and pause
*
* @method togglePlayPause
*
*/
togglePlayPause: function(date) {
if (this.state.paused === true) {
if (isMaster) {
// Trying to sync
wsio.emit('updateVideoTime', {
id: this.div.id,
timestamp: (this.state.frame / this.state.framerate),
play: true
});
// wsio.emit('playVideo', {id: this.div.id});
// if this is a sender, also send the command to the server holding variable
if (this.shouldSendCommands) {
wsio.emit("serverDataSetValue", {
nameOfValue: "videoSyncCommandVariable",
value: {
command: "play",
timestamp: (this.state.frame / this.state.framerate),
frame: this.state.frame,
framerate: this.state.framerate
},
description: "This variable contains the last send command"
});
}
}
this.state.paused = false;
} else {
if (isMaster) {
wsio.emit('pauseVideo', {id: this.div.id});
// if this is a sender, also send the command to the server holding variable
if (this.shouldSendCommands) {
wsio.emit("serverDataSetValue", {
nameOfValue: "videoSyncCommandVariable",
value: {
command: "pause",
timestamp: (this.state.frame / this.state.framerate),
frame: this.state.frame,
framerate: this.state.framerate
},
description: "This variable contains the last send command"
});
}
}
this.state.paused = true;
}
this.refresh(date);
this.playPauseBtn.state = (this.state.paused) ? 0 : 1;
this.getFullContextMenuAndUpdate();
},
/**
* Toggle between mute and unmute
*
* @method toggleMute
*
*/
toggleMute: function(date) {
if (this.state.muted === true) {
if (isMaster) {
wsio.emit('unmuteVideo', {id: this.div.id});
}
this.state.muted = false;
} else {
if (isMaster) {
wsio.emit('muteVideo', {id: this.div.id});
}
this.state.muted = true;
}
this.muteBtn.state = (this.state.muted) ? 0 : 1;
},
/**
* Toggle between looping and not looping
*
* @method toggleLoop
*
*/
toggleLoop: function(date) {
if (this.state.looped === true) {
if (isMaster) {
wsio.emit('loopVideo', {id: this.div.id, loop: false});
}
this.state.looped = false;
} else {
if (isMaster) {
wsio.emit('loopVideo', {id: this.div.id, loop: true});
}
this.state.looped = true;
}
this.loopBtn.state = (this.state.looped) ? 0 : 1;
this.getFullContextMenuAndUpdate();
},
stopVideo: function() {
if (isMaster) {
wsio.emit('stopVideo', {id: this.div.id});
// if this is a sender, also send the command to the server holding variable
if (this.shouldSendCommands) {
wsio.emit("serverDataSetValue", {
nameOfValue: "videoSyncCommandVariable",
value: {
command: "stop",
timestamp: (this.state.frame / this.state.framerate),
frame: this.state.frame,
framerate: this.state.framerate
},
description: "This variable contains the last send command"
});
}
}
this.state.paused = true;
// must change play-pause button (should show 'play' icon)
this.playPauseBtn.state = 0;
this.getFullContextMenuAndUpdate();
},
/**
* To enable right click context menu support this function needs to be present with this format.
*
* Must return an array of entries. An entry is an object with three properties:
* description: what is to be displayed to the viewer.
* callback: String containing the name of the function to activate in the app. It must exist.
* parameters: an object with specified datafields to be given to the function.
* The following attributes will be automatically added by server.
* serverDate, on the return back, server will fill this with time object.
* clientId, unique identifier (ip and port) for the client that selected entry.
* clientName, the name input for their pointer. Note: users are not required to do so.
* clientInput, if entry is marked as input, the value will be in this property. See pdf_viewer.js for example.
* Further parameters can be added. See pdf_view.js for example.
*/
getContextEntries: function() {
var entries = [];
var entry;
if (this.state.paused) {
entry = {};
entry.description = "Play";
entry.accelerator = "P";
entry.callback = "contextTogglePlayPause";
entry.parameters = {};
entries.push(entry);
} else {
entry = {};
entry.description = "Pause";
entry.accelerator = "P";
entry.callback = "contextTogglePlayPause";
entry.parameters = {};
entries.push(entry);
}
entry = {};
entry.description = "Stop";
entry.accelerator = "S";
entry.callback = "stopVideo";
entry.parameters = {};
entries.push(entry);
entry = {};
entry.description = "separator";
entries.push(entry);
if (this.state.muted) {
entry = {};
entry.description = "Unmute";
entry.callback = "contextToggleMute";
entry.accelerator = "M";
entry.parameters = {};
entries.push(entry);
} else {
entry = {};
entry.description = "Mute";
entry.accelerator = "M";
entry.callback = "contextToggleMute";
entry.parameters = {};
entries.push(entry);
}
if (this.state.looped) {
entry = {};
entry.description = "Stop looping";
entry.accelerator = "L";
entry.callback = "toggleLoop";
entry.parameters = {};
entries.push(entry);
} else {
entry = {};
entry.description = "Loop video";
entry.accelerator = "L";
entry.callback = "toggleLoop";
entry.parameters = {};
entries.push(entry);
}
// If a sender, then have access to additional command, step forward and step back.
if (this.shouldSendCommands) {
entry = {};
entry.description = "separator";
entries.push(entry);
entry = {};
entry.description = "< Step back";
entry.callback = "contextVideoSyncStep";
entry.parameters = {
step: "back"
};
entries.push(entry);
entry = {};
entry.description = "> Step forward";
entry.callback = "contextVideoSyncStep";
entry.parameters = {
step: "forward"
};
entries.push(entry);
}
entry = {};
entry.description = "separator";
entries.push(entry);
// Special callback: dowload the file
entries.push({
description: "Download video",
callback: "SAGE2_download",
parameters: {
url: this.state.video_url
}
});
entries.push({
description: "Copy URL",
callback: "SAGE2_copyURL",
parameters: {
url: this.state.video_url
}
});
return entries;
},
/**
* Calls togglePlayPause passing the given time.
*
* @method contextTogglePlayPause
* @param responseObject {Object} contains response from entry selection
*/
contextTogglePlayPause: function(responseObject) {
this.togglePlayPause(new Date(responseObject.serverDate));
this.getFullContextMenuAndUpdate();
},
/**
* Calls togglePlayPause passing the given time.
*
* @method contextToggleMute
* @param responseObject {Object} contains response from entry selection
*/
contextToggleMute: function(responseObject) {
this.toggleMute(new Date(responseObject.serverDate));
this.getFullContextMenuAndUpdate();
},
/**
* Sets variables necessary sending or receiving of commands.
* A player can send or receive, but not both.
*
* @method contextVideoSyncHandler
* @param responseObject {Object} contains response from entry selection
*/
contextVideoSyncHandler: function(responseObject) {
if (responseObject.send) {
this.shouldSendCommands = true;
this.shouldReceiveCommands = false;
// no purpose behind this other than to ensure variable exists after a sender is specified.
wsio.emit("serverDataSetValue", {
nameOfValue: "videoSyncCommandVariable",
value: {
command: "newSender",
timestamp: (this.state.frame / this.state.framerate),
frame: this.state.frame,
framerate: this.state.framerate
},
description: "This variable contains the last send command"
});
} else if (responseObject.receive) {
this.shouldSendCommands = false;
this.shouldReceiveCommands = true;
wsio.emit("serverDataSubscribeToValue", {
nameOfValue: "videoSyncCommandVariable",
app: this.id,
func: "videoSyncCommandHandler",
description: "This variable contains the last send command"
});
} else {
this.shouldSendCommands = false;
this.shouldReceiveCommands = false;
}
this.getFullContextMenuAndUpdate();
},
/**
* Initiates a step and pauses.
*
* @method contextVideoSyncStep
* @param responseObject {Object} contains response from entry selection
*/
contextVideoSyncStep: function(responseObject) {
var timestampToSend;
var shouldSendTimeUpdate = false;
if (responseObject.step == "back") {
shouldSendTimeUpdate = true;
if (this.state.frame == 0) { // if at 0, they go to last frame.
timestampToSend = this.state.numframes / this.state.framerate;
} else { // else frame is > 0
timestampToSend = this.state.frame - this.state.framerate;
if (timestampToSend < 0) { // stepping always stops at 0 before wrap around.
timestampToSend = 0;
} else { // non zero means calc its time.
timestampToSend = timestampToSend / this.state.framerate;
}
}
} else if (responseObject.step == "forward") {
shouldSendTimeUpdate = true;
if (this.state.frame == this.state.numframes) { // if at last frame, wrap around
timestampToSend = 0;
} else { // else frame is < max
timestampToSend = this.state.frame + this.state.framerate;
if (timestampToSend > this.state.numframes) { // stepping always stops at max before wrap
timestampToSend = this.state.numframes / this.state.framerate;
} else { // if not max, then calc
timestampToSend = timestampToSend / this.state.framerate;
}
}
}
// steps must be forward or back.
if (shouldSendTimeUpdate) {
wsio.emit('updateVideoTime', {
id: this.div.id,
timestamp: timestampToSend,
play: false
});
wsio.emit("serverDataSetValue", {
nameOfValue: "videoSyncCommandVariable",
value: {
command: "seek",
timestamp: timestampToSend,
frame: this.state.frame,
framerate: this.state.framerate,
play: false
},
description: "This variable contains the last send command"
});
}
},
/**
* Assumes that the update value is an object with properties:
* command
* timestamp
* frame
* framerate
* @method videoSyncCommandHandler
* @param valueUpdate {Object} contains last sent command
*/
videoSyncCommandHandler: function(valueUpdate) {
var playStatusToSend = false;
var timestampToSend = valueUpdate.timestamp;
var shouldSendTimeUpdate = false;
if (valueUpdate.command == "play") {
playStatusToSend = true;
this.playPauseBtn.state = 1; // show stop
shouldSendTimeUpdate = true;
} else if (valueUpdate.command == "pause") {
playStatusToSend = false;
this.playPauseBtn.state = 0; // show play
shouldSendTimeUpdate = true;
} else if (valueUpdate.command == "stop") {
playStatusToSend = false;
timestampToSend = 0;
this.playPauseBtn.state = 0; // show play
shouldSendTimeUpdate = true;
} else if (valueUpdate.command == "seek") {
this.state.playAfterSeek = valueUpdate.play;
playStatusToSend = valueUpdate.play;
this.playPauseBtn.state = playStatusToSend ? 1 : 0;
shouldSendTimeUpdate = true;
}
if (shouldSendTimeUpdate) {
wsio.emit('updateVideoTime', {
id: this.div.id,
timestamp: timestampToSend,
play: playStatusToSend
});
}
},
/**
* Handles event processing, arrow keys to navigate, and r to redraw
*
* @method event
* @param eventType {String} the type of event
* @param position {Object} contains the x and y positions of the event
* @param user_id {Object} data about the user who triggered the event
* @param data {Object} object containing extra data about the event,
* @param date {Date} current time from the server
*/
event: function(eventType, position, user, data, date) {
if (eventType === "keyboard") {
if (data.character === " ") {
this.togglePlayPause(date);
} else if (data.character === "l") {
this.toggleLoop(date);
} else if (data.character === "m") {
// m mute
if (this.state.muted === true) {
if (isMaster) {
wsio.emit('unmuteVideo', {id: this.div.id});
}
this.state.muted = false;
} else {
if (isMaster) {
wsio.emit('muteVideo', {id: this.div.id});
}
this.state.muted = true;
}
} else if (data.character === "1" || data.character === "s") {
// 1 start of video
this.stopVideo();
}
} else if (eventType === "specialKey") {
if (data.code === 80 && data.state === "up") { // P key
this.togglePlayPause(date);
}
} else if (eventType === "widgetEvent") {
switch (data.identifier) {
case "Loop":
this.toggleLoop(date);
break;
case "Mute":
this.toggleMute(date);
break;
case "PlayPause":
this.togglePlayPause(date);
break;
case "Stop":
this.stopVideo();
break;
case "Seek":
switch (data.action) {
case "sliderLock":
if (this.state.paused === false) {
if (isMaster) {
wsio.emit('pauseVideo', {id: this.div.id});
}
} else {
this.state.playAfterSeek = false;
}
break;
case "sliderUpdate":
break;
case "sliderRelease":
if (isMaster) {
wsio.emit('updateVideoTime', {
id: this.div.id,
timestamp: (this.state.frame / this.state.framerate),
play: !this.state.paused
});
if (this.shouldSendCommands) {
wsio.emit("serverDataSetValue", {
nameOfValue: "videoSyncCommandVariable",
value: {
command: "seek",
timestamp: (this.state.frame / this.state.framerate),
frame: this.state.frame,
framerate: this.state.framerate,
play: !this.state.paused
},
description: "This variable contains the last send command"
});
}
}
break;
}
break;
default:
console.log("No handler for:", data.identifier);
}
this.refresh(date);
}
}
});