/**
* @module Player
*/
/**
* I am the HypervideoController.
*
* I am the central controller module of the application. I control the interactions between the UI elements of the hypervideo and its data model.
*
* My two most important jobs are:
* * I init the video element, all its UI controls, and my sub-controllers (for annotations, overlays, videolinks)
* * I control the playback und the update handlers to show time-based contents
*
* @class HypervideoController
* @static
*/
FrameTrail.defineModule('HypervideoController', function(){
var HypervideoModel = FrameTrail.module('HypervideoModel'),
ViewVideo = FrameTrail.module('ViewVideo'),
AnnotationsController = FrameTrail.initModule('AnnotationsController'),
OverlaysController = FrameTrail.initModule('OverlaysController'),
VideolinksController = FrameTrail.initModule('VideolinksController'),
SubtitlesController = FrameTrail.initModule('SubtitlesController'),
InteractionController = FrameTrail.initModule('InteractionController'),
isPlaying = false,
currentTime = 0,
muted = false,
nullVideoStartDate = 0,
highPriorityInterval = 40,
lowPriorityInterval = 180,
nullVideoInterval = 40,
highPriorityIntervalID = null,
lowPriorityIntervalID = null,
nullVideoIntervalID = null,
highPriorityUpdater = null,
lowPriorityUpdater = null,
videoElement = ViewVideo.Video;
/**
* I initialize the Controller.
*
* I check, wether there are playable video source files, and append the right <source> nodes to the video element.
* Otherwise I prepare the "Null Player", meaning a simulated playback machine, which serves as a timer for update functions.
*
* After the video has sufficiently loaded (or the "Null Player" is ready), I initalize the UI control (play button and progress bar).
*
* @method initController
* @param {Function} callback
* @param {Function} failCallback
* @param {Boolean} update
*/
function initController(callback, failCallback, update) {
var RouteNavigation = FrameTrail.module('RouteNavigation'),
projectID = RouteNavigation.projectID,
hypervideoID = RouteNavigation.hypervideoID,
_video = $(videoElement);
updateDescriptions();
_video.width(1920).height(1080);
if (HypervideoModel.hasHTML5Video) {
highPriorityUpdater = highPriorityUpdater_HTML5;
lowPriorityUpdater = lowPriorityUpdater_HTML5;
FrameTrail.changeState('videoWorking', true);
_video.append('<source src="../_data/projects/' + projectID + '/resources/' + HypervideoModel.sourceFiles.mp4 +'" type="video/mp4"></source>');
_video.append('<source src="../_data/projects/' + projectID + '/resources/' + HypervideoModel.sourceFiles.webm +'" type="video/webm"></source>');
_video.on('play', _play);
_video.on('pause', _pause);
_video.on('seeking', function() {
FrameTrail.changeState('videoWorking', true);
});
_video.on('waiting', function() {
FrameTrail.changeState('videoWorking', true);
});
_video.on('canplaythrough', function() {
FrameTrail.changeState('videoWorking', false);
});
_video.on('seeked', function() {
FrameTrail.changeState('videoWorking', false);
});
_video.attr('preload', 'auto');
videoElement.load();
initVideo(
function(){
HypervideoModel.duration = videoElement.duration;
if (update) {
AnnotationsController.updateController();
} else {
AnnotationsController.initController();
}
OverlaysController.initController();
VideolinksController.initController();
SubtitlesController.initController();
initPlayButton();
initProgressBar();
InteractionController.initController();
if (RouteNavigation.hashTime) {
setCurrentTime(RouteNavigation.hashTime);
}
callback.call();
},
failCallback
);
} else {
highPriorityUpdater = highPriorityUpdater_NullVideo;
lowPriorityUpdater = lowPriorityUpdater_NullVideo;
if (update) {
AnnotationsController.updateController();
} else {
AnnotationsController.initController();
}
OverlaysController.initController();
VideolinksController.initController();
SubtitlesController.initController();
initPlayButton();
initProgressBar();
InteractionController.initController();
if (RouteNavigation.hashTime) {
setCurrentTime(RouteNavigation.hashTime);
}
callback.call();
}
FrameTrail.module('RouteNavigation').onHashTimeChange = function() {
setCurrentTime(RouteNavigation.hashTime);
};
};
/**
* I delay the execution of callback until enough data from the video source file has loaded.
*
* readyState == 0 means, that metadata is loaded. This is needed to know the __duration__ of the video.
*
* @method initVideo
* @param {Function} callback
* @param {Function} failCallback
* @private
*/
function initVideo(callback, failCallback) {
var waitingInterval = 500, // milliseconds
counter = 50; // 25 seconds waiting time
function checkReadyState() {
if (videoElement.readyState > 0){
callback();
} else {
if (--counter) {
window.setTimeout(checkReadyState, waitingInterval);
} else {
failCallback(
'VideoPlayer: Received no data within the time limit of '
+ Math.round(waitingInterval * 50 / 1000)
+ ' seconds.'
);
}
}
}
checkReadyState();
};
/**
* I init the UI of the play button and connect it with the play/pause functions.
*
* @method initPlayButton
* @private
*/
function initPlayButton(){
ViewVideo.PlayButton.click(function(){
if (isPlaying) {
pause();
} else {
play();
}
})
};
/**
* I initialize the UI elements of the progress bar.
*
* This depends on the duration of the video already being known.
*
* I make the DOM element a jQuery UI Slider, and attach its event listeners.
*
* @method initProgressBar
* @private
*/
function initProgressBar() {
ViewVideo.duration = formatTime(HypervideoModel.duration);
ViewVideo.PlayerProgress.slider({
value: 0,
step: 0.01,
orientation: "horizontal",
range: "min",
max: HypervideoModel.duration,
animate: false,
create: function(evt, ui) {
var circle = $('<div class="ui-slider-handle-circle"></div>'),
innerCircle = $('<div class="ui-slider-handle-circle-inner"></div>'),
_evtTarget = $(evt.target);
innerCircle.appendTo(circle);
_evtTarget.children('.ui-slider-handle').append(circle);
ViewVideo.adjustLayout();
ViewVideo.adjustHypervideo();
},
slide: function(evt, ui) {
setCurrentTime(ui.value);
},
start: function(evt, ui) {
},
stop: function(evt, ui) {
}
});
};
/**
* I update the descriptions of the hypervideo and of the current project, which is shown in the UI in the {{#crossLink "Sidebar"}}Sidebar{{/crossLink}}
* @method updateDescriptions
*/
function updateDescriptions() {
var created = new Date(HypervideoModel.created).toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\./g, '.'),
changed = new Date(HypervideoModel.lastchanged).toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\./g, '.');
FrameTrail.module('Sidebar').ProjectDescription = FrameTrail.module('Database').project.description;
FrameTrail.module('Sidebar').VideoDescription = ( '<div>' + HypervideoModel.description + '</div>'
+ '<div class="descriptionLabel">Author</div>'
+ '<div>' + HypervideoModel.creator + '</div>'
+ '<div class="descriptionDates">'
+ ' <div class="descriptionLabel">Created</div>'
+ ' <div>' + created + '</div>'
+ ' <div class="descriptionLabel">Last changed</div>'
+ ' <div>' + changed + '</div>'
+ '</div>'
);
};
/**
* I am the high priority update function, when there is a HTML5 video element present.
*
* I am called from the browser runtime environment via its window.setInterval mechanism. The interval is defined in the
* {{#crossLink "HypervideoController/_play:method"}}_play method{{/crossLink}}, and the interval length is set to 40 milliseconds.
*
* I fetch the currentTime attribute from the video element and store it in {{#crossLink "HypervideoController/currentTime:attribute"}}.
*
* I update the slider position of the progress bar.
*
* @method highPriorityUpdater_HTML5
* @private
*/
function highPriorityUpdater_HTML5() {
currentTime = videoElement.currentTime;
ViewVideo.PlayerProgress.slider('value', currentTime);
};
/**
* I am the low priority update function, when there is a HTML5 video element present.
*
* I am called from the browser runtime environment via its window.setInterval mechanism. The interval is defined in the
* {{#crossLink "HypervideoController/_play:method"}}_play method{{/crossLink}}, and the interval length is set to 180 milliseconds.
*
* I perform the following tasks:
*
* * Display the currentTime in the UI (numeric display in progress bar)
* * Call all sub-controllers ({{#crossLink "OverlaysController"}}OverlaysController{{/crossLink}}, {{#crossLink "VideosController"}}VideosController{{/crossLink}}, {{#crossLink "AnnotationsController"}}AnnotationsController{{/crossLink}}), to update the state for which they are responsible for.
*
* @method lowPriorityUpdater_HTML5
* @private
*/
function lowPriorityUpdater_HTML5() {
ViewVideo.currentTime = formatTime(currentTime);
OverlaysController.updateStatesOfOverlays(currentTime);
VideolinksController.updateStatesOfVideolinks(currentTime);
AnnotationsController.updateStatesOfAnnotations(currentTime);
SubtitlesController.updateStatesOfSubtitles(currentTime);
};
/**
* I am the high priority update function, when there is no HTML5 video element ("Null Player").
*
* I am called from the browser runtime environment via its window.setInterval mechanism. The interval is defined in the
* {{#crossLink "HypervideoController/_play:method"}}_play method{{/crossLink}}, and the interval length is set to 40 milliseconds.
*
* I update the slider position of the progress bar.
*
* @method highPriorityUpdater_NullVideo
* @private
*/
function highPriorityUpdater_NullVideo() {
ViewVideo.PlayerProgress.slider('value', currentTime);
};
/**
* I am the low priority update function, when there is no HTML5 video element ("Null Player").
*
* I am called from the browser runtime environment via its window.setInterval mechanism. The interval is defined in the
* {{#crossLink "HypervideoController/_play:method"}}_play method{{/crossLink}}, and the interval length is set to 180 milliseconds.
*
* I perform the following tasks:
*
* * Display the currentTime in the UI (numeric display in progress bar)
* * Call all sub-controllers ({{#crossLink "OverlaysController"}}OverlaysController{{/crossLink}}, {{#crossLink "VideosController"}}VideosController{{/crossLink}}, {{#crossLink "AnnotationsController"}}AnnotationsController{{/crossLink}}), to update the state for which they are responsible for.
*
* @method lowPriorityUpdater_NullVideo
* @private
*/
function lowPriorityUpdater_NullVideo() {
ViewVideo.currentTime = formatTime(currentTime);
OverlaysController.updateStatesOfOverlays(currentTime);
VideolinksController.updateStatesOfVideolinks(currentTime);
AnnotationsController.updateStatesOfAnnotations(currentTime);
SubtitlesController.updateStatesOfSubtitles(currentTime);
};
/**
* I am the update function of the "Null Player", which sets the {{#crossLink "HypervideoController/currentTime:attribute"}}.
*
* When the currentTime reaches the duration of the null video, I stop playback.
*
* @method nullVideoUpdater
* @private
*/
function nullVideoUpdater() {
currentTime = (Date.now() - nullVideoStartDate) / 1000;
if (currentTime >= HypervideoModel.duration) {
currentTime = HypervideoModel.duration;
pause();
}
};
/**
* I am the function, which starts the playback of the hypervideo.
*
* When there is a HTML5 video present, i simply call its .play() method,
* which in turn triggers the "play" event of the <video> element;
* The {{#crossLink "HypervideoController/_play:method"}}_play(){{/crossLink}} method is set as event handler for this event.
*
* When there is no HTML5 video ("Null player"), then I do two things:
* * I check if the currentTime reached the end of the "null video", and reset it to 0 if necessary.
* * I store the computer's current system clock time in the module var nullVideoStartDate (from this number the {{#crossLink "HypervideoController/nullVideoUpdater:method"}}nullVideoUpdater(){{/crossLink}}) can calculate the new currentTime.
*
* @method play
*/
function play() {
if (HypervideoModel.hasHTML5Video) {
videoElement.play();
} else {
if (!isPlaying){
if (currentTime === HypervideoModel.duration) {
currentTime = 0;
}
nullVideoStartDate = Date.now() - (currentTime * 1000)
_play();
}
}
};
/**
* I pause the playback of the hypervideo.
*
* When there is a HTML5 video present, i call its .pause() method,
* which in turn triggers the "pause" event of the <video> element;
* The {{#crossLink "HypervideoController/_pause:method"}}_pause(){{/crossLink}} method is set as event handler for the pause event.
*
* When there is no HTML5 video ("null player") I directly call the {{#crossLink "HypervideoController/_pause:method"}}_pause(){{/crossLink}} method.
*
* @method pause
*/
function pause() {
if (HypervideoModel.hasHTML5Video) {
videoElement.pause();
} else {
_pause();
}
};
/**
* After playback has started, we need to do several things:
* * Register interval functions (highPriorityUpdater and highPriorityInterval; if necessary: nullVideoUpdater)
* * Change play button into a pause button
* * Tell the {{#crossLink "OverlaysController/syncMedia:method"}}OverlaysController to synchronize media{{/crossLink}}.
*
* @method _play
* @private
*/
function _play() {
highPriorityIntervalID = window.setInterval(highPriorityUpdater, highPriorityInterval);
lowPriorityIntervalID = window.setInterval(lowPriorityUpdater, lowPriorityInterval);
if (!HypervideoModel.hasHTML5Video) {
nullVideoIntervalID = window.setInterval(nullVideoUpdater, nullVideoInterval);
}
ViewVideo.PlayButton.addClass('playing');
isPlaying = true;
OverlaysController.syncMedia();
};
/**
* After playback has paused, we need to do several things:
* * Clear the interval functions (highPriorityUpdater and highPriorityInterval; if necessary: nullVideoUpdater)
* * Change pause button back into play button
* * Tell the {{#crossLink "OverlaysController/syncMedia:method"}}OverlaysController to synchronize media{{/crossLink}}
*
* @method _pause
* @private
*/
function _pause() {
window.clearInterval(highPriorityIntervalID);
window.clearInterval(lowPriorityIntervalID);
if (!HypervideoModel.hasHTML5Video) {
window.clearInterval(nullVideoIntervalID);
}
ViewVideo.PlayButton.removeClass('playing');
isPlaying = false;
OverlaysController.syncMedia();
};
/**
* The HypervideoController stores the {{#crossLink "HypervideoController/currentTime:attribute"}}currentTime{{/crossLink}}.
* When this property is being set, several things have to happen:
* * The currentTime of the <video> element has to be updated...
* * or – when there is no video source file – the nullVideoStartDate has to be updated
* * The update functions have to be called (highPriorityUpdater and lowPriorityUpdater)
* * The OverlaysController has to {{#crossLink "OverlaysController/syncMedia:method"}}synchronize media{{/crossLink}}
*
* @method setCurrentTime
* @param {Number} aNumber
* @return Number
* @private
*/
function setCurrentTime(aNumber) {
if (HypervideoModel.hasHTML5Video) {
videoElement.currentTime = currentTime = aNumber;
} else {
currentTime = aNumber;
nullVideoStartDate = Date.now() - (currentTime * 1000)
}
highPriorityUpdater();
lowPriorityUpdater();
OverlaysController.syncMedia();
return aNumber;
};
/**
* The HypervideoController stores the {{#crossLink "HypervideoController/muted:attribute"}}muted{{/crossLink}}.
* When this property is being set, the muted attribute of the <video> element has to be updated
* (only when there is a video source file)
*
* @method setMuted
* @param {Boolean} muted
* @return muted
* @private
*/
function setMuted(muted) {
if (HypervideoModel.hasHTML5Video) {
videoElement.muted = muted;
}
OverlaysController.muteMedia(muted);
};
/**
* I take a number, which represents a time in seconds,
* and return a formatted string like HH:MM:SS
*
* @method formatTime
* @param {Number} aNumber
* @return String
*/
function formatTime(aNumber) {
var hours, minutes, seconds, hourValue;
seconds = Math.ceil(aNumber);
hours = Math.floor(seconds / (60 * 60));
hours = (hours >= 10) ? hours : '0' + hours;
minutes = Math.floor(seconds % (60*60) / 60);
minutes = (minutes >= 10) ? minutes : '0' + minutes;
seconds = Math.ceil(seconds % (60*60) % 60);
seconds = (seconds >= 10) ? seconds : '0' + seconds;
if (hours >= 1) {
hourValue = hours + ':';
} else {
hourValue = '';
}
return hourValue + minutes + ':' + seconds;
};
/**
* Cancel all currently running intervals
*
* @method clearIntervals
* @private
*/
function clearIntervals() {
window.clearInterval(highPriorityIntervalID);
window.clearInterval(lowPriorityIntervalID);
if (!HypervideoModel.hasHTML5Video) {
window.clearInterval(nullVideoIntervalID);
}
};
return {
initController: initController,
play: play,
pause: pause,
updateDescriptions: updateDescriptions,
clearIntervals: clearIntervals,
/**
* This read-only attribute tells if the hypervideo is playing or not.
* It is set by {{#crossLink "HypervideoController/_play:method"}}_play(){{/crossLink}} and {{#crossLink "HypervideoController/_pause:method"}}_pause(){{/crossLink}}
*
* @attribute isPlaying
* @readOnly
*/
get isPlaying() { return isPlaying },
/**
* This attributes stores the currentTime of the hypervideo.
*
* When this attribute is being read, it returns the value, which was automatically updated by {{#crossLink "HypervideoController/highPriorityUpdater_HTML5:method"}}highPriorityUpdater_HTML5(){{/crossLink}} or respectively {{#crossLink "HypervideoController/nullVideoUpdater:method"}}nullVideoUpdater(){{/crossLink}}.
*
* When the attribute is being set, like this:
*
* FrameTrail.module('HypervideoController').currentTime = 3
*
* then the {{#crossLink "HypervideoController/setCurrentTime:method"}}setCurrentTime(){{/crossLink}} is called.
*
* @attribute currentTime
*/
get currentTime() { return currentTime },
set currentTime(aNumber) { return setCurrentTime(aNumber) },
/**
* These attributes store the muted state of the hypervideo.
*
* The muted state is set like this:
*
* FrameTrail.module('HypervideoController').muted = true
*
* then the {{#crossLink "HypervideoController/setMuted:method"}}setMuted(){{/crossLink}} is called.
*
* @attribute muted
*/
get muted() { return muted },
set muted(aBoolean) { return setMuted(aBoolean) }
}
});