[#423] one baseplayer to rule them all.

[#423] updates to initial baseplayer code to work better with custom players

[#423] a fix to media function calls; added return statement

[#423] pulling initial work into proper repo location, fixing tests

[423] finished youtube player and tests for youtube player and baseplayer
This commit is contained in:
ScottDowne 2011-09-06 17:36:24 -04:00
Родитель 78b5b4393e
Коммит a3fdc5d53f
5 изменённых файлов: 810 добавлений и 699 удалений

Просмотреть файл

@ -15,6 +15,7 @@
<script>
// Helper function to get elements
function element(id) {
return document.getElementById(id);
}
@ -22,7 +23,7 @@
var paused = true,
popcorn;
popcorn = Popcorn( Popcorn.youtube( 'video', 'http://www.youtube.com/watch?v=9oar9glUCL0', { width: 400, controls: 0, annotations: 3 } ) );
popcorn = Popcorn.youtube( '#video', 'http://www.youtube.com/watch?v=9oar9glUCL0' );
popcorn = popcorn
.footnote({
@ -94,19 +95,18 @@
element( 'player-time' ).innerHTML = popcorn.currentTime();
})
// Update button labels
.listen( 'playing' , function() {
.listen( 'play' , function() {
paused = false;
element( 'btn-play-pause' ).innerHTML = 'Pause';
})
.listen('pause', function() {
paused = true;
element( 'btn-play-pause' ).innerHTML = 'Play';
})
popcorn.mute()
.play();
});
// Setup UI after loaded
popcorn.listen( 'load', function() {
element( 'player-status' ).innerHTML = 'Ready';
element( 'player_vol' ).innerHTML = popcorn.volume();
@ -121,6 +121,7 @@
// Seek
element('btn-seek').addEventListener('click', function() {
popcorn.currentTime( 30 );
}, false);
@ -131,7 +132,7 @@
}, false);
element('btn-mute').addEventListener('click', function() {
popcorn.mute();
popcorn.mute( !popcorn.media.muted );
}, false);
});
}, false );

Просмотреть файл

