» Fig Leaf Software Home

We've Got You Covered.

Thursday, November 13, 2014

Building a Sencha Touch Wrapper for the Brightcove Video Player

Building a Sencha Touch Wrapper for the Brightcove Video Player

We decided that it would be a good idea to develop a Brightcove Video Player object for future use in our Sencha Touch projects. The Brightcove Video Cloud service allows users to maintain and control the distribution of videos they upload. One aspect of the player that makes it useful for web development is that it will create itself as either a Flash or HTML5 object, depending on what the current browser supports.

The component has two main config properties: the playerKey and the playerID. To get these, make an account at brightcove.com and go to the Publishing tab. If you want to load a video when the player initializes, grab a videoID from the Media tab. Additional functions of the Brightcove API require Player and Experience objects, which can be obtained by calling the getPlayer() and getBrightcoveExperience() methods of this class.

In order to make sure that the API fully loads before anything can be done, the initialization work is done after the container is rendered. This is done in the handler for the 'painted' event:

1:  onBCPPainted: function(element, eOpts) {  
2:      if (typeof(brightcove) == 'undefined') {  
3:         this.setMasked({  
4:          xtype: 'loadmask',  
5:          message: 'Loading Brightcove API...',  
6:          indicator: true  
7:         });  
8:         Ext.Loader.loadScriptFile('http://admin.brightcove.com/js/BrightcoveExperiences.js',  
9:          function() {  
10:           this.setMasked(false);  
11:           this.createPlayer();  
12:          },  
13:          function() {  
14:           alert("Failed to load API");  
15:          },  
16:          this  
17:         );  
18:        } else {  
19:         this.createPlayer();  
20:        }  
21:        bp = this;  
22:    },  

First, this checks if the Brightcove API is already loaded. If it is, it proceeds to call the createPlayer() method. If not, it loads the script file and asynchronously calls createPlayer() after the file has been loaded. In order to show the player within a Sencha Touch container, we had to set its HTML to that of the player object and render it.
1:  createPlayer: function() {  
2:        console.log("SCOPE: ", this);  
3:        onTemplateLoadHandler = function(experienceID) {  
4:         bp.onTemplateLoad(experienceID);  
5:        };  
6:        onTemplateReadyHandler = function(evt) {  
7:         bp.onTemplateReady(evt);  
8:        };  
9:        this.htmlString = ''.concat('<object id="{0}" class="BrightcoveExperience">',  
10:         '<param name="bgcolor" value="{1}\" /><param name="width"',  
11:         'value="{2}" /><param name="height" value="{3}" />',  
12:         '<param name="playerID" value="{4}" />',  
13:         '<param name="playerKey\" value="{5}" />',  
14:         '<param name="isVid\" value="{6}" />',  
15:         '<param name="isUI" value="true" />',  
16:         '<param name="dynamicStreaming" value="{7}" />',  
17:         '<param name="@videoPlayer" value="{8}" />',  
18:         '<param name="includeAPI" value="true" />',  
19:         '<param name="templateLoadHandler" value="onTemplateLoadHandler" />',  
20:         '<param name="templateReadyHandler" value="onTemplateReadyHander" />');  
21:        Ext.Viewport.on('orientationchange', 'onOrientationChange', this, {  
22:         buffer: 50  
23:        });  
24:        this.setMasked({  
25:         xtype: 'loadmask',  
26:         message: 'Loading Video...',  
27:         indicator: true  
28:        });  
29:        this.setHtml(Ext.String.format(this.htmlString,  
30:         this.getPlayerName(),  
31:         this.getPlayerBgcolor(),  
32:         this.element.getWidth(),  
33:         this.element.getHeight(),  
34:         this.getPlayerID(),  
35:         this.getPlayerKey(),  
36:         this.getIsVid(),  
37:         this.getDynamicStreaming(),  
38:         this.getVideoID()));  
39:        brightcove.createExperiences();  
40:        this.setMasked(false);  
41:    },  

