Skip to content

Instantly share code, notes, and snippets.

@jsjohnst
Created May 13, 2010 01:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jsjohnst/399364 to your computer and use it in GitHub Desktop.
Save jsjohnst/399364 to your computer and use it in GitHub Desktop.
/*
@description Patch file to allow widgets using the KONtx.mediaplayer API to run on any Framework version 1.2.20 or higher on 2009 and 2010 model TVs
@author jstone
*/
KONtx_automation_log("loadjs","mediaplayer.js");
if(!KONtx.mediaplayer || !KONtx.mediaplayer._subscribers || typeof KONtx.mediaplayer._subscribers.onSystemPathCreationFailure !== "object") {
log("");log("**");log("");
log("Loading KONtx.mediaplayer patch for < FW 1.3.27");
log("");log("**");log("");
KONtx.mediaplayer = function() {
var internals = {};
internals.subscribeableEvents = [
'onRemoteKeyPress',
'onPauseRemoteKeyPress',
'onStopRemoteKeyPress',
'onPlayRemoteKeyPress',
'onRewindRemoteKeyPress',
'onFastForwardRemoteKeyPress',
'onStateChange',
'onPlaybackBuffering',
'onSetScreensaverMode',
'onTimeIndexChanged',
'onResetBufferingCount',
'onConnectionBandwidthChanged',
'onViewportBoundsChanged',
'onSetPlaybackSpeed',
'onControlPlay',
'onControlPause',
'onControlStop',
'onControlRewind',
'onControlFastForward',
'onControlSeek',
'onControlStreamSwitch',
'onConvertToSpeed',
'onFindBestStream',
'onStartStreamPlayback',
'onStreamLoadError',
'onPlayPlaylistEntry',
'onProcessPlaylistEntry',
'onPlaylistChange',
'onStartPlaylist',
'onLoadPlaylistEntry',
'onPlaylistRepeat',
'onPlaylistEnd',
'onLoadPreviousPlaylistEntry',
'onLoadNextPlaylistEntry',
'onNewStreamSelected',
'onSystemPathCreationFailure'
];
internals.constants = {};
/*
* Psuedo enum of all the remote transport keys
*/
internals.constants.keys = {
PAUSE : 19,
STOP : 413,
REWIND : 412,
PLAY : 415,
FASTFORWARD : 417
};
/*
* Pseudo Enum of all the possible states we will return on the onStateChange event
*/
internals.constants.states = {
INIT : -1,
PLAY : 0,
PAUSE : 1,
FASTFORWARD : 2,
FORWARD : 2,
FF : 2,
REWIND : 3,
STOP : 4,
BUFFERING : 5,
BUFFEREMPTY: 6,
INFOLOADED: 7,
EOF: 8,
UNKNOWN : 9,
ERROR : 10
};
/*
* Pseudo Enum of all the allowed stream switch methods
*/
internals.constants.streamswitch = {
INDEX_CHANGE : 1,
BANDWIDTH : 2
};
internals.active_states = [
internals.constants.states.PLAY,
internals.constants.states.PAUSE,
internals.constants.states.FASTFORWARD,
internals.constants.states.REWIND,
internals.constants.states.BUFFERING,
internals.constants.states.INFOLOADED
];
internals.eventListeners = {};
internals.tvapi = {
control: null,
path: null,
input: null,
output: null,
state: internals.constants.states.INIT,
timeIndex: null,
mediaDuration: null
};
internals.media = {
playlist: null,
playlist_index: null,
currentEntry: null,
stream_index: null,
stream_count: null,
buffering_count: -1
};
internals.bandwidth = {
bitrate: 0,
margin: 1
};
/*
* Object to handle various path creation related pointers
*/
internals.path_handling = {
timer: null,
playWhenReady: false,
playWhenReadyTimeIndex: 0
};
internals.init = function(config) {
config = config || {};
KONtx.HostEventManager.send("setExitVideoDialog", false);
if(!internals.tvapi.control || !internals.tvapi.path || !internals.tvapi.path.isValid() || !internals.tvapi.input || !internals.tvapi.input.isValid()) {
tv.onPathCreated = internals.handlePathCreated.bindTo(internals);
internals.tvapi.output = tv.getOutput( 'VideoPlaybackScreen' ) || tv.getOutput( 'PrimaryScreen' );
}
// The below logic is used to check if config.bindKeyHandler === false. If it is, then don't bind the key handler
if(!('bindKeyHandler' in config && config.bindKeyHandler === false) && !internals.boundPlayControlKeyHandler) {
internals.log("Binding Remote Key Handler");
internals.boundPlayControlKeyHandler = internals.playControlKeyHandler.subscribeTo(KONtx.application, 'onPlayControlKeyPress', internals);
} else {
internals.log("Skipping binding Remote Key Handler");
}
};
internals.destroyTVAPIPointers = function() {
internals.log("Destroying TV API pointers");
if(internals.tvapi.control) {
internals.log("Removing callbacks on TVControl instance and nulling out");
internals.tvapi.control.onStateChanged = null;
internals.tvapi.control.onTimeIndexChanged = null;
internals.tvapi.control = null;
}
if(internals.tvapi.path && internals.tvapi.path.isValid()) {
internals.log("Destroying TVPath");
tv.destroyPath( internals.tvapi.path.getID() );
internals.tvapi.path = null;
}
if(internals.tvapi.input && internals.tvapi.input.isValid()) {
internals.log("Destroying TVNetworkInput");
tv.destroyNetworkInput( internals.tvapi.input );
}
};
// start of path creation
internals.createAndLinkNewInput = function() {
internals.log("Creating new input and linking");
internals.destroyTVAPIPointers();
internals.tvapi.input = tv.createNetworkInput( '' );
var path = tv.createPath(internals.tvapi.input, internals.tvapi.output);
// TVPath might not be a defined class so we have to be extra careful on 2009 TVs
if (!(path === true || path === false) && path instanceof TVPath) {
internals.log("We have a TVPath now, so must be on >2009 TV");
internals.tvapi.path = path;
internals.setupTVAPIControl();
} else {
internals.log("We didn't get a TVPath back, so wait until the callback");
if(!internals.path_handling.timer) {
internals.log("Creating a timer in case we never get a onPathCreated callback");
internals.path_handling.timer = new Timer();
internals.path_handling.timer.interval = 2;
internals.path_handling.timer.firstPass = true;
internals.path_handling.timer.onTimerFired = function () {
if (internals.path_handling.timer.firstPass) {
internals.log("First firing of the timer and still no path, this isn't good");
internals.path_handling.timer.firstPass = false;
internals.path_handling.timer.interval = 30;
} else {
internals.log("Complete failure. Giving up waiting on a path creation.");
if (internals.path_handling.timer) {
internals.path_handling.timer.ticking = false;
internals.path_handling.timer = null;
}
if (this.fire("onStreamLoadError") && this.fire("onSystemPathCreationFailure")) {
internals.log("Neither failure event was canceled, so doing a little house keeping");
internals.destroyTVAPIPointers();
}
}
};
internals.path_handling.timer.ticking = true;
} else {
internals.log("We already have an existing timer");
}
}
};
internals.playControlKeyHandler = function(event) {
if(internals.fire("onRemoteKeyPress", { keyCode: event.payload.keyCode })) {
switch(event.payload.keyCode) {
case internals.constants.keys.PAUSE:
internals.log("pause remote key pressed");
if(internals.fire("onPauseRemoteKeyPress", { keyCode: event.payload.keyCode })) {
internals.pause();
}
break;
case internals.constants.keys.STOP:
internals.log("stop remote key pressed");
if(internals.fire("onStopRemoteKeyPress", { keyCode: event.payload.keyCode })) {
internals.stop();
}
break;
case internals.constants.keys.PLAY:
internals.log("play remote key pressed");
if(internals.fire("onPlayRemoteKeyPress", { keyCode: event.payload.keyCode })) {
internals.play();
}
break;
case internals.constants.keys.REWIND:
internals.log("rewind remote key pressed");
if(internals.fire("onRewindRemoteKeyPress", { keyCode: event.payload.keyCode })) {
internals.log("Rewind is only supported in limited circumstances");
internals.log("So if you want it handled, you need to provide your own handler");
}
break;
case internals.constants.keys.FASTFORWARD:
internals.log("fastforward remote key pressed");
if(internals.fire("onFastForwardRemoteKeyPress", { keyCode: event.payload.keyCode })) {
internals.log("Fastforward is only supported in limited circumstances");
internals.log("So if you want it handled, you need to provide your own handler");
}
break;
default:
log("We don't know what to do for keyCode: ", event.payload.keyCode);
break;
}
}
};
// Handling broken TV API in 5.5 engines by executing the callback inside a timer which will execute the callback after the current execution scope completes
internals.handleBrokenTVAPIWithDelay = function(callback) {
internals.log("Delaying execution of callback until the current execution scope completes");
internals.log($dump(callback));
var timer = new Timer();
timer.onTimerFired = function() {
this.ticking = false;
log("Timer delay has elapsed, now calling callback");
callback();
}
timer.interval = 0.1;
timer.ticking = true;
};
internals.delayStartNextClip = function() {
internals.log("Delaying start of next clip by 1/2 second.");
var callback = internals.next_playlist_entry.bindTo(internals);
internals.handleBrokenTVAPIWithDelay(callback);
};
internals.handleStateChange = function(newstate) {
var previousState = internals.tvapi.state;
internals.tvapi.state = newstate;
internals.log("State Change from " + previousState + " to " + newstate);
if (internals.fire("onStateChange", { newState: newstate, previousState: previousState })) {
if(newstate == internals.constants.states.BUFFERING) {
internals.media.buffering_count++;
if(internals.fire("onPlaybackBuffering")) {
KONtx.utility.WaitIndicator.on();
}
} else {
KONtx.utility.WaitIndicator.off();
}
if(newstate == internals.constants.states.EOF) {
if(internals.media.playlist_index + 1 < internals.media.playlist.entries.length) {
// if we will be for sure playing more clips, add a slight delay between clips to let low level player recover
internals.delayStartNextClip();
} else {
// otherwise, just let the normal logic handle it without a delay
internals.next_playlist_entry();
}
}
if(newstate == internals.constants.states.PAUSE || newstate == internals.constants.states.STOP) {
if(internals.fire("onSetScreensaverMode", { mode: "on" })) {
KONtx.HostEventManager.send("setScreensaver", true);
}
} else {
if(internals.fire("onSetScreensaverMode", { mode: "off" })) {
KONtx.HostEventManager.send("setScreensaver", false);
}
}
}
};
internals.handleTimeIndexChange = function() {
internals.log("Time index changed: ", internals.tvapi.control.timeIndex);
internals.tvapi.timeIndex = internals.tvapi.control.timeIndex;
internals.tvapi.mediaDuration = internals.tvapi.control.duration;
internals.fire("onTimeIndexChanged", { timeIndex: Math.floor(internals.tvapi.control.timeIndex / 1000),
duration: Math.floor(internals.tvapi.control.duration/1000),
rawTimeIndex: internals.tvapi.control.timeIndex,
rawDuration: internals.tvapi.control.duration });
};
internals.delayStartPlaybackIfNeeded = function() {
internals.log("Checking if we need to start playback now that we have a path");
if (internals.path_handling.playWhenReady) {
internals.log("We were asked to do a delayed start play, so call play_stream()");
var startIndex = internals.path_handling.playWhenReadyTimeIndex;
internals.path_handling.playWhenReadyTimeIndex = 0;
internals.play_stream(startIndex);
}
};
internals.setupTVAPIControl = function() {
internals.log("Fetching TVControl from the path");
internals.tvapi.control = internals.tvapi.path.control;
if(internals.tvapi.control) {
internals.log("We've got a TVControl, so bind the handlers");
internals.tvapi.control.onStateChanged = internals.handleStateChange.bindTo(internals);
internals.tvapi.control.onTimeIndexChanged = internals.handleTimeIndexChange.bindTo(internals);
var callback = internals.delayStartPlaybackIfNeeded.bindTo(this);
internals.handleBrokenTVAPIWithDelay(callback);
} else {
internals.log_error("Unable to retrieve TVPathControl for pathID: ", pathID);
}
};
// callback from tv.createPath
// async on 2009
internals.handlePathCreated = function(pathID) {
internals.log("Got a onPathCreated callback!");
if (internals.path_handling.timer) {
internals.log("Disabling the timer and nulling it out");
internals.path_handling.timer.ticking = false;
internals.path_handling.timer = null;
}
if(!internals.tvapi.path || !internals.tvapi.path.isValid() || internals.tvapi.path.getID() != pathID) {
internals.log("We don't already have a path, so using this new one");
internals.tvapi.path = tv.getPath(pathID);
if(internals.tvapi.path && internals.tvapi.path.isValid()) {
internals.log("Path is valid, so now setting up TVControl");
internals.setupTVAPIControl();
} else {
internals.log_error("Unable to retrieve TVPath for pathID: ", pathID);
}
} else {
internals.log("We already have a valid path, so ignoring this path creation");
}
};
internals.log_error = function(msg) {
log("");log("");log("");
log(msg);
log("");log("");log("");
return false;
};
// can be set to a no-op later potentially
internals.log = log;
internals.makeEventPayload = function(payload) {
internals.log("Creating event payload with additional payload of: ", $dump(payload, 3));
payload = payload || {};
payload.player = {};
payload.player.media = internals.media;
payload.player.tvapi = internals.tvapi;
payload.player.bandwidth = internals.bandwidth;
payload.player.states = internals.constants.states;
payload.player.keys = internals.constants.keys;
return payload;
};
// this is slightly different than the normal fire
internals.fire = function(eventType, eventPayload) {
var event;
if (eventType instanceof KONtx.system.Event) {
event = eventType;
eventType = event.type;
internals.log("firing event of type:" + eventType);
} else {
internals.log("firing event of type:" + eventType);
eventPayload = internals.makeEventPayload(eventPayload);
event = new KONtx.system.Event(eventType, eventPayload);
}
// filter the array of subscribers of this type - remove invalid entries
internals.eventListeners[eventType] = [].concat(internals.eventListeners[eventType]).filter(function(sub){
return sub instanceof Function;
});
if (!internals.eventListeners[eventType].length) {
internals.log("no listeners for event");
return true; // don't delete the key! it's a getter/setter!
}
var defaultPrevented = false;
for(var i in internals.eventListeners[event.type]) {
try {
internals.eventListeners[event.type][i](event);
} catch(e) {
log("Caught an exception from subscriber. Removing from subscribers list.", $dump(e, 3));
// array entry will get cleaned up by above filter next time, so just null it out
internals.eventListeners[event.type][i] = null;
}
if(event.defaultPrevented) {
internals.log("event default prevented");
defaultPrevented = true;
}
if(event.propagationStopped) {
internals.log("event propagation stopped");
break;
}
}
return !defaultPrevented;
};
internals.resetBufferingCount = function() {
internals.log("reset buffering count called");
if(internals.fire("onResetBufferingCount", { bufferingCount: internals.media.buffering_count })) {
internals.log("resetting buffering count");
internals.media.buffering_count = -1;
}
};
internals.setConnectionBandwidth = function(bitrate, margin) {
internals.log("Call to setConnectionBandwidth with values: ", bitrate, " / ", margin);
var previousBitrate = internals.bandwidth.bitrate;
var previousMargin = internals.bandwidth.margin;
internals.log("Previous bandwith values: ", previousBitrate, " / ", previousMargin);
internals.bandwidth.bitrate = bitrate;
margin = Number(margin);
if(margin > 0 && margin <= 1) {
internals.bandwidth.margin = margin;
}
internals.log("Player connection bandwidth set to: ", internals.bandwidth.bitrate, " with margin: ", internals.bandwidth.margin);
internals.fire("onConnectionBandwidthChanged", { bandwidth: { previous: { bitrate: previousBitrate, margin: previousMargin },
current: { bitrate: internals.bandwidth.bitrate, margin: internals.bandwidth.margin } } });
};
internals.getDefaultViewportBounds = function() {
if(internals.tvapi.output && internals.tvapi.output.nativeBounds && internals.tvapi.output.nativeBounds instanceof Rectangle) {
log("TV API gave us bounds: ", $dump(internals.tvapi.output.nativeBounds, 2));
var bounds = internals.tvapi.output.nativeBounds;
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
} else {
log("We can't get bounds from the TV API, so assume 1920x1080");
// we have no way to get this, so assume 1920x1080
return { x: 0, y: 0, width: 1920, height: 1080 };
}
};
internals.scaleViewportBounds = function(x, y, width, height) {
log("Scaling providing 960x540 offsets to native offsets");
var defaultBounds = internals.getDefaultViewportBounds();
var x_scaler = defaultBounds.width / 960;
var y_Scaler = defaultBounds.height / 540;
log("X scaler: ", x_scaler);
log("Y scaler: ", y_scaler);
x = Math.round(x * x_scaler);
y = Math.round(y * y_scaler);
width = Math.round(width * x_scaler);
height = Math.round(height * y_scaler);
internals.setViewportBounds(x, y, width, height);
};
internals.setViewportBounds = function(x, y, width, height) {
if(!internals.tvapi.output) {
internals.log_error("No tvapi output to set viewport bounds on!");
return;
}
if($type(x) == "object" && x.x >= 0 && x.y >= 0 && x.width > 0 && x.height > 0) {
internals.log("We got a viewport dimensions object as the first param, parsing it.");
y = x.y;
width = x.width;
height = x.height;
x = x.x;
}
internals.log("Changing viewport bounds to (x/y/w/h): ", x, "/", y, "/", width, "/", height);
var previousBounds = internals.tvapi.output.bounds;
internals.log("Previous viewport bounds were (x/y/w/h): ", previousBounds.x, "/", previousBounds.y, "/", previousBounds.width, "/", previousBounds.height);
internals.tvapi.output.bounds = new Rectangle(x, y, width, height);
internals.fire("onViewportBoundsChanged", { viewport: { previous: previousBounds, current: internals.tvapi.output.bounds } });
};
internals.setPlaybackSpeed = function(speed) {
if(!internals.tvapi.control) {
internals.log_error("No tvapi control to set speed on!");
return;
}
internals.log("Changing playback speed from: ", internals.tvapi.control.speed, " to: ", speed);
if(internals.fire("onSetPlaybackSpeed", { speed: { previous: internals.tvapi.control.speed, current: speed } })) {
internals.tvapi.control.speed = speed;
}
};
internals.play = function() {
if(!internals.tvapi.control || !internals.tvapi.control.play) {
internals.log_error("No tvapi control to call play on!");
return;
}
if(!(internals.media.playlist_index >= 0 && internals.media.stream_index >= 0)) {
internals.log("apparently someone pressed play before calling start on the playlist, so handle it for them!");
internals.start_playlist();
return;
}
internals.log("request to begin media playback");
if(internals.fire("onControlPlay")) {
internals.resetBufferingCount();
internals.setPlaybackSpeed(1);
internals.log("calling play() on TVPathControl object");
internals.tvapi.control.play();
}
};
internals.pause = function() {
if(!internals.tvapi.control || !internals.tvapi.control.pause) {
internals.log_error("No tvapi control to call pause on!");
return;
}
internals.log("request to pause media playback");
if(internals.fire("onControlPause")) {
internals.resetBufferingCount();
internals.log("calling pause() on TVPathControl object");
internals.tvapi.control.pause();
}
};
internals.stop = function() {
if(!internals.tvapi.control || !internals.tvapi.control.stop) {
internals.log_error("No tvapi control to call stop on!");
return;
}
internals.log("request to stop media playback");
if(internals.fire("onControlStop")) {
internals.resetBufferingCount();
internals.log("calling stop() on TVPathControl object");
internals.tvapi.control.stop();
}
};
internals.convertToSpeed = function(increment) {
var speed = 1;
increment = Math.abs(increment);
switch(true) {
case increment === 0:
speed = 1;
break;
case isNaN(increment): // intentionally falling through
case increment <= 2:
speed = 2;
break;
case increment <= 4:
speed = 4;
break;
case increment <= 8:
speed = 8;
break;
case increment > 8:
speed = 16;
break;
default:
speed = 2;
break;
}
internals.log("Converted increment: ", increment, " to speed: ", speed);
internals.fire("onConvertToSpeed", { increment: increment, speed: speed });
return speed;
};
internals.rewind = function(increment) {
internals.log("request to rewind media playback with increment: ", increment);
if(internals.fire("onControlRewind", { increment: increment })) {
internals.resetBufferingCount();
internals.setPlaybackSpeed(internals.convertToSpeed(increment) * -1);
}
};
internals.fastforward = function(increment) {
internals.log("request to fastforward media playback with increment: ", increment);
if(internals.fire("onControlFastForward", { increment: increment })) {
internals.resetBufferingCount();
internals.setPlaybackSpeed(internals.convertToSpeed(increment));
}
};
internals.seek = function(offset, absolute) {
if(!internals.tvapi.control) {
internals.log_error("No tvapi control to set time index on!");
return;
}
internals.log("request to seek media playback to offset: ", offset, absolute ? "" : " relative to current position");
if(internals.fire("onControlSeek", { offset: offset, absolute: absolute })) {
internals.resetBufferingCount();
if(absolute) {
offset = Math.abs(offset);
internals.log("seeking stream to position: ", offset, " seconds");
internals.tvapi.control.timeIndex = Number(offset) * 1000;
} else {
internals.log("seeking stream ", offset, " seconds from current position");
internals.tvapi.control.timeIndex += Number(offset) * 1000;
}
}
};
internals.streamswitch = function(method, config) {
internals.log("request to stream switch");
if(internals.fire("onControlStreamSwitch", { streamswitch: { method: method, config: config } })) {
switch(method) {
case internals.constants.streamswitch.INDEX_CHANGE:
var offset_amount = Number(config.offset_amount);
if(config.direction && (config.direction == "up" || config.direction == "down")) {
// if they didn't provide an offset amount, then default to 1
// if using direction, you normally wouldn't provide an offset
// unless you wanted to jump down 2 for example
if(!offset_amount) {
internals.log("no offset provided, so default to 1");
offset_amount = 1;
}
// the array is sorted from highest to lowest
// so to reduce bandwidth, our offset amount must increase
// to increase bitrate, our offset must decrease
if(config.direction == "down") {
internals.log("direction was down, so positive increment index");
offset_amount = Math.abs(offset_amount);
} else {
internals.log("direction was up, so negatively increment index");
offset_amount = Math.abs(offset_amount) * -1;
}
}
internals.log("stream switching by changing stream offset by: ", offset_amount);
var previous_stream_index = internals.media.stream_index;
internals.media.stream_index = Number(internals.media.stream_index) + offset_amount;
if(internals.media.stream_index >= internals.media.currentEntry.streams.length) {
internals.media.stream_index = internals.media.currentEntry.streams.length - 1;
}
// intentionally no else if, so we always make sure we are positive here
if(internals.media.stream_index < 0) {
internals.media.stream_index = 0;
}
internals.log("stream switching from previous stream index: ", previous_stream_index, " to new index of: ", internals.media.stream_index);
internals.fire("onNewStreamSelected");
break;
case internals.constants.streamswitch.BANDWIDTH:
internals.log("stream switching by setting new bandwidth bitrate: ", config.bitrate, " and margin: ", config.margin);
internals.setConnectionBandwidth(config.bitrate, config.margin);
internals.find_best_stream();
break;
default:
throw new Error("Can't switch stream without knowing what method to use for the switch");
break;
}
internals.log("now resuming play on new stream at existing time index");
internals.resetBufferingCount();
internals.play_stream(internals.tvapi.control.timeIndex);
}
};
internals.find_best_stream = function() {
if(!internals.media.currentEntry) {
internals.log_error("Can't find a stream to play without a valid entry!");
return;
}
if(internals.fire("onFindBestStream")) {
var index = 0;
internals.media.stream_count = internals.media.currentEntry.streams.length;
internals.log("Connection Bitrate: ", internals.bandwidth.bitrate, " Margin: ", internals.bandwidth.margin);
for(index in internals.media.currentEntry.streams) {
if(Number(internals.media.currentEntry.streams[index].bitrate) === 0) {
internals.log("Selecting stream index: ", index, " which has no bitrate and has URL: ", internals.media.currentEntry.streams[index].url);
internals.media.stream_index = index;
internals.fire("onNewStreamSelected");
return;
}
if(Number(internals.media.currentEntry.streams[index].bitrate) < (internals.bandwidth.bitrate * internals.bandwidth.margin)) {
internals.log("Selecting stream index: ", index, " which has bitrate: ", internals.media.currentEntry.streams[index].bitrate, " and URL: ", internals.media.currentEntry.streams[index].url);
internals.media.stream_index = index;
internals.fire("onNewStreamSelected");
return;
}
internals.log("Skipping stream index: ", index, " which has bitrate: ", internals.media.currentEntry.streams[index].bitrate, " and URL: ", internals.media.currentEntry.streams[index].url);
}
internals.log("Index: ", index);
if(internals.media.playlist.forcePlay) {
internals.media.stream_index = index;
internals.log("Forcing playback on stream index: ", index, " which has bitrate: ", internals.media.currentEntry.streams[index].bitrate, " and URL: ", internals.media.currentEntry.streams[index].url);
internals.fire("onNewStreamSelected");
} else {
internals.media.stream_index = null;
internals.log_error("We found no stream to play!");
}
}
};
// end of path
internals.play_stream = function(startIndex) {
internals.log("Attempting to play stream at time index:", startIndex);
internals.path_handling.playWhenReady = false;
if(internals.media.stream_index !== null && internals.media.stream_index >= 0 && internals.media.stream_index < internals.media.currentEntry.streams.length) {
if(!internals.tvapi.path || !internals.tvapi.path.isValid() || !internals.tvapi.control) {
internals.log("Have Path: ", internals.tvapi.path ? "true" : "false");
internals.log("Path Valid: ", internals.tvapi.path && internals.tvapi.path.isValid() ? "true" : "false");
internals.log("Have Control: ", internals.tvapi.control ? "true" : "false");
// set flag to play on path ready
internals.log("Setting flag requesting delayed start of playback when path is created");
internals.path_handling.playWhenReady = true;
internals.path_handling.playWhenReadyTimeIndex = startIndex;
return;
}
var url = internals.media.currentEntry.streams[internals.media.stream_index].url;
if(internals.fire("onStartStreamPlayback", { selectedURL: url, callbackHandler: internals.play_stream_handler.bindTo(internals), startIndex: startIndex })) {
internals.log("Calling play_stream_handler");
internals.play_stream_handler(url, startIndex);
}
} else {
internals.log("Stream load error!");
internals.fire("onStreamLoadError");
}
};
internals.play_stream_handler = function(url, startIndex) {
internals.log("setting networkinput to url: ", url);
internals.tvapi.input.uri = url;
if(startIndex) {
internals.log("starting media stream at index: ", startIndex);
internals.tvapi.control.timeIndex = startIndex;
}
internals.log("starting stream playback");
internals.tvapi.control.play();
};
internals.check_streams_ready_then_play = function() {
if(!internals.media.currentEntry.streamsReady(internals.check_streams_ready_then_play.bindTo(internals))) {
internals.log("Streams aren't ready yet. We will wait for the callback from the playlist entry indicating they are ready.");
return;
}
internals.find_best_stream();
internals.createAndLinkNewInput();
if(internals.fire("onPlayPlaylistEntry")) {
internals.play_stream(internals.media.currentEntry.startIndex * 1000);
}
};
internals.play_entry = function(entry) {
internals.log("fixing to play new playlist entry");
if(internals.fire("onProcessPlaylistEntry", { entry: entry })) {
internals.media.currentEntry = entry;
internals.media.stream_index = null;
internals.check_streams_ready_then_play();
}
};
internals.playlist_getter = function() {
internals.log("playlist get() was called");
return internals.media.playlist;
};
internals.playlist_setter = function(playlist) {
internals.log("playlist set() was called");
if(playlist instanceof KONtx.media.Playlist) {
if(internals.fire("onPlaylistChange", { playlist: playlist })) {
internals.media.playlist = playlist;
}
} else {
throw new Error("Playlists must be an instance of (or inherit from) KONtx.media.Playlist");
}
};
internals.start_playlist = function() {
internals.log("playlist start() was called");
if(internals.fire("onStartPlaylist")) {
if(internals.player_active()) {
internals.log_error("you didn't call stop() before trying to start a new playlist. I am doing it for you now, but this might have unintended side effects");
internals.stop();
}
internals.load_playlist_entry(0);
}
};
internals.load_playlist_entry = function(index) {
if(!internals.media.playlist || !internals.media.playlist.entries.length) {
internals.log_error("Can't load a playlist entry without having a playlist!");
return;
}
if(index < 0) index = 0;
internals.log("Fixing to load playlist entry at index: ", index);
if(internals.fire("onLoadPlaylistEntry", { index: index })) {
if( internals.media.playlist.entries[index] instanceof KONtx.media.PlaylistEntry ) {
internals.media.playlist_index = index;
internals.play_entry(internals.media.playlist.entries[index]);
} else if( internals.media.playlist.entries.length && internals.media.playlist.entries.length < index && internals.media.playlist.repeatAll ) {
// we check length first above to not get in an infinite loop from an empty list
if(internals.fire("onPlaylistRepeat")) {
internals.load_playlist_entry(0);
}
} else {
internals.fire("onPlaylistEnd");
}
}
};
internals.previous_playlist_entry = function() {
internals.log("loading previous playlist entry");
if(internals.fire("onLoadPreviousPlaylistEntry")) {
internals.load_playlist_entry(internals.media.playlist_index - 1);
}
};
internals.next_playlist_entry = function() {
internals.log("loading next playlist entry");
if(internals.fire("onLoadNextPlaylistEntry")) {
internals.load_playlist_entry(internals.media.playlist_index + 1);
}
};
internals.player_active = function() {
return internals.media.currentEntry && $contains(internals.tvapi.state, internals.active_states);
};
internals.subscriberProxy = {};
internals.subscribeableEvents.forEach(function(eventType) {
internals.subscriberProxy.__defineGetter__(eventType, function() {
return internals.eventListeners[eventType] ? internals.eventListeners[eventType] : [];
});
internals.subscriberProxy.__defineSetter__(eventType, function(data) {
if(data instanceof Array) {
internals.eventListeners[eventType] = data;
} else {
throw new Error("KONtx.mediaplayer.subscribe: Invalid setting of subscribers for eventType: ", eventType, "\n", $dump(data));
}
});
});
internals.controlsProxy = {};
internals.controlsProxy.__defineGetter__('play', function() {
return internals.play;
});
internals.controlsProxy.__defineGetter__('pause', function() {
return internals.pause;
});
internals.controlsProxy.__defineGetter__('stop', function() {
return internals.stop;
});
internals.controlsProxy.__defineGetter__('rewind', function() {
return internals.rewind;
});
internals.controlsProxy.__defineGetter__('fastforward', function() {
return internals.fastforward;
});
internals.controlsProxy.__defineGetter__('seek', function() {
return internals.seek;
});
internals.controlsProxy.__defineGetter__('streamswitch', function() {
return internals.streamswitch;
});
internals.playlistProxy = {};
internals.playlistProxy.__defineGetter__('get', function() {
return internals.playlist_getter;
});
internals.playlistProxy.__defineGetter__('set', function() {
return internals.playlist_setter;
});
internals.playlistProxy.__defineGetter__('start', function() {
return internals.start_playlist;
});
internals.playlistProxy.__defineGetter__('loadEntry', function() {
return internals.load_playlist_entry;
});
internals.playlistProxy.__defineGetter__('previousEntry', function() {
return internals.previous_playlist_entry;
});
internals.playlistProxy.__defineGetter__('nextEntry', function() {
return internals.next_playlist_entry;
});
internals.playlistProxy.__defineGetter__('currentEntry', function() {
return internals.media.currentEntry;
});
internals.playlistProxy.__defineGetter__('currentIndex', function() {
return { entry: internals.media.playlist_index, stream: internals.media.stream_index };
});
internals.tvAPIProxy = {};
internals.tvAPIProxy.__defineGetter__('activePath', function() {
return internals.tvapi.path;
});
internals.tvAPIProxy.__defineGetter__('activeControl', function() {
return internals.tvapi.control;
});
internals.tvAPIProxy.__defineGetter__('activeInput', function() {
return internals.tvapi.input;
});
internals.tvAPIProxy.__defineGetter__('activeOutput', function() {
return internals.tvapi.output;
});
internals.tvAPIProxy.__defineGetter__('currentPlayerState', function() {
return internals.tvapi.state;
});
internals.tvAPIProxy.__defineGetter__('currentTimeIndex', function() {
return Number(internals.tvapi.timeIndex);
});
internals.tvAPIProxy.__defineGetter__('currentMediaDuration', function() {
return Number(internals.tvapi.mediaDuration);
});
internals.tvAPIProxy.__defineGetter__('currentHTTPErrorCode', function() {
return internals.tvapi.input ? internals.tvapi.input.httpErrorCode : -1;
});
internals.constantsProxy = {};
internals.constantsProxy.__defineGetter__('states', function() {
return internals.constants.states;
});
internals.constantsProxy.__defineGetter__('keys', function() {
return internals.constants.keys;
});
internals.constantsProxy.__defineGetter__('streamswitch', function() {
return internals.constants.streamswitch;
});
internals.publicAPIProxy = {};
internals.publicAPIProxy.__defineGetter__('_subscribers', function() {
return internals.subscriberProxy;
});
internals.publicAPIProxy.__defineGetter__('control', function() {
return internals.controlsProxy;
});
internals.publicAPIProxy.__defineGetter__('tvapi', function() {
return internals.tvAPIProxy;
});
internals.publicAPIProxy.__defineGetter__('playlist', function() {
return internals.playlistProxy;
});
internals.publicAPIProxy.__defineGetter__('constants', function() {
return internals.constantsProxy;
});
internals.publicAPIProxy.__defineGetter__('initialize', function() {
return internals.init;
});
internals.publicAPIProxy.__defineGetter__('setConnectionBandwidth', function() {
return internals.setConnectionBandwidth;
});
internals.publicAPIProxy.__defineGetter__('setViewportBounds', function() {
return internals.setViewportBounds;
});
internals.publicAPIProxy.__defineGetter__('scaleViewportBounds', function() {
return internals.scaleViewportBounds;
});
internals.publicAPIProxy.__defineGetter__('getDefaultViewportBounds', function() {
return internals.getDefaultViewportBounds;
});
internals.publicAPIProxy.__defineGetter__('isPlaylistEntryActive', function() {
return internals.player_active();
});
internals.publicAPIProxy.__defineGetter__('debugInternals', function() {
log("");log("ATTENTION QA!!");log("");
log("");log("ATTENTION QA!!");log("");
log("Access into private internals of KONtx.mediaplayer is for debugging purposes only. This should never happen in live code!!");
log("");log("ATTENTION QA!!");log("");
log("");log("ATTENTION QA!!");log("");
log("");log("");
return internals;
});
return internals.publicAPIProxy;
}();
KONtx.mediaplayer.version = 2867; // disable patch loading if using our provided patch
KONtx.media = {};
KONtx.media.Playlist = new KONtx.Class({
config: {
autoStart: false,
repeatAll: false,
forcePlay: true
},
initialize: function() {
this._playlist = {};
this._playlist.entries = [];
this._playlist.autoStart = Boolean(this.config.autoStart);
this._playlist.repeatAll = Boolean(this.config.repeatAll);
this._playlist.forcePlay = Boolean(this.config.forcePlay);
this.__defineGetter__('autoStart', function() {
return this._playlist.autoStart;
});
this.__defineSetter__('autoStart', function(value) {
this._playlist.autoStart = Boolean(value);
});
this.__defineGetter__('repeatAll', function() {
return this._playlist.repeatAll;
});
this.__defineSetter__('repeatAll', function(value) {
this._playlist.repeatAll = Boolean(value);
});
this.__defineGetter__('forcePlay', function() {
return this._playlist.forcePlay;
});
this.__defineSetter__('forcePlay', function(value) {
this._playlist.forcePlay = Boolean(value);
});
this._isFiltered = false;
// intentionally no setter here, use helpers below
this.__defineGetter__('entries', this._entriesGetter);
},
_entriesGetter: function() {
if(!this._isFiltered) {
// make sure the list is valid first
this._playlist.entries = [].concat(this._playlist.entries).filter(function(entry){
return entry instanceof KONtx.media.PlaylistEntry;
});
this._isFiltered = true;
}
return this._playlist.entries;
},
// important to set _isFiltered flag to false if adding any additional setters which add/remove from _playlist
removeEntry: function(index) {
this._isFiltered = false;
this._playlist.entries[index] = null;
},
clearEntries: function() {
this._isFiltered = false;
this._playlist.entries = [];
},
addEntry: function(entry) {
this._isFiltered = false;
this.addEntries([entry]);
return this;
},
addEntries: function(entries) {
this._isFiltered = false;
this._playlist.entries = this._playlist.entries.concat(entries);
return this;
},
addEntryByURL: function(url, bitrate, startIndex) {
this._isFiltered = false;
var entry = new KONtx.media.PlaylistEntry({
url: url,
bitrate: bitrate,
startIndex: startIndex
});
this.addEntries([entry]);
return this;
}
});
KONtx.media.PlaylistEntry = new KONtx.Class({
config: {
url: null,
bitrate: null,
startIndex: null,
streams: null
},
initialize: function() {
this._startIndex = this.config.startIndex;
this.__defineGetter__('startIndex', function() {
return this._startIndex;
});
this.__defineSetter__('startIndex', function(value) {
this._startIndex = Number(value);
});
this._streams = [];
if(this.config.url) {
this._streams.push({ url: this.config.url, bitrate: this.config.bitrate });
}
if(this.config.streams instanceof Array) {
this.config.streams.forEach(function(stream) {
if(stream.url) {
this._streams.push({ url: stream.url, bitrate: stream.bitrate || this.config.bitrate });
} else {
throw new Error("Invalid stream: must at minimum provide a URL");
}
}, this);
}
this._isSorted = false;
// intentionally no setter here, use helpers below
this.__defineGetter__('streams', this._streamsGetter);
},
streamsReady: function(callback) {
// This method is called by the mediaplayer to see if streams are ready before fetching the streams
// If you have streams which expire quickly and need to be refetched immediately before playback
// override this method, save the callback passed in as the parameter to this function and return false.
// Once your streams are ready, then you would call the callback handler and the mediaplayer will resume
// like normal. Please note, this method will be called again after you call the callback handler,
// so you need to insure you return true the second time or else you could get stuck in an endless
// loop of fetching fresh urls.
return true;
},
_streamsGetter: function() {
if(!this._isSorted) {
this._streams.sort(function(a, b) {
if (Number(a.bitrate) == Number(b.bitrate)) {
return 0;
}
return Number(a.bitrate) < Number(b.bitrate) ? 1 : -1;
});
this._isSorted = true;
}
return this._streams;
},
// important to set _isSorted flag to false if adding any additional setters which add to _streams
addURL: function(url, bitrate) {
if(url) {
this._isSorted = false;
this._streams.push({ url: url, bitrate: bitrate });
} else {
throw new Error("Invalid stream: must at minimum provide a URL");
}
return this;
}
});
} // end patch conditional loader
KONtx_automation_log("endjs","mediaplayer.js");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment