2 * the mvPlayList object code
3 * only included if playlist object found
5 * part of mwEmbed media projects see:
6 * http://www.mediawiki.org/wiki/Media_Projects_Overview
8 * @author: Michael Dale mdale@wikimedia.org
11 var mv_default_playlist_attributes = {
12 //playlist attributes :
19 //playlist user controlled features
24 //enable sequencer? (only display top frame no navigation or accompanying text
27 //the call back rate for animations and internal timers in ms: 33 is about 30 frames a second:
28 var MV_ANIMATION_CB_RATE = 33;
31 //10 possible colors for clips: (can be in hexadecimal)
32 var mv_clip_colors = new Array('aqua', 'blue', 'fuchsia', 'green', 'lime', 'maroon', 'navy', 'olive', 'purple', 'red');
34 //the base url for requesting stream metadata
35 if(typeof wgServer=='undefined'){
36 var defaultMetaDataProvider = 'http://metavid.org/overlay/archive_browser/export_cmml?stream_name=';
38 var defaultMetaDataProvider = wgServer + wgScript + '?title=Special:MvExportStream&feed_format=roe&stream_name=';
41 * The playlist Object implements ~most~ of embedVideo but we don't inherit (other than to use the control builder)
42 * because pretty much every function has to be changed for the playlist context
44 var mvPlayList = function(element) {
45 return this.init(element);
47 //set up the mvPlaylist object
48 mvPlayList.prototype = {
49 instanceOf:'mvPlayList',
59 loading_external_data:true, //if we are loading external data (set to loading by default)
62 playlist_buffer_time: 20, // how many seconds of future clips we should buffer
64 interface_url:null, //the interface url
66 default_track:null, // the default track to add clips to.
67 //the layout for the playlist object
70 clip_desc:.63, //displays the clip description
71 clip_aspect:1.33, // 4/3 video aspect ratio
72 seq:.25, //display clip thumbnails
73 seq_thumb:.25, //size for thumbnails (same as seq by default)
74 seq_nav:0, //for a nav bar at the base (currently disabled)
75 //some pl_layout info:
79 //embed object type support system;
85 'volume_control':true,
88 'playlist_swap_loader':true //if the object supports playlist functions
90 init : function(element){
91 js_log('mvPlayList:init:');
93 this.default_track=null;
95 this.activeClipList = new activeClipList();
96 //add default track & default track pointer:
97 this.tracks[0]= new trackObj({'inx':0});
98 this.default_track = this.tracks[0];
100 //get all the attributes:
101 for(var attr in mv_default_playlist_attributes){
102 if( element.getAttribute(attr) ){
103 this[attr]=element.getAttribute(attr);
104 //js_log('attr:' + attr + ' val: ' + video_attributes[attr] +" "+'elm_val:' + element.getAttribute(attr) + "\n (set by elm)");
106 this[attr]=mv_default_playlist_attributes[attr];
107 //js_log('attr:' + attr + ' val: ' + video_attributes[attr] +" "+ 'elm_val:' + element.getAttribute(attr) + "\n (set by attr)");
110 //make sure height and width are int:
111 this.width =parseInt(this.width);
112 this.height=parseInt(this.height);
114 //if style is set override width and height
115 if(element.style.width)this.width = parseInt(element.style.width.replace('px',''));
116 if(element.style.height)this.height = parseInt(element.style.height.replace('px',''));
118 //if controls=false hide the title and the controls:
119 if(this.controls===false){
120 this.pl_layout.control_height=0;
121 this.pl_layout.title_bar_height=0;
124 //the element has now been swapped into the dom:
125 on_dom_swap:function(){
126 js_log('pl: dom swap');
127 //get and load the html:
130 //run inheritEmbedObj on every clip (we have changed the playback method)
131 inheritEmbedObj:function(){
132 $j.each(this.tracks, function(i,track){
133 track.inheritEmbedObj();
136 doOptionsHTML:function(){
137 //grab "options" use current clip:
138 this.cur_clip.embed.doOptionsHTML();
140 //pulls up the video editor inline
142 //black out the page:
143 //$j('body').append('<div id="ui-widget-overlay"/> <div id="modalbox" class="ui-widget ui-widget-content ui-corner-all modal_editor">' );
144 $j('body').append('<div class="ui-widget-overlay" style="width: 100%; height: 100%px; z-index: 10;"></div>');
145 $j('body').append('<div id="sequencer_target" style="z-index:11;position:fixed;top:10px;left:10px;right:10px;bottom:10px;" ' +
146 'class="ui-widget ui-widget-content ui-corner-all"></div>');
148 //@@todo clone the playlist (for faster startup)
150 * var this_plObj_Clone = $j('#'+this.id).get(0).cloneNode(true);
151 * this_plObj_Clone.sequencer=true;
152 * this_plObj_Clone.id= 'seq_plobj';
156 $j("#sequencer_target").sequencer({
157 "mv_pl_src" : this.src
161 showPlayerselect:function(){
162 this.cur_clip.embed.showPlayerselect();
164 closeDisplayedHTML:function(){
165 this.cur_clip.embed.closeDisplayedHTML();
167 showDownload:function(){
168 this.cur_clip.embed.showDownload();
170 showShare:function(){
171 var embed_code = '<script type="text/javascript" '+
172 'src="'+mv_embed_path+'mv_embed.js"></script> '+"\n" +
173 '<playlist id="'+this.id+'" ';
175 embed_code+='src="'+this.src+'" />';
177 embed_code+='>'+"\n";
178 embed_code+= this.data.htmlEntities();
179 embed_code+='<playlist/>';
181 this.cur_clip.embed.showShare( embed_code );
183 timedTextSources:function(){
186 getPlaylist:function(){
187 js_log("f:getPlaylist: " + this.srcType );
188 //@@todo lazy load plLib
189 eval('var plObj = '+this.srcType+'Playlist;');
190 //import methods from the plObj to this
191 for(var method in plObj){
192 //js parent preservation for local overwritten methods
193 if(this[method])this['parent_' + method] = this[method];
194 this[method]=plObj[method];
195 js_log('inherit:'+ method);
198 if(typeof this.doParse != 'function'){
199 js_log('error: method doParse not found in plObj'+ this.srcType);
203 if(typeof this.doParse == 'function'){
204 if( this.doParse() ){
205 this.doWhenParseDone();
207 js_log("error: failed to parse playlist");
209 //error or parse needs to do ajax requests
213 doNativeWarningCheck:function(){
214 var clip = this.default_track.clips[0];
216 return clip.embed.doNativeWarningCheck();
219 doWhenParseDone:function(){
220 js_log('f:doWhenParseDone');
221 //do additional init for clips:
224 _this.clip_ready_count=0;
225 for( var i in this.default_track.clips ){
226 var clip = this.default_track.clips[i];
227 if(clip.embed.load_error){
228 var error = clip.embed.load_error;
229 //break on any clip we can't playback:
232 if( clip.embed.ready_to_play ){
233 _this.clip_ready_count++;
236 //js_log('clip sources count: '+ clip.embed.media_element.sources.length);
237 clip.embed.on_dom_swap();
238 if( clip.embed.loading_external_data==false &&
239 clip.embed.init_with_sources_loadedDone==false){
240 clip.embed.init_with_sources_loaded();
244 //@@todo for some plugins we have to conform types of clips
245 // ie vlc can play flash _followed_by_ ogg _followed_by_ whatever
247 // native ff 3.1a2 can only play ogg
249 this.load_error=error;
251 }else if( _this.clip_ready_count == _this.getClipCount() ){
252 js_log("done init all clips: " + _this.clip_ready_count + ' = ' + _this.getClipCount());
253 this.doWhenClipLoadDone();
255 js_log("only "+ _this.clip_ready_count +" clips done, scheduling callback:");
256 var doParseDoneCheck = function(){
257 _this.doWhenParseDone();
259 if( !mvJsLoader.load_error ) //re-issue request if no load error:
260 setTimeout(doParseDoneCheck, 100);
263 doWhenClipLoadDone:function(){
264 js_log('mvPlaylist:doWhenClipLoadDone');
265 this.ready_to_play = true;
266 this.loading = false;
269 getDuration:function( regen ){
270 //js_log("GET PL DURRATION for : "+ this.tracks[this.default_track_id].clips.length + 'clips');
271 if(!regen && this.pl_duration)
272 return this.pl_duration;
275 $j.each( this.default_track.clips, function( i, clip ){
277 clip.dur_offset = durSum;
278 //only calculate the solo Duration if a smil clip that could contain a transition:
279 if( clip.instanceOf == 'mvSMILClip' ){
280 //don't include transition time (for playlist_swap_loader compatible clips)
281 durSum += clip.getSoloDuration();
283 durSum += clip.getDuration();
286 js_log("ERROR: clip " +clip.id + " not ready");
289 this.pl_duration=durSum;
290 //js_log("return dur: " + this.pl_duration);
291 return this.pl_duration;
293 getTimeReq:function(){
294 //playlist does not really support time request atm ( in theory in the future we could embed playlists with temporal urls)
295 return '0:0:0/' + seconds2npt( this.getDuration() );
297 getDataSource:function(){
298 js_log("f:getDataSource "+ this.src);
299 //determine the type / first is it m3u or xml?
300 var pl_parent = this;
301 this.makeURLAbsolute();
303 do_request(this.src, function(data){
305 pl_parent.getSourceType();
309 getSourceType:function(){
310 js_log('data type of: '+ this.src + ' = ' + typeof (this.data) + "\n"+ this.data);
312 //if not external use different detection matrix
313 if(this.loading_external_data){
314 if( typeof this.data == 'object' ){
316 //object assume xml (either xspf or rss)
317 plElm = this.data.getElementsByTagName('playlist')[0];
319 if(plElm.getAttribute('xmlns')=='http://xspf.org/ns/0/'){
320 this.srcType ='xspf';
323 //check itunes style rss "items"
324 rssElm = this.data.getElementsByTagName('rss')[0];
326 if(rssElm.getAttribute('xmlns:itunes')=='http://www.itunes.com/dtds/podcast-1.0.dtd'){
327 this.srcType='itunes';
330 //check for smil tag:
331 smilElm = this.data.getElementsByTagName('smil')[0];
333 //don't check dtd yet.. (have not defined the smil subset)
336 }else if(typeof this.data == 'string'){
338 //look at the first line:
339 var first_line = this.data.substring(0, this.data.indexOf("\n"));
340 js_log('first line: '+ first_line);
342 if(first_line.indexOf('#EXTM3U')!=-1){
343 this.srcType = 'm3u';
344 }else if(first_line.indexOf('<smil')!=-1){
345 //@@todo parse string
346 this.srcType = 'smil';
351 js_log('is of type:'+ this.srcType);
354 //unknown playlist type
355 js_log('unknown playlist type?');
357 this.innerHTML= 'error: unknown playlist type at url:<br> ' + this.src;
359 this.innerHTML='error: unset src or unknown inline playlist data<br>';
363 //simple function to make a path into an absolute url if its not already
364 makeURLAbsolute:function(){
366 if(this.src.indexOf('://')==-1){
367 var purl = parseUri(document.URL);
368 if(this.src.charAt(0)=='/'){
369 this.src = purl.protocol +'://'+ purl.host + this.src;
371 this.src= purl.protocol +'://'+ purl.host + purl.directory + this.src;
376 //set up minimal media_element emulation:
379 supports_url_time_encoding:true
382 //@@todo needs to update for multi-track clip counts
383 getClipCount:function(){
384 return this.default_track.clips.length;
387 //takes in the playlist
388 // inherits all the properties
389 // swaps in the playlist object html/interface div
391 js_log('mvPlaylist:getHTML: loading:' + this.loading);
393 $j('#'+this.id).html('loading playlist<blink>...</blink>');
394 if( this.loading_external_data ){
395 //load the data source chain of functions (to update the innerHTML)
396 this.getDataSource();
398 //detect datatype and parse directly:
399 this.getSourceType();
402 //check for empty playlist otherwise renderDisplay:
403 if(this.default_track.getClipCount()==0){
404 $j(this).html('empty playlist');
407 this.renderDisplay();
411 renderDisplay:function(){
412 js_log('mvPlaylist:renderDisplay:: track length: ' +this.default_track.getClipCount() );''
415 //setup layout for title and dc_ clip container
418 //add the playlist controls:
420 //append container and videoPlayer;
421 $j(this).html('<div id="dc_'+this.id+'" style="width:'+this.width+'px;' +
422 'height:'+(this.height+this.pl_layout.title_bar_height + this.pl_layout.control_height)+'px;position:relative;">' +
424 if(this.controls==true){
425 //append title & controler:
426 $j('#dc_'+_this.id).append(
427 '<div style="font-size:13px;border:solid thin;width:'+this.width+'px;" id="ptitle_'+this.id+'"></div>' +
428 '<div class="videoPlayer" style="position:absolute;top:'+(_this.height+_this.pl_layout.title_bar_height+4)+'px">' +
429 '<div id="mv_embedded_controls_'+_this.id+'" class="ui-widget ui-corner-bottom ui-state-default controls" '+
430 'style="width:' + _this.width + 'px" >' +
431 _this.getControlsHTML() +
436 //add the play button:
437 $j('#dc_'+_this.id).append(
438 this.cur_clip.embed.getPlayButton()
440 //once the controls are in the DOM add hooks:
441 ctrlBuilder.addControlHooks(this);
443 //just append the video:
444 $j('#dc_'+_this.id).append(
445 '<div class="videoPlayer" style="position:absolute;top:'+(_this.height+_this.pl_layout.title_bar_height+4)+'px"></div>'
448 this.setupClipDisplay();
450 //update the title and status bar
451 this.updateBaseStatus();
453 setupClipDisplay:function(){
454 js_log('mvPlaylist:setupClipDisplay:: clip len:'+ this.default_track.clips.length);
456 $j.each(this.default_track.clips, function(i, clip){
457 var cout = '<div class="clip_container cc_" id="clipDesc_'+clip.id+'" '+
458 'style="display:none;position:absolute;text-align: center;width:'+_this.width + 'px;'+
459 'height:'+(_this.height )+'px;'+
460 'top:' + this.title_bar_height + 'px;left:0px;';
462 cout+='border:solid thin black;';
465 $j('#dc_'+_this.id).append( cout );
466 //update the embed html:
467 clip.embed.height=_this.height;
468 clip.embed.width=_this.width;
469 clip.embed.play_button=false;
471 clip.embed.getHTML();//get the thubnails for everything
474 'position':"absolute",
478 if($j('#clipDesc_'+clip.id).length != 0){
479 js_log("should set: #clipDesc_"+clip.id + ' to: ' + $j(clip.embed).html() )
480 $j('#clipDesc_'+clip.id).append( clip.embed );
482 js_log('cound not find: clipDesc_'+clip.id);
486 $j('#clipDesc_'+this.cur_clip.id).css( { display:'inline' } );
488 updateThumbPerc:function( perc ){
490 var float_sec = ( this.getDuration() * perc );
491 this.updateThumbTime( float_sec );
493 updateThumbTime:function( float_sec ){
494 //update display & cur_clip:
496 var clip_float_sec=0;
497 //js_log('seeking clip: ');
498 for(var i in this.default_track.clips){
499 var clip = this.default_track.clips[i];
500 if( (clip.getDuration() + pl_sum_time) >= float_sec ){
501 if(this.cur_clip.id != clip.id){
502 $j('#clipDesc_'+this.cur_clip.id).hide();
503 this.cur_clip = clip;
504 $j('#clipDesc_'+this.cur_clip.id).show();
508 pl_sum_time+=clip.getDuration();
511 //issue thumbnail update request: (if plugin supports it will render out frame
512 // if not then we do a call to the server to get a new jpeg thumbnail
513 this.cur_clip.embed.updateThumbTime( float_sec - pl_sum_time );
515 this.cur_clip.embed.currentTime = (float_sec -pl_sum_time) + this.cur_clip.embed.start_offset ;
516 this.cur_clip.embed.seek_time_sec = (float_sec -pl_sum_time );
518 //render effects ontop: (handled by doSmilActions)
519 this.doSmilActions( single_line = true );
521 updateBaseStatus:function(){
523 js_log('Playlist:updateBaseStatus');
524 $j('#ptitle_'+this.id).html(''+
525 '<b>' + this.title + '</b> '+
526 this.getClipCount()+' clips, <i>'+
527 seconds2npt( this.getDuration() ) + '</i>');
529 //only show the inline edit button if mediaWiki write API is enabled:
531 //should probably be based on if we have a provider api url
532 if( typeof wgEnableWriteAPI != 'undefined'){
533 $j( $j.btnHtml('edit', 'editBtn_'+this.id, 'pencil',
534 {'style':'position:absolute;right:0;;font-size:x-small;height:10px;margin-bottom:0;padding-bottom:7px;padding-top:0;'} )
539 }).appendTo('#ptitle_'+this.id);
540 $j('.editBtn_'+this.id).btnBind();
542 //render out the dividers on the timeline:
543 this.colorPlayHead();
545 this.setStatus( '0:0:00/' + seconds2npt( this.getDuration() ) );
547 /*setStatus override (could call the jquery directly) */
548 setStatus:function(value){
549 $j('#mv_time_'+this.id).html( value );
551 setSliderValue:function(value){
552 //slider is on 1000 scale:
553 var val = parseInt( value *1000 );
554 $j('#mv_play_head_' + this.id).slider('value', val);
556 getPlayHeadPos: function(prec_done){
558 if($j('#mv_seeker_'+this.id).length==0){
559 //js_log('no playhead so we can\'t get playhead pos' );
562 var track_len = $j('#mv_seeker_'+this.id).css('width').replace(/px/, '');
563 //assume the duration is static and present at .duration during playback
564 var clip_perc = this.cur_clip.embed.duration / this.getDuration();
565 var perc_offset =time_offset = 0;
566 for(var i in this.default_track.clips){
567 var clip = this.default_track.clips[i];
568 if(this.cur_clip.id ==clip.id)break;
569 perc_offset+=(clip.embed.duration / _this.getDuration());
570 time_offset+=clip.embed.duration;
572 //run any update time line hooks:
573 if(this.update_tl_hook){
574 var cur_time_ms = time_offset + Math.round(this.cur_clip.embed.duration*prec_done);
575 if(typeof update_tl_hook =='function'){
576 this.update_tl_hook(cur_time_ms);
578 //string type passed use eval:
579 eval(this.update_tl_hook+'('+cur_time_ms+');');
583 //handle offset hack @@todo fix so this is not needed:
584 if(perc_offset > .66)
585 perc_offset+=( 8/track_len );
586 //js_log('perc:'+ perc_offset +' c:'+ clip_perc + '*' + prec_done + ' v:'+(clip_perc*prec_done));
587 return perc_offset + ( clip_perc * prec_done );
589 //attempts to load the embed object with the playlist
590 loadEmbedPlaylist: function(){
591 //js_log('load playlist');
593 /** mannages the loading of future clips
594 * called regurally while we are playing clips
596 * load works like so:
597 * if the current clip is full loaded
598 * load clips untill buffredEndTime < playlist_buffer_time load next
600 * this won't work so well with time range loading for smil (need to work on that)
602 loadFutureClips:function(){
603 /*if( this.cur_clip.embed.bufferedPercent == 1){
604 //set the buffer to the currentTime - duration
605 var curBuffredTime = this.cur_clip.getDuration() - this.cur_clip.embed.currentTime;
607 if(curBuffredTime < 0)
610 js_log( "curBuffredTime:: " + curBuffredTime );
611 if( curBuffredTime < this.playlist_buffer_time ){
612 js_log(" we only have " + curBuffredTime + ' buffed but we need: ' + this.playlist_buffer_time);
614 for(var inx = this.cur_clip.order + 1; inx < this.default_track.clips.length; inx++ ){
615 var cClip = this.default_track.getClip( inx );
617 //check if the clip is already loaded (add its duration)
618 if( cClip.embed.bufferedPercent == 1){
619 curBuffredTime += cClip.embed.getDuration();
621 //check if we still have to load a resource:
622 if( curBuffredTime < this.playlist_buffer_time ){
623 //issue the load request
624 if( cClip.embed.networkState==0 ){
627 break; //check back next time
633 //called to play the next clip if done call onClipDone
634 playNext: function(){
635 //advance the playhead to the next clip
636 var next_clip = this.getNextClip();
639 js_log('play next with no next clip... must be done:');
643 //@@todo where the plugin supports pre_loading future clips and manage that in javascript
645 this.cur_clip.embed.stop();
647 this.updateCurrentClip(next_clip);
649 this.cur_clip.embed.play();
651 onClipDone:function(){
652 js_log("pl onClipDone");
653 this.cur_clip.embed.stop();
655 updateCurrentClip:function( new_clip ){
656 js_log('f:updateCurrentClip:'+new_clip.id);
657 //make sure we are not switching to the current
658 if( this.cur_clip.id == new_clip.id ){
659 js_log('trying to updateCurrentClip to same clip');
663 //keep the active play clip in sync (stop the other clip)
665 if( !this.cur_clip.embed.isStoped() )
666 this.cur_clip.embed.stop();
667 this.activeClipList.remove(this.cur_clip )
670 this.activeClipList.add( new_clip );
673 $j('#clipDesc_'+this.cur_clip.id).hide();
674 this.cur_clip=new_clip;
675 $j('#clipDesc_'+this.cur_clip.id).show();
676 //update the playhead:
677 this.setSliderValue( this.cur_clip.dur_offset / this.getDuration() );
679 playPrev: function(){
680 //advance the playhead to the previous clip
681 var prev_clip = this.getPrevClip();
683 js_log("tried to play PrevClip with no prev Clip.. setting prev_clip to start clip");
684 prev_clip = this.start_clip;
686 //@@todo we could do something fancy like use playlist for sets of clips where supported.
687 // or in cases where the player nativly supports the playlist format we can just pass it in (ie m3u or xspf)
688 if(this.cur_clip.embed.supports['playlist_swap_loader']){
689 //where the plugin supports pre_loading future clips and manage that in javascript
691 this.cur_clip.embed.pause();
693 this.updateCurrentClip(prev_clip);
694 this.cur_clip.embed.play();
696 js_log('do prev hard embed swap');
697 this.switchPlayingClip(prev_clip);
700 switchPlayingClip:function(new_clip){
701 //swap out the existing embed code for next clip embed code
702 $j('#mv_ebct_'+this.id).empty();
703 new_clip.embed.width=this.width;
704 new_clip.embed.height=this.height;
705 //js_log('set embed to: '+ new_clip.embed.getEmbedObj());
706 $j('#mv_ebct_'+this.id).html( new_clip.embed.getEmbedObj() );
707 this.cur_clip=new_clip;
709 this.cur_clip.embed.pe_postEmbedJS();
715 //hide the playlist play button:
716 $j('#big_play_link_'+this.id).hide();
718 //un-pause if paused:
722 //update the control:
723 this.start_clip = this.cur_clip;
724 this.start_clip_src= this.cur_clip.src;
726 if(this.cur_clip.embed.supports['playlist_swap_loader'] ){
727 //set the cur_clip to active
728 this.activeClipList.add(this.cur_clip);
732 // * mv_playlist smil extension, manages transitions animations overlays etc.
733 //js_log('clip obj supports playlist swap_loader (ie playlist controlled playback)');
734 //@@todo pre-load each clip:
735 //play all active clips (playlist_swap_loader can have more than one clip active)
736 $j.each(this.activeClipList.getClipList(), function(inx, clip){
739 }else if(this.cur_clip.embed.supports['playlist_driver']){
740 //js_log('playlist_driver');
741 //embedObject is feed the playlist info directly and manages next/prev
742 this.cur_clip.embed.playMovieAt( this.cur_clip.order );
744 //not much playlist support just play the first clip:
745 //js_log('basic play');
747 this.cur_clip.embed.play();
749 //start up the playlist monitor
753 * the load function loads all the clips in order
756 //do nothing right now)
758 toggleMute:function(){
759 this.cur_clip.embed.toggleMute();
762 //js_log('f:pause: playlist');
764 this.pauseTime = this.currentTime;
766 //js_log('pause time: '+ this.pauseTime + ' call embed pause:');
768 //pause all the active clips:
769 $j.each(this.activeClipList.getClipList(), function(inx, clip){
773 //@@todo mute across all child clips:
774 toggleMute:function(){
775 var this_id = (this.pc!=null)?this.pc.pp.id:this.id;
778 $j('#volume_control_'+this_id + ' span').removeClass('ui-icon-volume-off').addClass('ui-icon-volume-on');
779 $j('#volume_bar_'+this_id).slider('value', 100);
780 this.updateVolumen(1);
783 $j('#volume_control_'+this_id + ' span').removeClass('ui-icon-volume-on').addClass('ui-icon-volume-off');
784 $j('#volume_bar_'+this_id).slider('value', 0);
785 this.updateVolumen(0);
787 js_log('f:toggleMute::' + this.muted);
789 updateVolumen:function(perc){
790 js_log('update volume not supported with current playback type');
792 fullscreen:function(){
793 this.cur_clip.embed.fullscreen();
795 //playlist stops playback for the current clip (and resets state for start clips)
798 /*js_log("pl stop:"+ this.start_clip.id + ' c:'+this.cur_clip.id);
800 if(this.start_clip.id!=this.cur_clip.id){
801 //restore clipDesc visibility & hide desc for start clip:
802 $j('#clipDesc_'+this.start_clip.id).html('');
803 this.start_clip.getDetail();
804 $j('#clipDesc_'+this.start_clip.id).css({display:'none'});
805 this.start_clip.setBaseEmbedDim(this.start_clip.embed);
806 //equivalent of base stop
807 $j('#'+this.start_clip.embed.id).html(this.start_clip.embed.getThumbnailHTML());
808 this.start_clip.embed.thumbnail_disp=true;
810 //empty the play-back container
811 $j('#mv_ebct_'+this.id).empty();*/
813 //stop all the clips: monitor:
814 window.clearInterval( this.smil_monitorTimerId );
815 /*for (var i=0;i<this.clips.length;i++){
816 var clip = this.clips[i];
819 $j('#clipDesc_'+clip.id).hide();
822 //stop, hide and remove all active clips:
823 $j.each(this.activeClipList.getClipList(), function(inx, clip){
826 $j('#clipDesc_'+clip.id).hide();
827 _this.activeClipList.remove(clip);
830 //set the current clip to the first clip:
832 this.cur_clip = this.start_clip;
833 //display the first clip thumb:
834 this.cur_clip.embed.stop();
836 $j('#'+this.id+' .clip_container').hide();
837 //show the first/current clip:
838 $j('#clipDesc_'+this.cur_clip.id).show();
840 //reset the currentTime:
841 this.currentTime = 0;
843 this.setSliderValue( 0 );
844 //FIXME still some issues with "stoping" and reseting the playlist
847 js_log('pl:doSeek:' + v + ' sts:' + this.seek_time_sec );
851 //jump to the clip in the current percent.
853 var next_perc_offset=0;
854 for(var i in _this.default_track.clips){
855 var clip = _this.default_track.clips[i];
856 next_perc_offset+=( clip.getDuration() / _this.getDuration()) ;
857 //js_log('on ' + clip.getDuration() +' next_perc_offset:'+ next_perc_offset);
858 if( next_perc_offset > v ){
859 //pass along the relative percentage to embed object:
860 //js_log('seek:'+ v +' - '+perc_offset + ') / (' + next_perc_offset +' - '+ perc_offset);
861 var relative_perc = (v -perc_offset) / (next_perc_offset - perc_offset);
862 //update the current clip:
863 _this.updateCurrentClip( clip );
865 //update the clip relative seek_time_sec
866 _this.cur_clip.embed.doSeek( relative_perc );
870 perc_offset = next_perc_offset;
873 setCurrentTime: function(pos, callback){
874 js_log('pl:setCurrentTime:' + pos + ' sts:' + this.seek_time_sec );
878 //jump to the clip at pos
879 var currentOffset = 0;
881 for (var i in _this.default_track.clips) {
882 var clip = _this.default_track.clips[i];
883 nextTime = clip.getDuration();
884 if (currentOffset + nextTime > pos) {
885 //update the clip relative seek_time_sec
886 clipTime = pos - currentOffset;
887 if (_this.cur_clip.id != clip.id) {
888 _this.updateCurrentClip( clip );
890 _this.cur_clip.embed.setCurrentTime(clipTime, function(){
894 _this.currentTime = pos;
895 _this.doSmilActions();
897 currentOffset += nextTime;
900 //gets playlist controls large control height for sporting
901 //next prev button and more status display
902 getControlsHTML:function(){
903 //get controls from current clip (add some playlist specific controls:
904 return ctrlBuilder.getControls( this );
906 //ads colors/dividers between tracks
907 colorPlayHead: function(){
910 if( !_this.mv_seeker_width)
911 _this.mv_seeker_width = $j('#mv_play_head_'+_this.id).width();
913 if( !_this.track_len )
914 _this.track_len = $j('#mv_play_head_'+_this.id).width();
917 var pl_duration = _this.getDuration();
922 //js_log("do play head total dur: "+pl_duration );
923 $j.each(this.default_track.clips, function(i, clip){
924 //(use getSoloDuration to not include transitions and such)
925 var perc = ( clip.getSoloDuration() / pl_duration );
926 var pwidth = Math.round( perc * _this.track_len);
927 //js_log('pstatus:c:'+ clip.getDuration() + ' of '+ pl_duration+' %:' + perc + ' width: '+ pwidth + ' of total: ' + _this.track_len);
928 //var pwidth = Math.round( perc * _this.track_len - (_this.mv_seeker_width*perc) );
930 //add the buffer child indicator:
931 var barHtml= '<div id="cl_status_' + clip.embed.id + '" class="cl_status" style="' +
932 'left:'+cur_pixle +'px;'+
933 'width:'+pwidth + 'px;';
934 //set left or right border based on track pos
935 barHtml+=( i == _this.default_track.getClipCount()-1 )?
936 'border-left:solid thin black;':
937 'border-right:solid thin black;';
938 barHtml+= 'filter:alpha(opacity=40);'+
939 '-moz-opacity:.40;">';
941 barHtml+= ctrlBuilder.getMvBufferHtml();
945 //background:#DDD +clip.getColor();
947 $j('#mv_play_head_'+_this.id).append(barHtml);
949 //js_log('offset:' + cur_pixle +' width:'+pwidth+' add clip'+ clip.id + ' is '+clip.embed.getDuration() +' = ' + perc +' of ' + _this.track_len);
953 //@@todo currently not really in use
954 setUpHover:function(){
955 js_log('Setup Hover');
956 //set up hover for prev,next
958 var tw = th*this.pl_layout.clip_aspect;
960 $j('#mv_prev_link_'+_this.id+',#mv_next_link_'+_this.id).hover(function() {
961 var clip = (this.id=='mv_prev_link_'+_this.id) ? _this.getPrevClip() : _this.getNextClip();
963 return js_log('missing clip for Hover');
964 //get the position of #mv_perv|next_link:
965 var loc = getAbsolutePos(this.id);
966 //js_log('Hover: x:'+loc.x + ' y:' + loc.y + ' :'+clip.img);
967 $j("body").append('<div id="mv_Athub" style="position:absolute;' +
968 'top:'+loc.y+'px;left:'+loc.x+'px;width:'+tw+'px;height:'+th+'px;">'+
969 '<img style="border:solid 2px '+clip.getColor()+';position:absolute;top:0px;left:0px;" width="'+tw+'" height="'+th+'" src="'+clip.img+'"/>'+
972 $j('#mv_Athub').remove();
975 //@@todo we need to move a lot of this track logic like "cur_clip" to the track Obj
976 // and have the playlist just drive the tracks.
977 getNextClip:function( track ){
979 track = this.default_track;
980 var tc = parseInt(this.cur_clip.order) + 1;
982 if( tc > track.getClipCount() -1 )
983 return false; // out of range
985 return track.getClip( tc );
987 getPrevClip:function( track ) {
989 track = this.default_track;
990 var tc = parseInt(this.cur_clip.order) - 1;
993 return track.getClip( tc );
996 * generic add Clip to ~default~ track
998 addCliptoTrack: function(clipObj, pos){
999 if( typeof clipObj['track_id'] =='undefined'){
1000 var track = this.default_track;
1002 var track = this.tracks[ clipObj.track_id ]
1004 js_log('add clip:' + clipObj.id +' to track: at:' + pos);
1005 //set the first clip to current (maybe deprecated )
1006 if(clipObj.order==0){
1007 if(!this.cur_clip)this.cur_clip=clipObj;
1009 track.addClip(clipObj, pos);
1011 swapClipDesc: function(req_clipID, callback){
1012 //hide all but the requested
1014 js_log('r:'+req_clipID+' cur:'+_this.id);
1015 if(req_clipID==_this.cur_clip.id){
1016 js_log('no swap to same clip');
1020 $j.each(this.default_track.clips, function(i, clip){
1021 if(clip.id!=req_clipID){
1022 //fade out if display!=none already
1023 if($j('#clipDesc_'+clip.id).css('display')!='none'){
1024 $j('#clipDesc_'+clip.id).fadeOut("slow");
1030 //fade in requested clip *and set req_clip to current
1031 $j('#clipDesc_'+req_clipID).fadeIn("slow", function(){
1032 _this.cur_clip = req_clip;
1038 //this is pretty outdated:
1039 getPLControls: function(){
1040 js_log('getPL cont');
1041 return '<a id="mv_prev_link_'+this.id+'" title="Previus Clip" onclick="document.getElementById(\''+this.id+'\').playPrev();return false;" href="#">'+
1042 getTransparentPng({id:'mv_prev_btn_'+this.id,style:'float:left',width:'27', height:'27', border:"0",
1043 src:mv_skin_img_path + 'vid_prev_sm.png' }) +
1045 '<a id="mv_next_link_'+this.id+'" title="Next Clip" onclick="document.getElementById(\''+this.id+'\').playNext();return false;" href="#">'+
1046 getTransparentPng({id:'mv_next_btn_'+this.id,style:'float:left',width:'27', height:'27', border:"0",
1047 src:mv_skin_img_path + 'vid_next_sm.png' }) +
1050 run_transition: function( clip_inx, trans_type){
1051 if(typeof this.default_track.clips[ clip_inx ][ trans_type ] == 'undefined')
1052 clearInterval( this.default_track.clips[ clip_inx ].timerId );
1054 this.default_track.clips[ clip_inx ][ trans_type ].run_transition();
1056 playerPixelWidth : function()
1058 var player = $j('#dc_'+this.id).get(0);
1059 if(typeof player!='undefined' && player['offsetWidth'])
1060 return player.offsetWidth;
1062 return parseInt(this.width);
1064 playerPixelHeight : function()
1066 var player = $j('#dc_'+this.id).get(0);
1067 if(typeof player!='undefined' && player['offsetHeight'])
1068 return player.offsetHeight;
1070 return parseInt(this.height);
1076 * @videoTrack ... stores clips and layer info
1078 * @clip... each clip segment is a clip object.
1080 var mvClip = function(o) {
1085 //set up the mvPlaylist object
1086 mvClip.prototype = {
1088 pp:null, // parent playlist
1089 order:null, //the order/array key for the current clip
1100 //init object including pointer to parent
1104 js_log('id is: '+ this.id);
1106 //setup the embed object:
1107 setUpEmbedObj:function(){
1108 js_log('mvClip:setUpEmbedObj()');
1113 //js_log('setup embed for clip '+ this.id + ':id is a function?');
1114 //set up the pl_mv_embed object:
1115 var init_pl_embed={id:'e_'+this.id,
1116 pc:this, //parent clip
1120 this.setBaseEmbedDim( init_pl_embed );
1123 //if in sequence mode hide controls / embed links
1124 // init_pl_embed.play_button=false;
1125 init_pl_embed.controls=false;
1126 //if(this.pp.sequencer=='true'){
1127 init_pl_embed.embed_link=null;
1128 init_pl_embed.linkback=null;
1130 if(this.poster)init_pl_embed['thumbnail']=this.poster;
1132 if( this.type )init_pl_embed['type'] = this.type;
1134 this.embed = new PlMvEmbed( init_pl_embed );
1136 //js_log('media Duration:' + this.embed.getDuration() );
1137 //js_log('media element:'+ this.embed.media_element.length);
1138 //js_log('type of embed:' + typeof(this.embed) + ' seq:' + this.pp.sequencer+' pb:'+ this.embed.play_button);
1140 doAdjust:function(side, delta){
1141 js_log("f:doAdjust: " + side + ' , ' + delta);
1144 var start_offset =parseInt(this.embed.start_offset)+parseInt(delta*-1);
1145 this.embed.updateVideoTime( seconds2npt(start_offset), seconds2npt ( this.embed.start_offset + this.embed.getDuration() ) );
1146 }else if(side=='end'){
1147 var end_offset = parseInt(this.embed.start_offset) + parseInt( this.embed.getDuration() ) + parseInt(delta);
1148 this.embed.updateVideoTime( seconds2npt(this.embed.start_offset), seconds2npt(end_offset) );
1150 //update everything:
1152 /*var base_src = this.src.substr(0,this.src.indexOf('?'));
1153 js_log("delta:"+ delta);
1155 //since we adjust start invert the delta:
1156 var start_offset =parseInt(this.embed.start_offset/1000)+parseInt(delta*-1);
1157 this.src = base_src +'?t='+ seconds2npt(start_offset) +'/'+ this.embed.end_ntp;
1158 }else if(side=='end'){
1159 //put back into seconds for adjustment:
1160 var end_offset = parseInt(this.embed.start_offset/1000) + parseInt(this.embed.duration/1000) + parseInt(delta);
1161 this.src = base_src +'?t='+ this.embed.start_ntp +'/'+ seconds2npt(end_offset);
1163 this.embed.updateVideoTime( this.src );
1165 this.duration = this.embed.getDuration();
1166 this.pp.pl_duration=null;
1167 //update playlist stuff:
1168 this.pp.updateTitle();*/
1171 getDuration:function(){
1172 if(!this.embed)this.setUpEmbedObj();
1173 return this.embed.getDuration();
1175 setBaseEmbedDim:function(o){
1177 //o.height=Math.round(pl_layout.clip_desc*this.pp.height)-2;//give it some padding:
1178 //o.width=Math.round(o.height*pl_layout.clip_aspect)-2;
1179 o.height= this.pp.height;
1180 o.width = this.pp.width;
1182 //output the detail view:
1184 /*getDetail:function(){
1185 //js_log('get detail:' + this.pp.title);
1186 var th=Math.round( this.pl_layout.clip_desc * this.pp.height );
1187 var tw=Math.round( th * this.pl_layout.clip_aspect );
1189 var twDesc = (this.pp.width-tw)-2;
1191 if(this.title==null)
1192 this.title='clip ' + this.order + ' ' +this.pp.title;
1194 this.desc=this.pp.desc;
1195 //update the embed html:
1196 this.embed.getHTML();
1198 $j(this.embed).css({ 'position':"absolute",'top':"0px", 'left':"0px"});
1200 //js_log('append child to:#clipDesc_'+this.id);
1201 if($j('#clipDesc_'+this.id).get(0)){
1202 $j('#clipDesc_'+this.id).get(0).appendChild(this.embed);
1204 $j('#clipDesc_'+this.id).append(''+
1205 '<div id="pl_desc_txt_'+this.id+'" class="pl_desc" style="position:absolute;left:'+(tw+2)+'px;width:'+twDesc+'px;height:'+th+'px;overflow:auto;">'+
1206 '<b>'+this.title+'</b><br>'+
1207 this.desc + '<br>' +
1208 '<b>clip length:</b> '+ seconds2npt( this.embed.getDuration() );
1212 getTitle:function(){
1213 if(typeof this.title == 'string')
1216 return 'untitled clip ' + this.order;
1218 getClipImg:function(start_offset, size){
1219 js_log('f:getClipImg ' + start_offset + ' s:'+size);
1221 return mv_default_thumb_url;
1223 if(!size && !start_offset){
1226 //if a metavid image (has request parameters) use size and time args
1227 if(this.img.indexOf('?')!=-1){
1228 js_log('get with offset: '+ start_offset);
1229 var time = seconds2npt( start_offset+ (this.embed.start_offset/1000) );
1230 js_log("time is: " + time);
1231 this.img = this.img.replace(/t\=[^&]*/gi, "t="+time);
1232 if(this.img.indexOf('&size=')!=-1){
1233 this.img = this.img.replace(/size=[^&]*/gi, "size="+size);
1235 this.img+='&size='+size;
1242 getColor: function(){
1243 //js_log('get color:'+ num +' : '+ num.toString().substr(num.length-1, 1) + ' : '+colors[ num.toString().substr(num.length-1, 1)] );
1244 var num = this.id.substr( this.id.length-1, 1);
1246 num=num.charCodeAt(0);
1248 if(num >= 10)num=num % 10;
1249 return mv_clip_colors[num];
1252 /* mv_embed extensions for playlists */
1253 var PlMvEmbed=function(vid_init){
1254 //js_log('PlMvEmbed: '+ vid_init.id);
1255 //create the div container
1256 var ve = document.createElement('div');
1257 //extend ve with all this
1258 this.init(vid_init);
1259 for(method in this){
1260 if(method!='readyState'){
1261 ve[method]= this[method];
1264 js_log('ve src len:'+ ve.media_element.sources.length);
1267 //all the overwritten and new methods for playlist extension of baseEmbed
1268 PlMvEmbed.prototype = {
1269 init:function(vid_init){
1270 //send embed_video a created video element:
1271 ve = document.createElement('div');
1272 for(var i in vid_init){
1273 //set the parent clip pointer:
1275 this['pc']=vid_init['pc'];
1277 ve.setAttribute(i,vid_init[i]);
1280 var videoInterface = new embedVideo(ve);
1281 //inherit the videoInterface
1282 for( method in videoInterface ){
1283 if(method!='style'){
1284 if( this[ method ] ){
1285 //parent embed method preservation:
1286 this['pe_'+method]=videoInterface[method];
1288 this[method]=videoInterface[method];
1291 //string -> boolean:
1292 if(this[method]=="false")this[method]=false;
1293 if(this[method]=="true")this[method]=true;
1296 onClipDone:function(){
1297 js_log('pl onClipDone (should go to next)');
1298 //go to next in playlist:
1299 this.pc.pp.playNext();
1302 js_log('pl:do stop');
1303 //set up convenience pointer to parent playlist
1304 var _this = this.pc.pp;
1306 var th=Math.round( _this.pl_layout.clip_desc * _this.height );
1307 var tw=Math.round( th * _this.pl_layout.clip_aspect );
1309 //run the parent stop:
1311 var pl_height = (_this.sequencer=='true')?_this.height+27:_this.height;
1316 //js_log('pl eb play');
1317 var _this = this.pc.pp;
1318 //check if we are already playing
1319 if( !this.thumbnail_disp ){
1323 mv_lock_vid_updates=true;
1326 //do post interface operations
1327 postEmbedJS:function(){
1328 //add playlist clips (if plugin supports it)
1329 if(this.pc.pp.cur_clip.embed.playlistSupport())
1330 this.pc.pp.loadEmbedPlaylist();
1331 //color playlist points (if play_head present)
1332 if(this.pc.pp.disp_play_head)
1333 this.pc.pp.colorPlayHead();
1334 //setup hover images (for playhead and next/prev buttons)
1335 this.pc.pp.setUpHover();
1336 //call the parent postEmbedJS
1337 this.pe_postEmbedJS();
1338 mv_lock_vid_updates=false;
1340 getPlayButton:function(){
1341 return this.pe_getPlayButton(this.pc.pp.id);
1343 setStatus:function(value){
1344 //status updates handled by playlist obj
1346 setSliderValue:function(value){
1347 js_log('PlMvEmbed:setSliderValue:' + value);
1348 //setSlider value handled by playlist obj
1357 //for each line not # add as clip
1360 //js_log('data:'+ this.data.toString());
1361 $j.each(this.data.split("\n"), function(i,n){
1362 //js_log('on line '+i+' val:'+n+' len:'+n.length);
1363 if( n.charAt(0) != '#' ){
1365 //@@todo make sure its a valid url
1366 //js_log('add url: '+i + ' '+ n);
1367 var cur_clip = new mvClip({type:'srcClip',id:'p_'+this_pl.id+'_c_'+inx,pp:this_pl,src:n,order:inx});
1368 //setup the embed object
1369 cur_clip.setUpEmbedObj();
1370 js_log('m3uPlaylist len:'+ thisClip.embed.media_element.sources.length);
1371 this_pl.addCliptoTrack(cur_clip);
1380 var itunesPlaylist = {
1382 var properties = { title:'title', linkback:'link',
1383 author:'itunes:author',desc:'description',
1386 for(i in properties){
1387 tmpElm = this.data.getElementsByTagName(properties[i])[0];
1389 this[i] = tmpElm.childNodes[0].nodeValue;
1390 //js_log('set '+i+' to '+this[i]);
1393 //image src is nested in itunes rss:
1394 tmpElm = this.data.getElementsByTagName('image')[0];
1396 imgElm = tmpElm.getElementsByTagName('url')[0];
1398 this.img = imgElm.childNodes[0].nodeValue;
1402 var clips = this.data.getElementsByTagName("item");
1403 properties.src = 'guid';
1404 for (var i=0;i<clips.length;i++){
1405 var cur_clip = new mvClip({type:'srcClip',id:'p_'+this.id+'_c_'+i,pp:this,order:i});
1406 for(var j in properties){
1407 tmpElm = clips[i].getElementsByTagName( properties[j] )[0];
1409 cur_clip[j] = tmpElm.childNodes[0].nodeValue;
1410 //js_log('set clip property: ' + j+' to '+cur_clip[j]);
1414 tmpElm = clips[i].getElementsByTagName('image')[0];
1416 imgElm = tmpElm.getElementsByTagName('url')[0];
1418 cur_clip.img = imgElm.childNodes[0].nodeValue;
1421 //set up the embed object now that all the values have been set
1422 cur_clip.setUpEmbedObj();
1424 //add the current clip to the clip list
1425 this.addCliptoTrack(cur_clip);
1433 * http://www.xspf.org/xspf-v1.html
1437 //js_log('do xsfp parse: '+ this.data.innerHTML);
1438 var properties = { title:'title', linkback:'info',
1439 author:'creator',desc:'annotation',
1440 poster:'image', date:'date' };
1442 //get the first instance of any of the meta tags (ok that may be the meta on the first clip)
1443 //js_log('do loop on properties:' + properties);
1444 for(i in properties){
1445 js_log('on property: '+i);
1446 tmpElm = this.data.getElementsByTagName(properties[i])[0];
1448 if(tmpElm.childNodes[0]){
1449 this[i] = tmpElm.childNodes[0].nodeValue;
1450 js_log('set pl property: ' + i+' to '+this[i]);
1454 var clips = this.data.getElementsByTagName("track");
1455 js_log('found clips:'+clips.length);
1456 //add any clip specific properties
1457 properties.src = 'location';
1458 for (var i=0;i<clips.length;i++){
1459 var cur_clip = new mvClip({id:'p_'+this.id+'_c_'+i,pp:this,order:i});
1460 //js_log('cur clip:'+ cur_clip.id);
1461 for(var j in properties){
1462 tmpElm = clips[i].getElementsByTagName( properties[j] )[0];
1464 if( tmpElm.childNodes.length!=0){
1465 cur_clip[j] = tmpElm.childNodes[0].nodeValue;
1466 js_log('set clip property: ' + j+' to '+cur_clip[j]);
1470 //add mvClip ref from info link:
1471 if(cur_clip.linkback){
1474 mvclippos = cur_clip.linkback.indexOf(mvInx);
1475 if(mvclippos!==false){
1476 cur_clip.mvclip=cur_clip.linkback.substr( mvclippos+mvInx.length );
1479 //set up the embed object now that all the values have been set
1480 cur_clip.setUpEmbedObj();
1481 //add the current clip to the clip list
1482 this.addCliptoTrack(cur_clip);
1484 //js_log('done with parse');
1488 /*****************************
1489 * SMIL CODE (could be put into another js file / lazy_loaded for improved basic playlist performance / modularity)
1490 *****************************/
1491 /*playlist driver extensions to the playlist object*/
1492 mvPlayList.prototype.monitor = function(){
1493 //js_log('pl:monitor');
1494 //if paused stop updates
1496 //clearInterval( this.smil_monitorTimerId );
1499 //js_log("pl check: " + this.currentTime + ' > '+this.getDuration());
1500 //check if we should be done:
1501 if( this.currentTime > this.getDuration() )
1504 //update the playlist current time:
1505 //check for a trsnOut from the previus clip to subtract
1506 this.currentTime = this.cur_clip.dur_offset + this.cur_clip.embed.relativeCurrentTime();
1509 if(!this.userSlide){
1510 this.setStatus(seconds2npt(this.currentTime) + '/' + seconds2npt(this.getDuration()) );
1511 this.setSliderValue( this.currentTime / this.getDuration() );
1513 //pre-load any future clips:
1514 this.loadFutureClips();
1517 //status updates are handled by children clips ... playlist mostly manages smil actions
1518 this.doSmilActions();
1520 if( ! this.smil_monitorTimerId ){
1521 if(document.getElementById(this.id)){
1522 this.smil_monitorTimerId = setInterval('$j(\'#'+this.id+'\').get(0).monitor()', 250);
1526 //handles the rendering of overlays load of future clips (if necessary)
1527 //@@todo could be lazy loaded if necessary
1528 mvPlayList.prototype.doSmilActions = function( single_frame ){
1529 //js_log('f:doSmilActions: ' + this.cur_clip.id + ' tid: ' + this.cur_clip.transOut );
1530 var offSetTime = 0; //offset time should let us start a transition later on if we have to.
1531 var _clip = this.cur_clip; //setup a local pointer to cur_clip
1534 //do any smil time actions that may change the current clip
1535 if( this.userSlide ){
1536 //current clip set is set via updateThumbTime function
1538 //assume playing and go to next:
1539 if( _clip.dur <= _clip.embed.currentTime
1540 && _clip.order != _clip.pp.getClipCount()-1 ){
1542 js_log('order:' + _clip.order + ' != count:' + ( _clip.pp.getClipCount()-1 ) +
1543 ' smil dur: ' + _clip.dur + ' <= curTime: ' + _clip.embed.currentTime + ' go to next clip..');
1545 _clip.pp.playNext();
1548 //@@todo could maybe generalize transIn with trasOut into one "flow" with a few scattered if statements
1549 //update/setup all transitions (will render current transition state)
1551 //pretty similar actions per transition types so group into a loop:
1552 var tran_types = {'transIn':true,'transOut':true};
1553 for(var tid in tran_types ){
1554 eval('var tObj = _clip.'+tid);
1557 //js_log('f:doSmilActions: ' + _clip.id + ' tid:'+tObj.id + ' tclip_id:'+ tObj.pClip.id);
1558 //make sue we are in range:
1559 if( tid=='transIn' )
1560 in_range = (_clip.embed.currentTime <= tObj.dur)?true:false;
1562 if( tid=='transOut' )
1563 in_range = (_clip.embed.currentTime >= (_clip.dur - tObj.dur))?true:false;
1566 if( this.userSlide || single_frame ){
1567 if( tid=='transIn' )
1568 mvTransLib.doUpdate(tObj, (_clip.embed.currentTime / tObj.dur) );
1570 if( tid=='transOut' )
1571 mvTransLib.doUpdate(tObj, (_clip.embed.currentTime-(_clip.dur - tObj.dur)) /tObj.dur);
1574 if( tObj.animation_state==0 ){
1575 js_log('init/run_transition ');
1576 tObj.run_transition();
1580 //close up transition if done & still onDispaly
1581 if( tObj.overlay_selector_id ){
1582 js_log('close up transition :'+tObj.overlay_selector_id);
1583 mvTransLib.doCloseTransition( tObj );
1590 * mvTransLib library of transitions
1591 * a single object called to initiate transition effects can easily be extended in separate js file
1592 * /mvTransLib is a all static object no instances of mvTransLib/
1593 * (that way a limited feature set "sequence" need not include a _lot_ of js unless necessary )
1595 * Smil Transition Effects see:
1596 * http://www.w3.org/TR/SMIL3/smil-transitions.html#TransitionEffects-TransitionAttribute
1600 * function doTransition lookups up the transition in the mvTransLib obj
1601 * and init the transition if its available
1602 * @param tObj transition attribute object
1603 * @param offSetTime default value 0 if we need to start rendering from a given time
1605 doInitTransition:function(tObj){
1606 js_log('mvTransLib:f:doInitTransition');
1608 js_log('transition is missing type attribute');
1613 js_log('transition is missing subtype attribute');
1617 if(!this['type'][tObj.type]){
1618 js_log('mvTransLib does not support type: '+tObj.type);
1622 if(!this['type'][tObj.type][tObj.subtype]){
1623 js_log('mvTransLib does not support subType: '+tObj.subtype);
1627 //setup overlay_selector_id
1628 if(tObj.subtype=='crossfade'){
1629 if(tObj.transAttrType=='transIn')
1630 var other_pClip = tObj.pClip.pp.getPrevClip();
1631 if(tObj.transAttrType=='transOut')
1632 var other_pClip = tObj.pClip.pp.getNextClip();
1634 if(typeof(other_pClip)=='undefined' || other_pClip === false || other_pClip.id == tObj.pClip.pp.cur_clip.id)
1635 js_log('Error: crossfade without target media asset');
1636 //if not sliding start playback:
1637 if(!tObj.pClip.pp.userSlide){
1638 other_pClip.embed.play();
1639 //manualy ad the extra layer to the activeClipList
1640 tObj.pClip.pp.activeClipList.add( other_pClip );
1642 tObj.overlay_selector_id = 'clipDesc_'+other_pClip.id;
1644 tObj.overlay_selector_id =this.getOverlaySelector(tObj);
1647 //all good call function with tObj param
1648 js_log('should call: '+tObj.type + ' ' + tObj.subtype );
1649 this['type'][tObj.type][tObj.subtype].init(tObj);
1651 doCloseTransition:function(tObj){
1652 if(tObj.subtype=='crossfade'){
1653 //close up crossfade
1654 js_log("close up crossfade");
1656 $j('#'+tObj.overlay_selector_id).remove();
1659 tObj.overlay_selector_id=null;
1661 getOverlaySelector:function(tObj){
1662 var overlay_selector_id= tObj.transAttrType + tObj.pClip.id;
1663 js_log('f:getOverlaySelector: '+overlay_selector_id + ' append to: ' +'#videoPlayer_'+tObj.pClip.embed.id );
1664 //make sure overlay_selector_id not already here:
1665 if( $j('#'+overlay_selector_id).length == 0 ){
1666 $j('#videoPlayer_'+tObj.pClip.embed.id).prepend(''+
1667 '<div id="'+overlay_selector_id+'" ' +
1668 'style="position:absolute;top:0px;left:0px;' +
1669 'height:'+parseInt(tObj.pClip.pp.height)+'px;'+
1670 'width:'+parseInt(tObj.pClip.pp.width)+'px;' +
1674 return overlay_selector_id;
1676 doUpdate:function(tObj, percent){
1677 //init the transition if nessesary:
1678 if(!tObj.overlay_selector_id)
1679 this.doInitTransition(tObj);
1681 //@@todo we should ensure visability outside of doUpate loop
1682 if(!$j('#'+tObj.overlay_selector_id).is(':visible'))
1683 $j('#'+tObj.overlay_selector_id).show();
1686 /*js_log('doing update for: '+ tObj.pClip.id +
1687 ' type:' + tObj.transAttrType +
1688 ' t_type:'+ tObj.type +
1689 ' subypte:'+ tObj.subtype +
1690 ' percent:' + percent);*/
1692 this['type'][tObj.type][tObj.subtype].u(tObj,percent);
1694 getTransitionIcon:function( type, subtype){
1695 return mv_embed_path +'/skins/'+mwConfig['skin_name']+'/transition_images/'+ type+'_'+ subtype+ '.png';
1698 * mvTransLib: functional library mapping:
1704 'attr':['fadeColor'],
1705 'init':function(tObj){
1706 //js_log('f:fadeFromColor: '+tObj.overlay_selector_id +' to color: '+ tObj.fadeColor);
1708 js_log('missing fadeColor');
1709 if($j('#'+tObj.overlay_selector_id).length==0){
1710 js_log("ERROR can't find: "+ tObj.overlay_selector_id);
1712 //set the initial state
1713 $j('#'+tObj.overlay_selector_id).css({
1714 'background-color':tObj.fadeColor,
1718 'u':function(tObj, percent){
1719 //js_log(':fadeFromColor:update: '+ percent);
1720 //fade from color (invert the percent)
1721 var percent = 1- percent;
1722 $j('#'+tObj.overlay_selector_id).css({
1730 "init":function(tObj){
1731 js_log('f:crossfade: '+tObj.overlay_selector_id);
1732 if($j('#'+tObj.overlay_selector_id).length==0)
1733 js_log("ERROR overlay selector not found: "+tObj.overlay_selector_id);
1735 //set the initial state show the zero opacity animation
1736 $j('#'+tObj.overlay_selector_id).css({'opacity':0}).show();
1738 'u':function(tObj, percent){
1739 $j('#'+tObj.overlay_selector_id).css({
1748 /* object to manage embedding html with smil timings
1749 * grabs settings from parent clip
1751 var transitionObj = function(element) {
1754 transitionObj.prototype = {
1755 supported_attributes : new Array(
1762 transAttrType:null, //transIn or transOut
1763 overlay_selector_id:null,
1766 animation_state:0, //can be 0=unset, 1=running, 2=done
1767 interValCount:0, //inter-intervalCount for animating between time updates
1768 dur:2, //default duration of 2
1769 init:function(element){
1770 //load supported attributes:
1772 $j.each(this.supported_attributes, function(i, attr){
1773 if(element.getAttribute(attr))
1774 _this[attr]= element.getAttribute(attr);
1776 //@@todo process duration (for now just strip s) per:
1777 //http://www.w3.org/TR/SMIL3/smil-timing.html#Timing-ClockValueSyntax
1779 _this.dur = smilParseTime(_this.dur);
1782 * returns a visual representation of the transition
1784 getIconSrc:function(opt){
1785 //@@todo support some arguments
1786 return mvTransLib.getTransitionIcon(this.type, this.subtype);
1788 getDuration:function(){
1791 //returns the values of supported_attributes:
1792 getAttributeObj:function(){
1794 for(var i in this.supported_attributes){
1795 var attr = this.supported_attributes[i];
1797 elmObj[ attr ] = this[attr];
1802 * the main animation loop called every MV_ANIMATION_CB_RATE or 34ms ~around 30frames per second~
1804 run_transition:function(){
1805 //js_log('f:run_transition:' + this.interValCount);
1807 //update the time from the video if native:
1808 if(typeof this.pClip.embed.vid !='undefined'){
1809 this.interValCount=0;
1810 this.pClip.embed.currentTime = this.pClip.embed.vid.currentTime;
1814 //relay on currentTime update grabs (every 250ms or so) (ie for images)
1815 // if(this.prev_curtime!=this.pClip.embed.currentTime){
1816 // this.prev_curtime = this.pClip.embed.currentTime;
1817 // this.interValCount=0;
1820 //start_time =asigned by doSmilActions
1821 //base_cur_time = pClip.embed.currentTime;
1822 //dur = asigned by attribute
1823 if(this.animation_state==0){
1824 mvTransLib.doInitTransition(this);
1825 this.animation_state=1;
1827 //set percentage include difrence of currentTime to prev_curTime
1828 // ie updated in-between currentTime updates)
1830 if(this.transAttrType=='transIn')
1831 var percentage = ( this.pClip.embed.currentTime +
1832 ( (this.interValCount*MV_ANIMATION_CB_RATE)/1000 )
1835 if(this.transAttrType=='transOut')
1836 var percentage = (this.pClip.embed.currentTime +
1837 ( (this.interValCount*MV_ANIMATION_CB_RATE)/1000 )
1838 - (this.pClip.dur - this.dur)
1841 /*js_log('percentage = ct:'+this.pClip.embed.currentTime + ' + ic:'+this.interValCount +' * cb:'+MV_ANIMATION_CB_RATE +
1842 ' / ' + this.dur + ' = ' + percentage );
1845 //js_log('cur percentage of transition: '+percentage);
1846 //update state based on current time + cur_time_offset (for now just use pClip.embed.currentTime)
1847 mvTransLib.doUpdate(this, percentage);
1849 if( percentage >= 1 ){
1850 js_log("transition done update with percentage "+percentage);
1851 this.animation_state=2;
1852 clearInterval(this.timerId);
1853 mvTransLib.doCloseTransition(this)
1857 this.interValCount++;
1858 //setInterval in we are still in running state and user is not using the playhead
1859 if( this.animation_state==1 ){
1861 this.timerId = setInterval('document.getElementById(\'' + this.pClip.pp.id + '\').'+
1862 'run_transition(\'' + this.pClip.pp.cur_clip.order + '\','+
1863 '\''+ this.transAttrType + '\')',
1864 MV_ANIMATION_CB_RATE);
1867 clearInterval(this.timerId);
1872 var cObj = new this.constructor();
1879 //very limited smile feature set more details soon:
1880 //region="video_region" transIn="fromGreen" begin="2s"
1881 //http://www.w3.org/TR/2007/WD-SMIL3-20070713/smil-extended-media-object.html#edef-ref
1886 js_log('f:doParse smilPlaylist');
1887 //@@todo get/parse meta that we are intersted in:
1888 var meta_tags = this.data.getElementsByTagName('meta');
1897 $j.each(meta_tags, function(i,meta_elm){
1898 //js_log( "on META tag: "+ $j(meta_elm).attr('name') );
1899 if( $j(meta_elm).attr('name') in metaNames){
1900 _this[ $j(meta_elm).attr('name') ] = $j(meta_elm).attr('content');
1902 //special check for wikiDesc
1903 if( $j(meta_elm).attr('name') == 'wikiDesc'){
1904 if(meta_elm.firstChild)
1905 _this.wikiDesc = meta_elm.firstChild.nodeValue;
1908 //add transition objects:
1909 var transition_tags = this.data.getElementsByTagName('transition');
1910 $j.each(transition_tags, function( i, trans_elm ){
1911 if( $j(trans_elm).attr("id") ){
1912 _this.transitions[ $j(trans_elm).attr("id")]= new transitionObj( trans_elm );
1914 js_log('skipping transition: (missing id) ' + trans_elm );
1917 js_log('loaded transitions:' + _this.transitions.length);
1918 //add seq (latter we will have support more than one seq tag) / more than one "track"
1919 var seq_tags = this.data.getElementsByTagName('seq');
1920 $j.each(seq_tags, function(i,seq_elm){
1922 //get all the clips for the given seq:
1923 $j.each(seq_elm.childNodes, function(i, mediaElement){
1924 //~complex~ @@todo to handlde a lot like "switch" "region" etc
1925 //js_log('process: ' + mediaElemnt.tagName);
1926 if(typeof mediaElement.tagName!='undefined'){
1927 if( _this.tryAddMedia( mediaElement, inx ) ){
1933 js_log("done proc seq tags");
1936 tryAddMediaObj:function(mConfig, order, track_id){
1937 js_log('tryAddMediaObj::');
1938 var mediaElement = document.createElement('div');
1939 for(var i =0; i < mv_smil_ref_supported_attributes.length;i++){
1940 var attr = mv_smil_ref_supported_attributes[i];
1942 $j(mediaElement).attr( attr, mConfig[attr] );
1944 this.tryAddMedia(mediaElement, order, track_id);
1946 tryAddMedia:function(mediaElement, order, track_id){
1947 js_log('SMIL:tryAddMedia:' + mediaElement);
1949 //set up basic mvSMILClip send it the mediaElemnt & mvClip init:
1952 "id":'p_' + _this.id + '_c_' + order,
1953 "pp":this, //set the parent playlist object pointer
1956 var clipObj = new mvSMILClip(mediaElement, cConfig );
1958 //set optional params track
1959 if( typeof track_id != 'undefined')
1960 clipObj["track_id"] = track_id;
1965 clipObj.setUpEmbedObj();
1966 //inhreit embedObject (only called on "new media"
1967 clipObj.embed.init_with_sources_loaded();
1968 //add clip to track:
1969 this.addCliptoTrack( clipObj , order);
1973 //@@todo we could throw error details here once we integrate try catches everywhere :P
1977 //http://www.w3.org/TR/2007/WD-SMIL3-20070713/smil-extended-media-object.html#smilMediaNS-BasicMedia
1978 //and added resource description elements
1979 //@@ supporting the "ID" attribute turns out to be kind of tricky since we use it internally
1980 // (for now don't include)
1981 var mv_smil_ref_supported_attributes = new Array(
1990 //some custom attributes:
1995 /* extension to mvClip to support smil properties */
1996 var mvSMILClip=function(sClipElm, mvClipInit){
1997 return this.init(sClipElm, mvClipInit);
1999 //all the overwritten and new methods for SMIL extension of mv_embed
2000 mvSMILClip.prototype = {
2001 instanceOf:'mvSMILClip',
2002 params : {}, //support param as child of ref clips per SMIL spec
2003 init:function(sClipElm, mvClipInit){
2006 //make new mvCLip with ClipInit vals
2007 var myMvClip = new mvClip( mvClipInit );
2009 for(var method in myMvClip){
2010 if(typeof this[method] != 'undefined' ){
2011 this['parent_'+method]=myMvClip[method];
2013 this[method] = myMvClip[method];
2017 //get supported media attr init non-set
2018 for(var i =0; i < mv_smil_ref_supported_attributes.length;i++){
2019 var attr = mv_smil_ref_supported_attributes[i];
2020 if( $j(sClipElm).attr(attr)){
2021 _this[attr] = $j(sClipElm).attr(attr);
2024 this['tagName'] = sClipElm.tagName;
2026 if( sClipElm.firstChild ){
2027 this['wholeText'] = sClipElm.firstChild.nodeValue;
2028 js_log("SET wholeText for: " + this['tagName'] + ' '+ this['wholeText']);
2031 //mv_embed specific property:
2032 if( $j(sClipElm).attr('poster') )
2033 this['img'] = $j(sClipElm).attr('poster');
2035 //lookup and assign copies of transitions
2036 // (since transition needs to hold some per-instance state info)
2037 if(this.transIn && this.pp.transitions[ this.transIn ]){
2038 this.transIn = this.pp.transitions[ this.transIn ].clone();
2039 this.transIn.pClip = _this;
2040 this.transIn.transAttrType='transIn';
2043 if(this.transOut && this.pp.transitions[ this.transOut ]){
2044 this.transOut = this.pp.transitions[ this.transOut ].clone();
2045 this.transOut.pClip = _this;
2046 this.transOut.transAttrType = 'transOut';
2048 //parse duration / begin times:
2050 this.dur = smilParseTime( this.dur );
2051 //parse the media duration hint ( the source media length)
2052 if( this.durationHint )
2053 this.durationHint = smilParseTime( this.durationHint );
2055 //conform type to vido/ogg:
2056 if( this.type == 'application/ogg' )
2057 this.type = 'video/ogg'; //conform to 'video/ogg' type
2059 //if unset type and we have innerHTML assume text/html type
2060 if( !this.type && this.wholeText ){
2061 this.type = 'text/html';
2063 //also grab andy child param elements if present:
2064 if( sClipElm.getElementsByTagName('param')[0] ){
2065 for(var i=0; i< sClipElm.getElementsByTagName('param').length; i++){
2066 this.params[ sClipElm.getElementsByTagName('param')[i].getAttribute("name") ] =
2067 sClipElm.getElementsByTagName('param')[i].firstChild.nodeValue;
2072 //returns the values of supported_attributes:
2073 getAttributeObj:function(){
2075 for(var i=0; i < mv_smil_ref_supported_attributes.length; i++){
2076 var attr = mv_smil_ref_supported_attributes[i];
2078 elmObj[ attr ] = this[attr];
2084 * @returns duration in int
2086 getDuration:function(){
2087 //check for smil dur:
2090 return this.embed.getDuration();
2092 //gets the duration of the clip subracting transitions
2093 getSoloDuration:function(){
2094 var fulldur = this.getDuration();
2095 //see if we need to subtract from time eating transitions (transOut)
2097 fulldur -= this.transOut.getDuration();
2099 //js_log("getSoloDuration:: td: " + this.getDuration() + ' sd:' + fulldur);
2102 //gets the duration of the original media asset (usefull for bounding setting of in-out-points)
2103 getSourceDuration:function(){
2104 if( this.durationHint )
2105 return this.durationHint;
2106 //if we have no source duration just return the media dur:
2107 return this.getDuration();
2112 * @time_str input time string
2113 * returns time in seconds
2115 * @@todo process duration (for now just srip s) per:
2116 * http://www.w3.org/TR/SMIL3/smil-timing.html#Timing-ClockValueSyntax
2117 * (probably have to use a Time object to fully support the smil spec
2119 function smilParseTime( time_str ){
2120 time_str = time_str + '';
2121 //first check for hh:mm:ss time:
2122 if(time_str.split(':').length == 3){
2123 return npt2seconds(time_str);
2125 //assume 34s secconds representation
2126 return parseInt(time_str.replace('s', ''));
2129 //stores a list pointers to active clips (maybe this should just be a property of clips (but results in lots of seeks)
2130 var activeClipList = function(){
2133 activeClipList.prototype = {
2135 this.clipList = new Array();
2137 add:function( clip ){
2138 //make sure the clip is not already active:
2139 for(var i =0;i < this.clipList.lenght; i++){
2140 var active_clip = this.clipList[i];
2141 if(clip.id == active_clip.id) //clip already active:
2144 this.clipList.push( clip );
2147 remove:function( clip ){
2148 for(var i = 0; i < this.clipList.length; i++){
2149 var active_clip = this.clipList[i];
2150 if(clip.id == active_clip.id){
2151 this.clipList.splice(i, 1);
2157 getClipList:function(){
2158 return this.clipList;
2161 var trackObj = function( iObj ){
2162 return this.init( iObj );
2164 var supported_track_attr =
2165 trackObj.prototype = {
2166 //should be something like "seq" per SMIL spec
2167 //http://www.w3.org/TR/SMIL3/smil-timing.html#edef-seq
2168 // but we don't really support anywhere near the full concept of seq containers yet either
2169 supported_attributes: new Array(
2174 disp_mode:'timeline_thumb',
2175 init : function(iObj){
2178 //make sure clips is new:
2179 this.clips = new Array();
2182 $j.each(this.supported_attributes, function(i, attr){
2184 _this[attr] = iObj[attr];
2187 //returns the values of supported_attributes:
2188 getAttributeObj:function(){
2190 for(var i in this.supported_attributes){
2191 var attr = this.supported_attributes[i];
2193 elmObj[ attr ] = this[attr];
2197 addClip:function(clipObj, pos){
2198 js_log('pl_Track: AddClip at:' + pos + ' clen: ' + this.clips.length);
2199 if( typeof pos == 'undefined' )
2200 pos = this.clips.length;
2201 //get everything after pos
2202 this.clips.splice(pos, 0, clipObj);
2203 //keep the clip order values accurate:
2204 this.reOrderClips();
2205 js_log("did add now cLen: " + this.clips.length);
2207 getClip:function( inx ){
2208 if( !this.clips[inx] )
2210 return this.clips[inx];
2212 reOrderClips:function(){
2213 for(var k in this.clips){
2214 this.clips[k].order=k;
2217 getClipCount:function(){
2218 return this.clips.length;
2220 inheritEmbedObj: function(){
2221 $j.each(this.clips, function(i, clip){
2222 clip.embed.inheritEmbedObj();
2227 /* utility functions
2228 * (could be combined with other stuff)
2230 function getAbsolutePos(objectId) {
2231 // Get an object left position from the upper left viewport corner
2232 o = document.getElementById(objectId);
2233 oLeft = o.offsetLeft; // Get left position from the parent object
2234 while(o.offsetParent!=null) { // Parse the parent hierarchy up to the document element
2235 oParent = o.offsetParent // Get parent object reference
2236 oLeft += oParent.offsetLeft // Add parent left position
2239 o = document.getElementById(objectId);
2241 while(o.offsetParent!=null) { // Parse the parent hierarchy up to the document element
2242 oParent = o.offsetParent // Get parent object reference
2243 oTop += oParent.offsetTop // Add parent top position
2246 return {x:oLeft,y:oTop};
2248 String.prototype.htmlEntities = function(){
2249 var chars = new Array ('&','à','á','â','ã','ä','å','æ','ç','è','é',
2250 'ê','ë','ì','í','î','ï','ð','ñ','ò','ó','ô',
2251 'õ','ö','ø','ù','ú','û','ü','ý','þ','ÿ','À',
2252 'Á','Â','Ã','Ä','Å','Æ','Ç','È','É','Ê','Ë',
2253 'Ì','Í','Î','Ï','Ð','Ñ','Ò','Ó','Ô','Õ','Ö',
2254 'Ø','Ù','Ú','Û','Ü','Ý','Þ','€','\"','ß','<',
2255 '>','¢','£','¤','¥','¦','§','¨','©','ª','«',
2256 '¬','','®','¯','°','±','²','³','´','µ','¶',
2257 '·','¸','¹','º','»','¼','½','¾');
2259 var entities = new Array ('amp','agrave','aacute','acirc','atilde','auml','aring',
2260 'aelig','ccedil','egrave','eacute','ecirc','euml','igrave',
2261 'iacute','icirc','iuml','eth','ntilde','ograve','oacute',
2262 'ocirc','otilde','ouml','oslash','ugrave','uacute','ucirc',
2263 'uuml','yacute','thorn','yuml','Agrave','Aacute','Acirc',
2264 'Atilde','Auml','Aring','AElig','Ccedil','Egrave','Eacute',
2265 'Ecirc','Euml','Igrave','Iacute','Icirc','Iuml','ETH','Ntilde',
2266 'Ograve','Oacute','Ocirc','Otilde','Ouml','Oslash','Ugrave',
2267 'Uacute','Ucirc','Uuml','Yacute','THORN','euro','quot','szlig',
2268 'lt','gt','cent','pound','curren','yen','brvbar','sect','uml',
2269 'copy','ordf','laquo','not','shy','reg','macr','deg','plusmn',
2270 'sup2','sup3','acute','micro','para','middot','cedil','sup1',
2271 'ordm','raquo','frac14','frac12','frac34');
2274 for (var i = 0; i < chars.length; i++)
2276 myRegExp = new RegExp();
2277 myRegExp.compile(chars[i],'g')
2278 newString = newString.replace (myRegExp, '&' + entities[i] + ';');