There are also declared handler's for when the player is loaded and when it is ready to be used. The onTemplateLoad event fires when the Brightcove player is loaded, and instantiates the brightcoveExperience and player fields. It also adds an event listener (A Brightcove event, not a Sencha Touch event) for when the video is played for the first time. This will be important later. The onTemplateReady function simply logs that the player is ready for use.

1:    onTemplateLoad: function(experienceID) {  
2:        var BCExperience = (brightcove.api.getExperience(experienceID));  
3:        this.setBrightcoveExperience(BCExperience);  
4:        var APIModules = (brightcove.api.modules.APIModules);  
5:        var player = BCExperience.getModule(APIModules.VIDEO_PLAYER);  
6:        this.setPlayer(player);  
7:        this.getPlayer().addEventListener(brightcove.api.events.MediaEvent.BEGIN,  
8:         function() {  
9:          console.log('Event media begin');  
10:          if (typeof bp.currentPosition !== "undefined") {  
11:           player.seek(bp.currentPosition);  
12:          }  
13:         });  
14:    },  
15:    onTemplateReady: function(evt) {  
16:      console.log('template ready');  
17:      console.log("SCOPE: " , this);  
18:    }  

We hit a specific challenge when it came to mobile users rotating the device. We initially thought that re-rendering the player with the new width and height would be enough, but that resulted in the player being completely wiped. Instead, we had to create a parent container for the video player. When the player detects a device rotation, it calls its onOrientationChange method. This grabs the current time of the video (if it has been played) and passes it along to the onParentOrientationChange of the parent container along with the new width and height. The reason it checks if the user is using iOS is because iOS devices launch their video in Quicktime by default, which already supports device rotation with seamless playback.

1:    onOrientationChange: function(scope, newOrientation, width, height, eOpts) {  
2:       this.getPlayer().getIsPlaying(function(currentlyPlaying) {  
3:         if (!Ext.os.is('iOS') || !currentlyPlaying) {  
4:          var currentPosition;  
5:          // debugger;  
6:          this.getPlayer().getVideoPosition(false, function(r) {  
7:           console.log(r);  
8:           bp.getParent().parentOrientationChange({  
9:            currentPosition: r,  
10:            html: Ext.String.format(bp.htmlString,  
11:             bp.getPlayerName(),  
12:             bp.getPlayerBgcolor(),  
13:             width,  
14:             height,  
15:             bp.getPlayerID(),  
16:             bp.getPlayerKey(),  
17:             bp.getIsVid(),  
18:             bp.getDynamicStreaming(),  
19:             bp.getVideoID()),  
20:           });  
21:          });  
22:         }  
23:        });  
24:    },  

The parentOrientationChange method destroys the current video and replaces it with a new player with the updated dimensions. Going back to line 10 of onTemplateLoad, it checks if the current object has a field variable called 'currentPosition', which is only given to it by onOrientationChange. If it is, then it tells the player to start playback at that position. This way, even though we are completely destroying and recreating the video player, users can still resume playback at the point where they rotated the device.

1:    parentOrientationChange: function(bcPlayerConfigs) {  
2:      this.remove(this.down('BCPlayer'),true); 
3:      var newPlayer = Ext.create('MyApp.view.BrightcovePlayer', bcPlayerConfigs);  
4:      this.add(newPlayer);  
5:      brightcove.createExperiences();  
6:    }  

Tuesday, November 4, 2014

CommonSpot Custom Field Craziness!

We were recently tasked by the University of Wisconsin, Eau-Claire to produce a series of advanced custom elements to help their content contributors generate a Pinterest-style image grid.
There are three major components to the element:
  1. An image editor that allows contributors to upload an image and then pan/zoom/crop to a specific image size.
  2. An editor that allows contributors to overlay caption on top of the cropped image.
  3. Output of multiple cropped & captioned images into a responsive, pinterest-style display

Creating the Image Editor Custom Field