@ -1,558 +1,187 @@
// Popcorn Youtube Player Wrapper
// A global callback for youtube... that makes me angry
var onYouTubePlayerReady = function( containerId ) {
var onYouTubePlayerReady;
onYouTubePlayerReady[ containerId ] && onYouTubePlayerReady[ containerId ]();
};
onYouTubePlayerReady.stateChangeEventHandler = {};
( function( Popcorn ) {
/**
* Youtube wrapper for popcorn.
* This plug-in adds capability for Popcorn.js to deal with Youtube
* videos. This plug-in also doesn't use Popcorn's plugin() API and
* instead hacks directly into Popcorn's core.
*
* To use this plug-in, onYouTubePlayerReady() event handler needs to be
* called by the Youtube video player, before videos can be registered.
* Once videos are registered, calls to them can be made the same way as
* regular Popcorn objects. Also note that enablejsapi=1 needs to be added
* to the embed code, in order for Youtube's JavaScript API to work.
*
* Note that there are a few methods, properties and events that are not
* supported. See the bottom of this plug-in for a complete list.
*/
Popcorn.player( "youtube", {
_setup: function( options ) {
// Intended
var undef;
var media = this,
player = {},
youtubeObject,
container = document.createElement( "div" ),
currentTime = 0,
seekTime = 0,
seeking = false,
dataLoaded = false;
// Config parameters
// 33 ms per update is suitable for 30 fps
// 0.05 sec tolerance between old and new times to determine if currentTime has been set programatically
// 250 ms progress interval as specified by WHATWG
var timeupdateInterval = 33,
timeCheckInterval = 0.5,
progressInterval = 250;
container.id = media.id + Popcorn.guid();
// Ready State Constants
var READY_STATE_HAVE_NOTHING = 0,
READY_STATE_HAVE_METADATA = 1,
READY_STATE_HAVE_CURRENT_DATA = 2,
READY_STATE_HAVE_FUTURE_DATA = 3,
READY_STATE_HAVE_ENOUGH_DATA = 4;
media.appendChild( container );
// Youtube State Constants
var YOUTUBE_STATE_UNSTARTED = -1,
YOUTUBE_STATE_ENDED = 0,
YOUTUBE_STATE_PLAYING = 1,
YOUTUBE_STATE_PAUSED = 2,
YOUTUBE_STATE_BUFFERING = 3,
YOUTUBE_STATE_CUED = 5;
var youtubeInit = function() {
var urlRegex = /^.*[\/=](.{11})/;
// expose a callback to this scope, that is called from the global callback youtube calls
onYouTubePlayerReady[ container.id ] = function() {
// Collection of all Youtube players
var registry = {},
loadedPlayers = {};
youtubeObject = document.getElementById( container.id );
var abs = Math.abs;
// more youtube callback nonsense
onYouTubePlayerReady.stateChangeEventHandler[ container.id ] = function( state ) {
Popcorn.getScript( "http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js" );
// playing is state 1
// paused is state 2
if ( state === 1 ) {
// Extract the id from a web url
function extractIdFromUrl( url ) {
if ( !url ) {
media.paused && media.play();
// youtube fires paused events while seeking
// this is the only way to get seeking events
} else if ( state === 2 ) {
// silly logic forced on me by the youtube API
// calling youtube.seekTo triggers multiple events
// with the second events getCurrentTime being the old time
if ( seeking && seekTime === currentTime && seekTime !== youtubeObject.getCurrentTime() ) {
seeking = false;
youtubeObject.seekTo( currentTime );
return;
}
var matches = urlRegex.exec( url );
currentTime = youtubeObject.getCurrentTime();
media.dispatchEvent( "timeupdate" );
!media.paused && media.pause();
}
};
// Return id, which comes after first equals sign
return matches ? matches[1] : "";
// youtube requires callbacks to be a string to a function path from the global scope
youtubeObject.addEventListener( "onStateChange", "onYouTubePlayerReady.stateChangeEventHandler." + container.id );
var timeupdate = function() {
currentTime = youtubeObject.getCurrentTime();
media.dispatchEvent( "timeupdate" );
timeout = setTimeout( timeupdate, 10 );
};
media.play = function() {
media.paused = false;
media.dispatchEvent( "play" );
if ( dataLoaded ) {
media.dispatchEvent( "loadeddata" );
dataLoaded = false;
}
// Extract the id from a player url
function extractIdFromUri( url ) {
if ( !url ) {
return;
media.dispatchEvent( "playing" );
timeupdate();
youtubeObject.playVideo();
};
media.pause = function() {
if ( !media.paused ) {
media.paused = true;
media.dispatchEvent( "pause" );
youtubeObject.pauseVideo();
}
};
media.__defineSetter__( "currentTime", function( val ) {
// make sure val is a number
currentTime = seekTime = +val;
seeking = true;
media.dispatchEvent( "seeked" );
media.dispatchEvent( "timeupdate" );
youtubeObject.seekTo( currentTime );
return currentTime;
});
media.__defineGetter__( "currentTime", function() {
return currentTime;
});
media.__defineSetter__( "muted", function( val ) {
if ( youtubeObject.isMuted() !== val ) {
if ( val ) {
youtubeObject.mute();
} else {
youtubeObject.unMute();
}
var matches = urlRegex.exec( url );
// Return id, which comes after first equals sign
return matches ? matches[1] : "";
media.dispatchEvent( "volumechange" );
}
function getPlayerAddress( vidId, playerId ) {
if( !vidId ) {
return;
return youtubeObject.isMuted();
});
media.__defineGetter__( "muted", function() {
return youtubeObject.isMuted();
});
media.__defineSetter__( "volume", function( val ) {
if ( youtubeObject.getVolume() !== val ) {
youtubeObject.setVolume( val );
media.dispatchEvent( "volumechange" );
}
return "http://www.youtube.com/e/" + id;
return youtubeObject.getVolume();
});
media.__defineGetter__( "volume", function() {
return youtubeObject.getVolume();
});
media.readyState = 4;
media.dispatchEvent( 'load' );
dataLoaded = true;
media.duration = youtubeObject.getDuration();
media.dispatchEvent( 'durationchange' );
if ( !media.paused ) {
media.play();
}
function makeSWF( url, container ) {
var bounds,
params,
flashvars,
attributes,
self = this;
media.paused && media.dispatchEvent( 'loadeddata' );
};
options.controls = +options.controls === 0 || +options.controls === 1 ? options.controls : 1;
options.annotations = +options.annotations === 1 || +options.annotations === 3 ? options.annotations : 1;
var flashvars = {
playerapiid: container.id,
controls: options.controls,
iv_load_policy: options.annotations
};
swfobject.embedSWF( "http://www.youtube.com/e/" + /^.*[\/=](.{11})/.exec( media.src )[ 1 ] + "?enablejsapi=1&playerapiid=" + container.id + "&version=3",
container.id, media.offsetWidth, media.offsetHeight, "8", null,
flashvars, {wmode: "transparent", allowScriptAccess: "always"}, {id: container.id} );
};
if ( !window.swfobject ) {
setTimeout( function() {
makeSWF.call( self, url, container );
}, 1 );
return;
}
bounds = container.getBoundingClientRect();
// Determine width/height/etc based on container
this.width = container.style.width || 460;
this.height = container.style.height || 350;
// Just in case we got the attributes as strings. We'll need to do math with these later
this.width = parseFloat(this.width);
this.height = parseFloat(this.height);
// For calculating position relative to video (like subtitles)
this.offsetWidth = this.width;
this.offsetHeight = this.height;
this.offsetLeft = bounds.left;
this.offsetTop = bounds.top;
this.offsetParent = container.offsetParent;
flashvars = {
playerapiid: this.playerId,
controls: this.controls,
iv_load_policy: this.iv_load_policy
};
params = {
allowscriptaccess: 'always',
allowfullscreen: 'true',
// This is so we can overlay html on top of Flash
wmode: 'transparent'
};
attributes = {
id: this.playerId
};
swfobject.embedSWF( "http://www.youtube.com/e/" + this.vidId +"?enablejsapi=1&playerapiid=" + this.playerId + "&version=3",
this.playerId, this.width, this.height, "8", null, flashvars, params, attributes );
}
// Called when a player is loaded
// Playerid must match the element id
onYouTubePlayerReady = function ( playerId ) {
var vid = registry[playerId];
loadedPlayers[playerId] = 1;
// Video hadn't loaded yet when ctor was called
vid.video = document.getElementById( playerId );
vid.duration = vid.video.getDuration();
// Issue load event
vid.dispatchEvent( 'load' );
vid.dispatchEvent( "durationchange" );
};
Popcorn.youtube = function( elementId, url, options ) {
return new Popcorn.youtube.init( elementId, url, options );
};
Popcorn.youtube.init = function( elementId, url, options ) {
if ( !elementId ) {
throw "Element id is invalid.";
} else if ( /file/.test( location.protocol ) ) {
throw "This must be run from a web server.";
}
options = options || {};
var self = this;
this.playerId = elementId + Popcorn.guid();
this.readyState = READY_STATE_HAVE_NOTHING;
this._eventListeners = {};
this.loadStarted = false;
this.loadedData = false;
this.fullyLoaded = false;
this.paused = false;
// If supplied as number, append 'px' on end
// If suppliied as '###' or '###px', convert to number and append 'px' back on end
options.width = options.width && (+options.width)+"px";
options.height = options.height && (+options.height)+"px";
// show controls on video. Integer value - 1 is for show, 0 is for hide
this.controls = +options.controls === 0 || +options.controls === 1 ? options.controls : 1;
// show video annotations, 1 is show, 3 is hide
this.iv_load_policy = +options.annotations === 1 || +options.annotations === 3 ? options.annotations : 1;
this._target = document.getElementById( elementId );
this._container = document.createElement( "div" );
this._container.id = this.playerId;
this._container.style.height = this._target.style.height = options.height || this._target.style.height || "350px";
this._container.style.width = this._target.style.width = options.width || this._target.style.width || "460px";
this._target.appendChild( this._container );
this.parentNode = this._target.parentNode;
this.offsetHeight = +this._target.offsetHeight;
this.offsetWidth = +this._target.offsetWidth;
this.currentTime = this.previousCurrentTime = 0;
this.volume = this.previousVolume = this.preMuteVol = 1;
this.duration = 0;
this.vidId = extractIdFromUrl( url ) || extractIdFromUri( url );
if ( !this.vidId ) {
throw "Could not find video id";
}
this.addEventListener( "load", function() {
// For calculating position relative to video (like subtitles)
this.offsetWidth = this.video.offsetWidth;
this.offsetHeight = this.video.offsetHeight;
this.offsetParent = this.video.offsetParent;
// Set up stuff that requires the API to be loaded
this.registerYoutubeEventHandlers();
this.registerInternalEventHandlers();
});
(function() {
var hasBeenCalled = 0;
self.addEventListener( "playing", function() {
if (hasBeenCalled) {
return;
}
hasBeenCalled = 1;
self.duration = self.video.getDuration();
self.dispatchEvent( "durationchange" );
});
})();
if ( loadedPlayers[this.playerId] ) {
this.video = registry[this.playerId].video;
this.vidId = this.vidId || extractIdFromUrl( this._container.getAttribute( "src" ) ) || extractIdFromUri( this._container.getAttribute( "src" ) );
if (this.vidId !== registry[this.playerId].vidId ) {
this.video.cueVideoById( this.vidId );
Popcorn.getScript( "http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js", youtubeInit );
} else {
// Same video, new ctor. Force a seek to the beginning
this.previousCurrentTime = 1;
youtubeInit();
}
this.dispatchEvent( 'load' );
} else if ( this._container ) {
makeSWF.call( this, url, this._container );
} else {
// Container not yet loaded, get it on DOMDontentLoad
document.addEventListener( "DOMContentLoaded", function() {
self._container = document.getElementById( elementId );
if ( !self._container ) {
throw "Could not find container!";
}
makeSWF.call( self, url, self._container );
}, false);
}
registry[this.playerId] = this;
};
// end Popcorn.youtube.init
Popcorn.extend( Popcorn.youtube.init.prototype, {
// For internal use only.
// Register handlers to YouTube events.
registerYoutubeEventHandlers: function() {
var youcorn = this,
stateChangeHandler = 'Popcorn.youtube.stateChangeEventHandler',
errorHandler = 'Popcorn.youtube.errorEventHandler';
this.video.addEventListener( 'onStateChange', stateChangeHandler );
this.video.addEventListener( 'onError', errorHandler );
/**
* Since Flash can only call named functions, they are declared
* separately here.
*/
Popcorn.youtube.stateChangeEventHandler = function( state ) {
// In case ctor has been called many times for many ctors
// Only use latest ctor call for each player id
var self = registry[youcorn.playerId];
if ( state === YOUTUBE_STATE_UNSTARTED ) {
self.readyState = READY_STATE_HAVE_METADATA;
self.dispatchEvent( 'loadedmetadata' );
} else if ( state === YOUTUBE_STATE_ENDED ) {
self.dispatchEvent( 'ended' );
} else if ( state === YOUTUBE_STATE_PLAYING ) {
// Being able to play means current data is loaded.
if ( !this.loadedData ) {
this.loadedData = true;
self.dispatchEvent( 'loadeddata' );
}
self.readyState = READY_STATE_HAVE_CURRENT_DATA;
self.dispatchEvent( 'playing' );
} else if ( state === YOUTUBE_STATE_PAUSED ) {
self.dispatchEvent( 'pause' );
} else if ( state === YOUTUBE_STATE_BUFFERING ) {
self.dispatchEvent( 'waiting' );
} else if ( state === YOUTUBE_STATE_CUED ) {
// not handled
Popcorn.nop();
}
};
Popcorn.youtube.errorEventHandler = function( state ) {
youcorn.dispatchEvent( 'error' );
};
},
// For internal use only.
// Start current time and loading progress syncing intervals.
registerInternalEventHandlers: function() {
this.addEventListener( 'playing', function() {
this.startTimeUpdater();
});
this.addEventListener( 'loadedmetadata', function() {
this.startProgressUpdater();
});
},
play: function() {
// In case called before video is loaded, defer acting
if ( !loadedPlayers[this.playerId] ) {
this.addEventListener( "load", function() {
this.play();
});
return;
}
this.dispatchEvent( 'play' );
this.video.playVideo();
},
pause: function() {
// In case called before video is loaded, defer acting
if ( !loadedPlayers[this.playerId] ) {
this.addEventListener( "load", this.pause );
return;
}
this.video.pauseVideo();
// pause event is raised by Youtube.
},
load: function() {
// In case called before video is loaded, defer acting
if ( !loadedPlayers[this.playerId] ) {
this.addEventListener( "load", function() {
this.load();
});
return;
}
this.video.playVideo();
this.video.pauseVideo();
},
seekTo: function( time ) {
var playing = this.video.getPlayerState() == YOUTUBE_STATE_PLAYING;
this.video.seekTo( time, true );
// Prevent Youtube's behaviour to start playing video after seeking.
if ( !playing ) {
this.video.paused = true;
this.video.pauseVideo();
} else {
this.video.paused = false;
}
// Data need to be loaded again.
if ( !this.fullyLoaded ) {
this.loadedData = false;
}
// Raise event.
this.dispatchEvent( 'seeked' );
},
// Mute is toggleable
mute: function() {
// In case called before video is loaded, defer acting
if ( !loadedPlayers[this.playerId] ) {
this.addEventListener( "load", this.mute );
return;
}
if ( this.volume !== 0 ) {
this.preMuteVol = this.volume;
this.setVolume( 0 );
} else {
this.setVolume( this.preMuteVol );
}
},
// Expects beteween 0 and 1
setVolume: function( vol ) {
this.volume = this.previousVolume = vol;
this.video.setVolume( vol * 100 );
this.dispatchEvent( 'volumechange' );
},
addEventListener: function( evt, func ) {
var evtName = evt.type || evt;
if ( !this._eventListeners[evtName] ) {
this._eventListeners[evtName] = [];
}
this._eventListeners[evtName].push( func );
},
/**
* Notify event listeners about an event.
*/
dispatchEvent: function( name ) {
var evtName = name.type || name;
if ( !this._eventListeners[evtName] ) {
return;
}
var self = this;
Popcorn.forEach( this._eventListeners[evtName], function( evt ) {
evt.call( self, null );
});
},
/* Unsupported methods. */
defaultPlaybackRate: function( arg ) {
},
playbackRate: function( arg ) {
},
getBoundingClientRect: function() {
var b,
self = this;
if ( this.video ) {
b = this.video.getBoundingClientRect();
return {
bottom: b.bottom,
left: b.left,
right: b.right,
top: b.top,
// These not guaranteed to be in there
width: b.width || ( b.right - b.left ),
height: b.height || ( b.bottom - b.top )
};
} else {
b = self._container.getBoundingClientRect();
// Update bottom, right for expected values once the container loads
return {
left: b.left,
top: b.top,
width: self._target.offsetWidth,
height: self._target.offsetHeight,
bottom: b.top + self._target.offsetWidth,
right: b.left + self._target.offsetHeight
};
}
},
startTimeUpdater: function() {
var state = typeof this.video.getPlayerState != "function" ? this.readyState : this.video.getPlayerState(),
self = this,
seeked = 0;
if ( abs( this.currentTime - this.previousCurrentTime ) > timeCheckInterval ) {
// Has programatically set the currentTime
this.previousCurrentTime = this.currentTime - timeCheckInterval;
this.seekTo( this.currentTime );
seeked = 1;
} else {
this.previousCurrentTime = this.currentTime;
this.currentTime = typeof this.video.getCurrentTime != "function" ? this.currentTime : this.video.getCurrentTime();
}
if ( this.volume !== this.previousVolume ) {
this.setVolume( this.volume );
}
if ( state !== YOUTUBE_STATE_ENDED && state !== YOUTUBE_STATE_PAUSED || seeked ) {
this.dispatchEvent( 'timeupdate' );
}
if( state !== YOUTUBE_STATE_ENDED ) {
setTimeout( function() {
self.startTimeUpdater.call(self);
}, timeupdateInterval);
}
},
startProgressUpdater: function() {
var bytesLoaded = this.video.getVideoBytesLoaded(),
bytesToLoad = this.video.getVideoBytesTotal(),
self = this;
// do nothing if size is not yet determined
if ( bytesToLoad === 0 ) {
return;
}
// raise an event if load has just started
if ( !this.loadStarted ) {
this.loadStarted = true;
this.dispatchEvent( 'loadstart' );
}
// fully loaded
if ( bytesLoaded >= bytesToLoad ) {
this.fullyLoaded = true;
this.readyState = READY_STATE_HAVE_ENOUGH_DATA;
this.dispatchEvent( 'canplaythrough' );
return;
}
this.dispatchEvent( 'progress' );
setTimeout( function() {
self.startProgressUpdater.call( self );
}, progressInterval);
}
}); // end Popcorn.extend
/* Unsupported properties and events. */
/**
* Unsupported events are:
* * suspend
* * abort
* * emptied
* * stalled
* * canplay
* * seeking
* * ratechange
*/
})( Popcorn );

Просмотреть файл

@ -1,158 +1,288 @@
test( "Popcorn YouTube Plugin Event Tests", function() {
test("Update Timer", function () {
QUnit.reset();
var p2 = Popcorn.youtube( '#video2', 'http://www.youtube.com/watch?v=9oar9glUCL0' ),
expects = 12,
count = 0,
execCount = 0,
// These make sure events are only fired once
// any second call will produce a failed test
forwardStart = false,
forwardEnd = false,
backwardStart = false,
backwardEnd = false,
wrapperRunning = { one: false, two: false, };
function plus() {
if ( ++count == expects ) {
if ( ++count === expects ) {
// clean up added events after tests
Popcorn.removePlugin( "forwards" );
Popcorn.removePlugin( "backwards" );
Popcorn.removePlugin( "wrapper" );
p2.removePlugin( "exec" );
start();
}
}
QUnit.reset();
// events must be fired in this order
var expectedEvents = [
'play',
'loadeddata',
'playing',
'volumechange',
'pause',
'play',
'playing',
'seeked',
'volumechange',
'volumechange',
'playing',
'pause',
'ended'
];
var popcorn = Popcorn( Popcorn.youtube( 'video', "http://www.youtube.com/e/ac7KhViaVqc" ) ),
count = 0,
eventCount = 0,
added = [],
set1Executed = false,
set2Executed = false,
set3Executed = false,
expects = expectedEvents.length + 6,
listeners = [];
// These tests come close to 10 seconds on chrome, increasing to 15
stop( 15000 );
expect(expects);
popcorn.volume(1); // is muted later
Popcorn.plugin( "forwards", function () {
return {
start: function ( event, options ) {
// check time sync
popcorn.exec(2, function() {
ok( popcorn.currentTime() >= 2, "Check time synchronization." );
if ( !options.startFired ) {
options.startFired = true;
forwardStart = !forwardStart;
ok( forwardStart, "forward's start fired" );
plus();
ok( popcorn.video.paused == false, "Video is not paused" );
plus();
});
popcorn.exec(49, function() {
ok( popcorn.currentTime() >= 49, "Check time synchronization." );
plus();
});
popcorn.exec(40, function() {
ok( false, "This should not be run." );
});
}
},
end: function ( event, options ) {
if ( !options.endFired ) {
// register each events
for ( var i in expectedEvents ) {
(function( event ) {
// skip same listeners already added
for ( var i in added ) {
if ( added[i] == event ) {
return;
options.endFired = true;
forwardEnd = !forwardEnd;
p2.currentTime(1).play();
ok( forwardEnd, "forward's end fired" );
plus();
}
}
};
});
listeners.push( {
evt: event,
fn: popcorn.listen( event, function() {
eventCount++;
var expected = expectedEvents.shift();
if ( expected == event ) {
ok( true, "Event: "+event + " is fired." );
p2.forwards({
start: 3,
end: 4
});
Popcorn.plugin( "backwards", function () {
return {
start: function ( event, options ) {
if ( !options.startFired ) {
options.startFired = true;
backwardStart = !backwardStart;
p2.currentTime(0).play();
ok( true, "backward's start fired" );
plus();
}
},
end: function ( event, options ) {
if ( !options.endFired ) {
options.endFired = true;
backwardEnd = !backwardEnd;
ok( backwardEnd, "backward's end fired" );
plus();
p2.currentTime( 5 ).play();
}
}
};
});
p2.backwards({
start: 1,
end: 2
});
Popcorn.plugin( "wrapper", {
start: function ( event, options ) {
wrapperRunning[ options.wrapper ] = true;
},
end: function ( event, options ) {
wrapperRunning[ options.wrapper ] = false;
}
});
// second instance of wrapper is wrapping the first
p2.wrapper({
start: 6,
end: 7,
wrapper: "one"
})
.wrapper({
start: 5,
end: 8,
wrapper: "two"
})
// checking wrapper 2's start
.exec( 5, function() {
if ( execCount === 0 ) {
execCount++;
ok( wrapperRunning.two, "wrapper two is running at second 5" );
plus();
ok( !wrapperRunning.one, "wrapper one is stopped at second 5" );
plus();
} else {
ok( false, event + " is fired unexpectedly, expecting: " + expected );
}
})
});
added.push( event );
})( expectedEvents[i] );
// checking wrapper 1's start
.exec( 6, function() {
if ( execCount === 1 ) {
execCount++;
ok( wrapperRunning.two, "wrapper two is running at second 6" );
plus();
ok( wrapperRunning.one, "wrapper one is running at second 6" );
plus();
}
})
// checking wrapper 1's end
.exec( 7, function() {
// Cleanup
listeners.push( popcorn.listen( "ended", function() {
Popcorn.forEach( listeners, function ( obj ) {
popcorn.unlisten( obj.evt, obj.fn );
});
}));
if ( execCount === 2 ) {
// operations set1
popcorn.listen( 'playing', function() {
// prevent double calling
if ( set1Executed ) {
return;
execCount++;
ok( wrapperRunning.two, "wrapper two is running at second 7" );
plus();
ok( !wrapperRunning.one, "wrapper one is stopped at second 7" );
plus();
}
})
// checking wrapper 2's end
.exec( 8, function() {
// toggle volume 1 second after playing
setTimeout(function() {
popcorn.volume(0.5);
}, 1000);
if ( execCount === 3 ) {
// pause 3 seconds after playing
setTimeout(function() {
popcorn.pause();
}, 3000);
set1Executed = true;
execCount++;
ok( !wrapperRunning.two, "wrapper two is stopped at second 9" );
plus();
ok( !wrapperRunning.one, "wrapper one is stopped at second 9" );
plus();
}
});
// begin the test
popcorn.play();
p2.currentTime(3).play();
// operations set2
popcorn.listen( 'pause', function() {
if ( set2Executed ) {
return;
}
// continue to play
setTimeout(function() {
popcorn.play();
}, 500);
// seek to the end
setTimeout(function() {
popcorn.currentTime(48);
}, 1000);
set2Executed = true;
});
// operations set3
popcorn.listen( 'seeked', function() {
if ( set3Executed ) {
return;
test("Plugin Factory", function () {
QUnit.reset();
var popped = Popcorn.youtube( '#video2', 'http://www.youtube.com/watch?v=9oar9glUCL0' ),
methods = "load play pause currentTime mute volume roundTime exec removePlugin",
expects = 34, // 15*2+2+2. executor/complicator each do 15
count = 0;
function plus() {
if ( ++count == expects ) {
Popcorn.removePlugin("executor");
Popcorn.removePlugin("complicator");
start();
}
}
popcorn.volume(0.5);
expect( expects );
stop( 15000 );
popcorn.mute();
equals( popcorn.volume(), 0, "Muted" );
Popcorn.plugin("executor", function () {
return {
start: function () {
var self = this;
// These ensure that a popcorn instance is the value of `this` inside a plugin definition
methods.split(/\s+/g).forEach(function (k,v) {
ok( k in self, "executor instance has method: " + k );
plus();
});
ok( "media" in this, "executor instance has `media` property" );
plus();
ok( Object.prototype.toString.call(popped.media) === "[object Object]", "video property is a HTML DIV element" );
plus();
popcorn.mute();
ok( popcorn.volume() !== 0, "Not Muted" );
ok( "data" in this, "executor instance has `data` property" );
plus();
ok( Object.prototype.toString.call(popped.data) === "[object Object]", "data property is an object" );
plus();
equals( popcorn.volume(), 0.5, "Back to volume of 1" );
ok( "trackEvents" in this.data, "executor instance has `trackEvents` property" );
plus();
ok( Object.prototype.toString.call(popped.data.trackEvents) === "[object Object]", "executor trackEvents property is an object" )
plus();
},
end: function () {
}
};
set3Executed = true;
});
ok( "executor" in popped, "executor plugin is now available to instance" );
plus();
equals( Popcorn.registry.length, 1, "One item in the registry");
plus();
popped.executor({
start: 1,
end: 2
});
Popcorn.plugin("complicator", {
start: function ( event ) {
var self = this;
// These ensure that a popcorn instance is the value of `this` inside a plugin definition
methods.split(/\s+/g).forEach(function (k,v) {
ok( k in self, "complicator instance has method: " + k );
plus();
});
ok( "media" in this, "complicator instance has `media` property" );
plus();
ok( Object.prototype.toString.call(popped.media) === "[object Object]", "video property is a HTMLVideoElement" );
plus();
ok( "data" in this, "complicator instance has `data` property" );
plus();
ok( Object.prototype.toString.call(popped.data) === "[object Object]", "complicator data property is an object" );
plus();
ok( "trackEvents" in this.data, " complicatorinstance has `trackEvents` property" );
plus();
ok( Object.prototype.toString.call(popped.data.trackEvents) === "[object Object]", "complicator trackEvents property is an object" )
plus();
},
end: function () {
//start();
},
timeupdate: function () {
}
});
ok( "complicator" in popped, "complicator plugin is now available to instance" );
plus();
equals( Popcorn.registry.length, 2, "Two items in the registry");
plus();
popped.complicator({
start: 4,
end: 5
});
popped.currentTime(0).play();
});
test( "Popcorn YouTube Plugin Url and Duration Tests", function() {
@ -166,19 +296,19 @@ test( "Popcorn YouTube Plugin Url and Duration Tests", function() {
var count = 0,
expects = 3,
popcorn = Popcorn( Popcorn.youtube( 'video2', 'http://www.youtube.com/watch?v=9oar9glUCL0' ) );
popcorn = Popcorn.youtube( '#video2', 'http://www.youtube.com/watch?v=9oar9glUCL0' );
expect( expects );
stop( 10000 );
equals( popcorn.video.vidId, '9oar9glUCL0', 'Video id set' );
equals( popcorn.media.id, 'video2', 'Video id set' );
plus();
equals( popcorn.duration(), 0, 'Duration starts as 0');
plus();
popcorn.listen( "durationchange", function() {
notEqual( 0, popcorn.duration(), "Duration has been changed from 0" );
notEqual( popcorn.duration(), 0, "Duration has been changed from 0" );
plus();
popcorn.pause();
@ -194,47 +324,53 @@ test( "Popcorn YouTube Plugin Url Regex Test", function() {
var urlTests = [
{ name: 'standard',
url: 'http://www.youtube.com/watch?v=9oar9glUCL0',
expected: '9oar9glUCL0',
expected: 'http://www.youtube.com/watch?v=9oar9glUCL0',
},
{ name: 'share url',
url: 'http://youtu.be/9oar9glUCL0',
expected: '9oar9glUCL0',
expected: 'http://youtu.be/9oar9glUCL0',
},
{ name: 'long embed',
url: 'http://www.youtube.com/embed/9oar9glUCL0',
expected: '9oar9glUCL0',
expected: 'http://www.youtube.com/embed/9oar9glUCL0',
},
{ name: 'short embed 1 (e)',
url: 'http://www.youtube.com/e/9oar9glUCL0',
expected: '9oar9glUCL0',
expected: 'http://www.youtube.com/e/9oar9glUCL0',
},
{ name: 'short embed 2 (v)',
url: 'http://www.youtube.com/v/9oar9glUCL0',
expected: '9oar9glUCL0',
expected: 'http://www.youtube.com/v/9oar9glUCL0',
},
{ name: 'contains underscore',
url: 'http://www.youtube.com/v/GP53b__h4ew',
expected: 'GP53b__h4ew',
expected: 'http://www.youtube.com/v/GP53b__h4ew',
},
];
expect( urlTests.length );
var count = 0,
expects = urlTests.length;
expect( expects );
stop( 10000 );
for ( var t in urlTests ) {
Popcorn.forEach( urlTests, function( valuse, key ) {
var urlTest = urlTests[t],
popcorn = Popcorn( Popcorn.youtube( 'video3', urlTest.url ) );
var urlTest = urlTests[ key ],
popcorn = Popcorn.youtube( '#video3', urlTest.url );
equals( popcorn.video.vidId, urlTest.expected, 'Video id is correct for ' + urlTest.name + ': ' + urlTest.url );
popcorn.listen( "loadeddata", function() {
equals( popcorn.media.src, urlTest.expected, 'Video id is correct for ' + urlTest.name + ': ' + urlTest.url );
popcorn.pause();
// Get rid of the youtube object inside the video3, to keep things simple
var div = document.getElementById('video3');
div.removeChild(div.firstChild);
}
count++;
if ( count === expects ) {
start();
}
});
});
});
test( "Controls and Annotations toggling", function() {
@ -243,24 +379,25 @@ test( "Controls and Annotations toggling", function() {
expect( 6 );
var popcorn = Popcorn( Popcorn.youtube( "video", "http://www.youtube.com/watch?v=9oar9glUCL0" ) ),
var popcorn = Popcorn.youtube( "#video", "http://www.youtube.com/watch?v=9oar9glUCL0" ),
targetDiv = document.getElementById( "video" );
testTarget = targetDiv.querySelector( "object" ).querySelector( "param:nth-of-type( 4 )" );
testTarget = targetDiv.querySelector( "object" ).querySelector( "param[name=flashvars]" );
ok( /controls=1/.test( testTarget.value ), "controls are defaulted to 1 ( displayed )" );
ok( /iv_load_policy=1/.test( testTarget.value ), "annotations ( iv_load_policy ) are defaulted to ( enabled )" );
targetDiv.innerHTML = "";
popcorn = Popcorn( Popcorn.youtube( "video", "http://www.youtube.com/watch?v=9oar9glUCL0", { controls: 1, annotations: 1 } ) );
testTarget = targetDiv.querySelector( "object" ).querySelector( "param:nth-of-type( 4 )" );
popcorn = Popcorn.youtube( "#video", "http://www.youtube.com/watch?v=9oar9glUCL0", { controls: 1, annotations: 1 } );
testTarget = targetDiv.querySelector( "object" ).querySelector( "param[name=flashvars]" );
ok( /controls=1/.test( testTarget.value ), "controls is set to 1 ( displayed )" );
ok( /iv_load_policy=1/.test( testTarget.value ), "annotations ( iv_load_policy ) is set to 1 ( enabled )" );
targetDiv.innerHTML = "";
popcorn = Popcorn( Popcorn.youtube( "video", "http://www.youtube.com/watch?v=9oar9glUCL0", { controls: 0, annotations: 3 } ) );
testTarget = targetDiv.querySelector( "object" ).querySelector( "param:nth-of-type( 4 )" );
popcorn = Popcorn.youtube( "#video", "http://www.youtube.com/watch?v=9oar9glUCL0", { controls: 0, annotations: 3 } );
testTarget = targetDiv.querySelector( "object" ).querySelector( "param[name=flashvars]" );
ok( /controls=0/.test( testTarget.value ), "controls is set to 0 ( hidden )" );
ok( /iv_load_policy=3/.test( testTarget.value ), "annotations ( iv_load_policy ) is set to 3 ( hidden )" );

Просмотреть файл

@ -1488,6 +1488,165 @@
return parser;
};
Popcorn.player = function( name, player ) {
player = player || {};
var playerFn = function( target, src, options ) {
options = options || {};
// List of events
var date = new Date() / 1000,
baselineTime = date,
currentTime = 0,
timeout,
events = {},
// The container div of the resource
container = document.getElementById( rIdExp.exec( target ) && rIdExp.exec( target )[ 2 ] ) || document.createElement( "div" ),
basePlayer = {},
popcorn;
// copies a div into the media object
for( var val in container ) {
if ( typeof container[ val ] === "object" ) {
basePlayer[ val ] = container[ val ];
} else if ( typeof container[ val ] === "function" ) {
basePlayer[ val ] = ( function( value ) {
return function() {
return container[ value ].apply( container, arguments );
};
}( val ));
} else {
basePlayer.__defineGetter__( val, ( function( value ) {
return function() {
return container[ value ];
};
}( val )));
}
}
var timeupdate = function() {
date = new Date() / 1000;
if( !basePlayer.paused ) {
basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime );
basePlayer.dispatchEvent( "timeupdate" );
timeout = setTimeout( timeupdate, 10 );
}
baselineTime = date;
};
basePlayer.play = function() {
this.paused = false;
if ( basePlayer.readyState >= 4 ) {
baselineTime = new Date() / 1000;
basePlayer.dispatchEvent( "play" );
timeupdate();
}
};
basePlayer.pause = function() {
this.paused = true;
basePlayer.dispatchEvent( "pause" );
};
basePlayer.__defineSetter__( "currentTime", function( val ) {
// make sure val is a number
currentTime = +val;
basePlayer.dispatchEvent( "timeupdate" );
return currentTime;
});
basePlayer.__defineGetter__( "currentTime", function() {
return currentTime;
});
// Adds an event listener to the object
basePlayer.addEventListener = function( evtName, fn ) {
if ( !events[ evtName ] ) {
events[ evtName ] = [];
}
events[ evtName ].push( fn );
return fn;
};
// Can take event object or simple string
basePlayer.dispatchEvent = function( oEvent ) {
var evt,
self = this,
eventInterface,
eventName = oEvent.type;
// A string was passed, create event object
if ( !eventName ) {
eventName = oEvent;
eventInterface = Popcorn.events.getInterface( eventName );
if ( eventInterface ) {
evt = document.createEvent( eventInterface );
evt.initEvent( eventName, true, true, window, 1 );
}
}
Popcorn.forEach( events[ eventName ], function( val ) {
val.call( self, evt, self );
});
};
// Attempt to get src from playerFn parameter
basePlayer.src = src || "";
basePlayer.readyState = 0;
basePlayer.duration = 0;
basePlayer.paused = true;
basePlayer.ended = 0;
// basePlayer has no concept of sound
basePlayer.volume = 1;
basePlayer.muted = false;
if ( player._setup ) {
player._setup.call( basePlayer, options );
} else {
// there is no setup, which means there is nothing to load
basePlayer.readyState = 4;
basePlayer.dispatchEvent( 'load' );
}
popcorn = new Popcorn.p.init( basePlayer, options );
return popcorn;
};
Popcorn[ name ] = Popcorn[ name ] || playerFn;
};
// Cache references to reused RegExps
var rparams = /\?/,

Просмотреть файл

@ -3368,7 +3368,192 @@ test("dataType: XML Response", function() {
});
module( "Popcorn Player" );
test( "Base player methods", function() {
var expects = 2;
expect( expects );
stop( 10000 );
ok( Popcorn.player, "Popcorn.player function exists" );
Popcorn.player( "newplayer" );
ok( Popcorn.newplayer, "Popcorn.player registers new players" );
start();
});
test( "Base player functionality", function() {
Popcorn.player( "baseplayer" );
//QUnit.reset();
var p2 = Popcorn.baseplayer( '#video' ),
expects = 12,
count = 0,
execCount = 0,
// These make sure events are only fired once
// any second call will produce a failed test
forwardStart = false,
forwardEnd = false,
backwardStart = false,
backwardEnd = false,
wrapperRunning = { one: false, two: false, };
function plus() {
if ( ++count === expects ) {
// clean up added events after tests
Popcorn.removePlugin( "forwards" );
Popcorn.removePlugin( "backwards" );
Popcorn.removePlugin( "wrapper" );
p2.removePlugin( "exec" );
start();
}
}
// These tests come close to 10 seconds on chrome, increasing to 15
stop( 15000 );
Popcorn.plugin( "forwards", function () {
return {
start: function ( event, options ) {
if ( !options.startFired ) {
options.startFired = true;
forwardStart = !forwardStart;
ok( forwardStart, "forward's start fired" );
plus();
}
},
end: function ( event, options ) {
if ( !options.endFired ) {
options.endFired = true;
forwardEnd = !forwardEnd;
p2.currentTime(1).play();
ok( forwardEnd, "forward's end fired" );
plus();
}
}
};
});
p2.forwards({
start: 3,
end: 4
});
Popcorn.plugin( "backwards", function () {
return {
start: function ( event, options ) {
if ( !options.startFired ) {
options.startFired = true;
backwardStart = !backwardStart;
p2.currentTime(0).play();
ok( true, "backward's start fired" );
plus();
}
},
end: function ( event, options ) {
if ( !options.endFired ) {
options.endFired = true;
backwardEnd = !backwardEnd;
ok( backwardEnd, "backward's end fired" );
plus();
p2.currentTime( 5 ).play();
}
}
};
});
p2.backwards({
start: 1,
end: 2
});
Popcorn.plugin( "wrapper", {
start: function ( event, options ) {
wrapperRunning[ options.wrapper ] = true;
},
end: function ( event, options ) {
wrapperRunning[ options.wrapper ] = false;
}
});
// second instance of wrapper is wrapping the first
p2.wrapper({
start: 6,
end: 7,
wrapper: "one"
})
.wrapper({
start: 5,
end: 8,
wrapper: "two"
})
// checking wrapper 2's start
.exec( 5, function() {
if ( execCount === 0 ) {
execCount++;
ok( wrapperRunning.two, "wrapper two is running at second 5" );
plus();
ok( !wrapperRunning.one, "wrapper one is stopped at second 5" );
plus();
}
})
// checking wrapper 1's start
.exec( 6, function() {
if ( execCount === 1 ) {
execCount++;
ok( wrapperRunning.two, "wrapper two is running at second 6" );
plus();
ok( wrapperRunning.one, "wrapper one is running at second 6" );
plus();
}
})
// checking wrapper 1's end
.exec( 7, function() {
if ( execCount === 2 ) {
execCount++;
ok( wrapperRunning.two, "wrapper two is running at second 7" );
plus();
ok( !wrapperRunning.one, "wrapper one is stopped at second 7" );
plus();
}
})
// checking wrapper 2's end
.exec( 8, function() {
if ( execCount === 3 ) {
execCount++;
ok( !wrapperRunning.two, "wrapper two is stopped at second 9" );
plus();
ok( !wrapperRunning.one, "wrapper one is stopped at second 9" );
plus();
}
});
p2.currentTime(3).play();
});
module("Popcorn Parser");