Starting from a codebase with limited pan/zoom cropping functionality (http://danielhellier.com/imagecrop/), we refactored the jQuery component to support fixed-sized crop areas as well as implement a bounding box and also tied in a slider component to enable easy-to-use zooming. We also added in the capability to transmit the scaling/cropping coordinates to an application server for server-side processing.

Adding Text Captioning by using a Draggable, Editable <h3> Element

For this project, we needed to give contributors the ability to place a text overlay on the image. The word “and” would need to be automatically converted to uppercase and have a style applied via CSS. To enable the users to choose a color, we used the spectrum plugin for jQuery which generally worked as advertised.
You can make editable <div> segments by simply adding a contenteditable attribute to a block element as illustrated below. The cropped image was simply placed behind the <h3> in the container
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="#fqfieldname#-container" style="width: #adjustedWidth#px; height: #adjustedHeight#px">
  <div id="#fqfieldname#-preview" style="border: 1px solid black">
    <h3 class="POAeditablelabel"
            id="edittext-#fqFieldName#" contenteditable
            onFocus="stripAnd(this)">#poaAttribs.text#</h3>
    <img id="#fqfieldname#-image"
      style="width:#stParams.nWidth#px;
      height: #stParams.nHeight#px;"
     <cfif poaAttribs.src is not ""> src="#poaAttribs.src#"</cfif>
        >
  </div>
</div>
We then applied jQuery UI’s draggable plugin to enable the user to drag the editable heading within it’s container:
1
2
3
4
5
6
jQuery('##edittext-#fqFieldName#').draggable({ containment: 'parent', axis: 'y' })
.click(function() {
  jQuery(this).draggable({ disabled: false });
}).dblclick(function() {
  jQuery(this).draggable({ disabled: true });
});



Creating a Pinterest-Style Render Handler


Leveraging the jQuery isotope and jQuery lazyload plugins, Fig Leaf’s developers were able to devise an algorithm that would automatically resize images into a “pinterest”-style image grid in order to minimize the right margin space that typically results from implementing these types of layouts as well as work with the dyanamic text overlays required by the customer.
responsiveimagegrid
responsiveimagegrid
Every time the browser is resized, we dynamically rewrite the image CSS classes and then reinvoke the isotope plugin. The key javascript function, executed on browser resize, is illustrated here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function generateStyles() {
  
 // get pointer to stylesheet          
 var ss = $("#POADynamicStyles");
 var container = $("#POAcontainer");
 var width = container.width();
  
// set minimum width of images to 160, maximum width to 201
 var minWidth = 160, maxWidth = 201, iWidth = 0;
                 
                 
 // figure out the optimal number of cols, given the available space
 for (var numCols=2; numCols<15; numCols++) {
   iWidth = Math.ceil(width / numCols);
   // console.log('cols', numCols, 'width', iWidth);
   if (iWidth <= maxWidth && iWidth >= minWidth) {
    break;
   }
  }
                 
  if (numCols == 15) {
    numCols = 3;
    console.log('auto sizing failed');
  }
             
  // 10px margin around images
  var w1 = Math.floor(width / numCols) - 10;
  var reducePercentage = w1/250;
  var fontSize = reducePercentage * 2.7125;
  var lineHeight = 1;
  var heightOffset = 0;
  heightOffset = (-0.13 * (100 - (reducePercentage * 100)));
                 
  var styles = ''.concat(
    '.item.w2  {width : {1}px;} \n',
    '.item.h2 {height: {1}px;} \n',
    '.item {width : {0}px; height: {0}px;}\n',
    '.item h3 {font-size: {2}rem; line-height: {3}; transform : translate(0px,{4}px)}'
   ).format(w1,w1*2+10,fontSize,lineHeight,heightOffset);
                 
                 
   ss.html(styles);
   initIsotope(w1 + 10);
             
}

Would you like to know more?

Contact Fig Leaf Software’s Professional Services group at info@figleaf.com to discover how we can help you achieve your CMS implementation goals.
Written by: Steve Drucker

About Us

Fig Leaf Software is an award-winning team of imaginative designers, innovative developers, experienced instructors, and insightful strategists.

For over 20 years, we’ve helped a diverse range of clients...

Read More

Contact Us

202-797-7711

Fig Leaf Software

1400 16th Street NW
Suite 450
Washington, DC 20036

info@figleaf.com