jump to navigation

ActionScript wrapper for the Brightcove Media APIs October 13, 2009

Posted by Allen Manning in : Brightcove, Flex, Refactoring , 1 comment so far

Brightcove has made some big strides in opening up their online service to developers in the last few years.  In particular, the Brightcove Media API, a JSON-based read and write API for interacting with the Brightcove platform, is a powerful tool for online experience development.

Developers who want to assemble customized online experiences can access the media libraries created by publishers and define custom visual experiences with really no limit.  Even though this is JSON-based, access to the Media APIs is not limited to AJAX clients. ActionScript clients as well have the full wealth of the Brightcove Media API at their disposal.

Let’s start by looking at a simple example application which loads a set of videos from the Brightcove Media APIs and populates them in a datagrid.

Before:  Example Application - Loading a Datagrid with Brightcove Videos

As is available in the documentation, the below application provides a fairly straight-forward approach to loading videos:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?xml version="1.0" encoding="utf-8"?>
<mx:Application 
        xmlns:mx="http://www.adobe.com/2006/mxml">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
        import com.adobe.serialization.json.JSON;
        import mx.collections.ArrayCollection;
        import mx.controls.Alert;
 
        /** The object used to load URLs */
        private var _loader:URLLoader;
 
        /** The path to the brightcove api services */
        private const BRIGHTCOVE_API_PATH:String = "http://api.brightcove.com/services/library";
 
        /** The publisher token to be used to make the requests */
        private var _token:String = "0Z2dtxTdJAxtbZ-d0U7Bhio2V1Rhr5Iafl5FFtDPY8E."
 
        /** The command string to find all videos*/
        private const FIND_ALL_COMMAND_STRING:String = "find_all_videos";
 
        /** 
         * The "click" event listener for the find_all_videos button - load the videos from 
         * Brightcove convert them for JSON to native AS Objects and populate it in a datagrid
         */  
        private function onButtonClick():void {
 
            var url:String = BRIGHTCOVE_API_PATH + 
                "?command=" + 
                FIND_ALL_COMMAND_STRING +
                "&token=" +
                _token;
 
            trace('url to request: ' + url);
            var request:URLRequest = new URLRequest(url);
 
            _loader = new URLLoader();
            _loader.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
            _loader.addEventListener(Event.COMPLETE, loaderCompleteHandler);
 
            try {
                _loader.load(request);
            }
            catch (error:SecurityError) {
                trace("A SecurityError has occurred.");
            }
 
        }
 
        /** "fault" event listener to the remote call to the Brightcove media APIs */ 
        private function errorHandler(event:Event):void {
 
           mx.controls.Alert.show(
              "An error occurred" + 
                  event.target.toString(),
              "An error occurred");
 
        }
 
        /** "result" event listener to the remote call to the Brightcove media APIs */ 
        private function loaderCompleteHandler(event:Event):void {
 
            //Get the returned JSON data string
            var response:String = event.target.data as String;
 
            //The list of returned videos is embedded in the "items" property
            //of the root JSON object, so we will decode to a container
            var container:Object = (JSON.decode(response) as Object);
 
            videosDataGrid.dataProvider = new ArrayCollection(container.items);
 
            //Convert the UNIX date into an AS3 Date
            for(var i:int = 0; i< videosDataGrid.dataProvider.length; i++) {
                var video:Object = videosDataGrid.dataProvider.getItemAt(i);
                var n:Number = video.publishedDate;
                video.publishedDate = new Date(n);
            }           
 
        }
 
    ]]>
    </mx:Script>
 
    <!--
    	********************************************************************************************
    	VIEW
    	********************************************************************************************
    -->
    <mx:Button id="loadButton"
        label="Load Vidoes"
        click="onButtonClick()"/>
 
    <mx:DataGrid id="videosDataGrid"
            width="100%" height="100%">
 
        <mx:columns>
            <mx:DataGridColumn 
                dataField="id" 
                headerText="ID"/>
            <mx:DataGridColumn 
                dataField="name" 
                headerText="Name"/>
            <mx:DataGridColumn 
                dataField="shortDescription" 
                headerText="Description"/>
            <mx:DataGridColumn 
                dataField="publishedDate" 
                headerText="Published Date"/>
        </mx:columns>
    </mx:DataGrid>
 
</mx:Application>

Room for Improvement

Although the above is a good start, there are many areas of improvement. 

In particular:

After the refactoring
The below code, is the example application after it has been refactored to use a Brightcove Media API wrapper.  It cleans up the client code and frees ActionScript developers to work at a higher, more spacious level of abstraction

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<?xml version="1.0" encoding="utf-8"?>
<mx:Application 
        xmlns:mx="http://www.adobe.com/2006/mxml">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
        import com.allenmanning.brightaction.api.MediaRequestErrorType;
        import mx.controls.Alert;
        import mx.collections.ArrayCollection;
        import com.allenmanning.brightaction.api.MediaRequestType;
        import com.allenmanning.brightaction.api.MediaRequest;
 
        /** The publisher token to be used to make the requests */
        private var _token:String = "0Z2dtxTdJAxtbZ-d0U7Bhio2V1Rhr5Iafl5FFtDPY8E."
 
        /** 
         * The "click" event listener for the find_all_videos button - work with the MediaRequest
         * Object to construct a "load videos" request, and register event listeners for handling
         * the response.
         */  
        private function onButtonClick():void {
 
            var mediaRequest:MediaRequest = new MediaRequest(
                _token,
                MediaRequestType.FIND_ALL_VIDEOS);
 
            mediaRequest.addEventListener(
                MediaRequest.RESULT_NAME,
                function():void {
                    videosDataGrid.dataProvider = mediaRequest.resultData;
                }
            );
 
            mediaRequest.addEventListener(
                MediaRequest.FAULT_NAME,
                function():void {
 
                    //tyepsafe error checking, no knowledge of BC Media API Error codes here
                    if(mediaRequest.error.errorType() == MediaRequestErrorType.INVALID_TOKEN) {
                        Alert.show("This token is not valid");
                    } else {
                        Alert.show(mediaRequest.error.errorType().toString());
                    }
 
                }
            );
 
            mediaRequest.execute();
 
        }
 
   ]]>
    </mx:Script>
 
    <!--
    	********************************************************************************************
    	VIEW
    	********************************************************************************************
    -->
    <mx:Button id="loadButton"
        label="Load Vidoes"
        click="onButtonClick()"/>
 
    <mx:DataGrid id="videosDataGrid"
            width="100%" height="100%">
 
        <mx:columns>
            <mx:DataGridColumn 
                dataField="id" 
                headerText="ID"/>
            <mx:DataGridColumn 
                dataField="name" 
                headerText="Name"/>
            <mx:DataGridColumn 
                dataField="shortDescription" 
                headerText="Description"/>
            <mx:DataGridColumn 
                dataField="publishedDate" 
                headerText="Published Date"/>
        </mx:columns>
    </mx:DataGrid>
 
</mx:Application>

In this application we have pushed the concerns of the API interaction down into the MediaRequest Object. The View is only responsible with interacting with with this wrapper or Remote Proxy.

Let’s look at a couple techniques being used here:


Typesafe and simplified error handling

The below code snippet is from MediaRequest, the object is responsible for knowing how to interact with Brightcove and parse the results. Additionally, all errors, remote faults and also client-side security exceptions are treated as being the same. It makes the interaction with the client cleaner, because there is only one type of fault to listen for.

The Media Request Object, also interacts with a collaborator: MediaRequestError. This Error object is responsible for storing all errors, client-side or server-side. All of these errors and registered, documented, with the understandable MediaRequestErrorType.

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
	  	//------------------------------------------------------------------------------------------
		// Methods
		//------------------------------------------------------------------------------------------
	    /** 
	     * Execute the Brightcove Media API asynchronous request for the token based upon the given 
	     * Media Request type.  
	     * 
	     * If a result has been returned, populate this object with the result converted into native
	     * AS3 objects, not JSON, and dispatch an event notifing listeners of this object that the 
	     * request has been completed.
	     * 
	     * If a fault has been returned, store the fault in this object and dispatch a Fault event 
	     * notifying listeners that a fault has occured.
	     */
	    public function execute():void {
 
	        // initilize existing result data
	        _resultData = null;
            _error = null;
 
            var url:String = BRIGHTCOVE_API_PATH + 
                "?command=" + 
                _requestType.toString() +
                "&token=" +
                _readToken;
 
            var request:URLRequest = new URLRequest(url);
 
            _loader = new URLLoader();
            _loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
            _loader.addEventListener(Event.COMPLETE, loaderCompleteHandler);
 
            try {
                _loader.load(request);
            } catch (error:SecurityError) {
                handleErrors(
                    new MediaRequestError(
                        MediaRequestErrorType.FLASH_SECURITY_ERROR, 
                        error));
            }
 
	    }
 
        /** Construct an error object and dispatch a fault event */
        private function handleErrors(error:MediaRequestError):void {
 
            _error = error;
            dispatchEvent(new Event(MediaRequest.FAULT_NAME));
 
        }
 
        /** "fault" event listener to the remote call to the Brightcove media APIs */ 
        private function ioErrorHandler(event:Event):void {
 
            handleErrors(new MediaRequestError(MediaRequestErrorType.IO_ERROR, event));
 
        }
 
        /**
         * Return true if the result of the call should be considerd a fault, false otherwise
         * 
         * @param result    the media API result as an ActionScript object
         */
        private function checkAndSetFault(result:Object):void {
 
            if(MediaRequestError.hasError(result)) {
 
                handleErrors(MediaRequestError.createFromResponse(result));
 
            }
 
        }
 
        /** "result" event listener to the remote call to the Brightcove media APIs */ 
        private function loaderCompleteHandler(event:Event):void {
 
            //Get the returned JSON data string
            var response:String = event.target.data as String;
 
            //The list of returned videos is embedded in the "items" property
            //of the root JSON object, so we will decode to a container
 
            //TODO Make the JSON decoding pluggable
            var result:Object = (JSON.decode(response) as Object);
 
            checkAndSetFault(result);
 
            if(_error != null) {
                return;
            }
 
            _resultData = new ArrayCollection(result.items);
 
            //Convert the UNIX date into an AS3 Date
            for(var i:int = 0; i< _resultData.length; i++) {
 
                var video:Object = _resultData.getItemAt(i);
                var n:Number = video.publishedDate;
                video.publishedDate = new Date(n);
 
            }           
 
            dispatchEvent(new Event(MediaRequest.RESULT_NAME));
 
        }


Self Documenting and Typesafe Error Handling with an AS “Enumeration”

Rather than sprinkling error parsing and checking code throughout our Brightcove ActionScript applications, defining them in a typesafe enum is a cleaner and more centralized place to document these conditions and lets other objects work with how they should be handled.

Without real Enum support, we define a set of static constants which are themselves instances a of the MediaRequestErrorType. This object defines a fault name and also registers and associated Error Code (if applicable).

The getErrorForCode() method lets us return a unqiue ErrorType object for a given error code. By using this registration-based approach, we have avoided any long conditional or awkward switch statements. As new errors are discovered, they are simply registered with this object, in a single location.

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package com.allenmanning.brightaction.api {
 
    /**
     * An enumeration modeling the different types of Media Requests Errors
     * 
     * @author amanning
     */
    public class MediaRequestErrorType {
 
        //------------------------------------------------------------------------------------------
        // Member variables
        //------------------------------------------------------------------------------------------
        // Invalid Token was passed to the Media APIs
        public static const INVALID_TOKEN:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("INVALID_TOKEN", 210);
 
        // Request Validation Errors
        public static const VALIDATION_ERROR:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("VALIDATION_ERROR", 301);
 
        // Flash Communication Security Error
        public static const FLASH_SECURITY_ERROR:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("FLASH_SECURITY_ERROR");
 
        // IO Error, a problem with issuing the HTTP Get request to the media API
        public static const IO_ERROR:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("IO_ERROR");
 
        // Unknown Error, either not mapped into this wrapper API, or not invalid construction on 
        // the server
        public static const UNKNOWN_ERROR:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("UNKNOWN_ERROR");
 
        /** The toString value for the enum */
        private var _value:String;
 
        /** Return a string representation of this type */
        public function toString():String {
 
            return _value;
 
        }
 
        /** The error code associated with the error, if any */
        private var _code:Number;
 
        public function code():Number {
 
            return _code;
 
        }
 
        /** A look-up table for all of the codes, this gets initialized in the class constructor */
        private static var _codeLookupTable:Object;
 
        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class constructor - initialize the string representation of this enum
         * 
         * @param value     The string representation of this enum
         * @param code      An associated error code, if any
         */ 
        function MediaRequestErrorType(value:String,code:Number=-1) {
 
            _value = value;
            _code = code;
 
        }
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
 
        /**
         * Given a value and a code, create an error type and register it with the error lookup 
         * table
         * 
         * @param value     The string representation of this enum
         * @param code      An associated error code, if any
         * @return          Registered Media Request Error type
         */
        public static function createAndRegister(
                value:String,
                code:Number=-1):MediaRequestErrorType {
 
            var errorType:MediaRequestErrorType = new MediaRequestErrorType(value,code);
 
            if(_codeLookupTable == null) {
                _codeLookupTable = new Object();
            }
 
            if(errorType.code() != -1 && !_codeLookupTable.hasOwnProperty(code)) {
 
                _codeLookupTable[code] = errorType;
 
            }
 
            return errorType;
 
        }
 
        /**
         * Given an error code, return the corresponding ErrorType object, if it exists, or null 
         * otherwise
         * 
         * @param code  the error code to look up
         * @return      the corresponding ErrorType object, if it exists, or null otherwise
         */
        public static function getErrorForCode(code:Number):MediaRequestErrorType {
 
            if(_codeLookupTable.hasOwnProperty(code)) {
 
                return _codeLookupTable[code];
 
            }
 
            return null;
 
        }
 
    }   
 
}

Where to go from here?
A working example of this can be downloaded here, along with a set of Flexunit tests supporting this wrapper.

This is just the beginning, there are many places to go from here:

Two refactorings to remedy Cairngorm Model Locator Singletonitis May 29, 2009

Posted by Allen Manning in : Cairngorm, Flex, Refactoring, Unit Testing , 9comments

It is a common misconception that Cairngorm prescribes over-use of Singletons.  Cairngorm doesn’t suffer from Singletonitis inherently, but it is easy to use the framework in a way that sprinkles Singletons everywhere- in particular Model Locators.  Presented here are two refactorings (Bind Model Into View and Use Callback in Command) to help reduce the symptoms.

For Flex developers, Singletonitis can lead to an acute onset of Tight Coupling and real complications when writing Unit Tests.  Singletons for this reason should be used judiciously.

Bind Model into View

Views bind to Models via data binding and then update themselves as the model changes.  Many Models may have no direct coupling with the View at all, and may in fact have a closer representation to the business domain than the application itself.

One common cause of Cairngorm Singletonitis is how Views are tightly coupled to the Model Locator.  Here is an example of this below.  Imagine a simple Customer Editor form that pre-populates itself based upon a Model.

1
2
3
4
5
6
7
8
    <mx:form>
        <mx:formitem label="First Name">
            <mx:textinput text="{AppModelLocator.getInstance().selectedUser.firstName}" />
        </mx:formitem>
        <mx:formitem label="Last Name">
            <mx:textinput text="{AppModelLocator.getInstance().selectedUser.lastName}" />
        </mx:formitem>
    </mx:form>

 

We can see here that the View is in very close collusion with its Model Locator collaborator.  Imagine an application that has hundreds of Views used in this way and that we wanted to rename or move a bound property.  Get out the find and replace tool, because we are going to be updating lots of files.

This makes testing especially difficult, because if we want to inject a mock of the model into the View for the purposes of our tests we are stuck. 

These objects are not only tightly bound to the structure of their collaborating Models but also to their type.  AppModelLocator is the only class that is accepted here.

Let’s create a little bit more room to breathe by binding the Views to local properties, which represent their contract, and binding in the views in some sort of Main application space above.

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
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:script>
        /** The user we want to pre-populate */
        [Bindable]
        public var user:User;
    </mx:script>
 
 
    <!--
        ********************************************************************************************
        VIEW
        ********************************************************************************************
    -->
    <mx:form>
        <mx:formitem label="First Name">
            <mx:textinput text="{user.firstName}" />
        </mx:formitem>
        <mx:formitem label="Last Name">
            <mx:textinput text="{user.lastName}" />
        </mx:formitem>
    </mx:form>
1
2
3
4
5
6
    <!--
        ********************************************************************************************
        VIEW
        ********************************************************************************************
    -->
    <userform id="userForm" user="{AppModelLocator.getInstance().selectedUser}" />

 

Now we can re-jigger the structure of our models to our hearts content and only be required to change one place that we are injecting this in.  In addition, we can create Model Mocks in our unit tests and inject those in for testing purposes.  The Views have not concept of a ModelLocator, and there they can be Singleton free.

Use Callback in Command

One of the main uses of Responding Commands are to update local models with data updated in remote systems- essentially syncing remote persisted objects with their local Flex cache representations.

It can seem only natural to ‘get a handle’ on the Model data through a Singleton invocation from inside of the Command itself.

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
/**
 * Execute this command - proxy a remote call through the business delegate
 * 
 * @param event    the CairngormEvent which was dispatched to invoke this command
 */
public function execute(cairngormEvent:CairngormEvent):void {
 
    var event:UpdateUserEvent = (cairngormEvent as UpdateUserEvent);
 
    var delegate:IMetadataDelegate = new AccountDelegate(this);
 
    delegate.updateUser(event.UserToUpdate);
 
}
 
/**
 * Called as part of a successfull result from the delegate- update the local model with the
 * result
 * 
 * @parm data   the data passed back from the server
 */
public function onResult(event : * = null):void {            
 
    var updatedUser:User = (event.result as User);
 
    AppModelLocator.getInstance().selectedUser = updatedUser;
 
}

Alas, again we find ourselves stuck in the overly-tight coupling of Singletonitis.  Refactoring this Command to take a callback rather than performing the update work itself breaks the fever.

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
49
50
51
52
53
54
55
56
    /**
     * Cairngorm event broadcast to update a user account
     * 
     * @author amanning
     */
     public class UpdateUserEvent extends CairngormEvent {
 
        //------------------------------------------------------------------------------------------
        // Member Variables
        //------------------------------------------------------------------------------------------
        /** The unique name or type for this event */
        public static const NAME:String = "UPDATE_USER_EVENT";
 
        /** The user to update */
        private var _userToUpdate:UserDTO;
 
        public function get userToUpdate():UserDTO {
 
            return _userToUpdate;
 
        }
 
        /** The callback function for updating the user */
        private var _updateUserCallback:Function;
 
        public function get updateUserCallback():Function {
 
            return _updateUserCallback;
 
        }
 
        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class constructor for this event, initialize parameters
         * 
         * @param userToUpdate          The user account to update
         * @param updateUserCallback    The callback function for updating the user- 
         *                              this function takes a single userDTO as
         *                              it's only argument:  
         * 
         *                              updateUserCallback(user:UserDTO)
         */
        public function UpdateUserEvent(
                user:UserDTO,
                updateUserCallback:Function) {
 
            super(NAME);
 
            _userToUpdate = userToUpdate;
            _updateUserCallback = updateUserCallback;            
 
        }
 
    }
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
/**
 * Execute this command - proxy a remote call through the business delegate
 * 
 * @param event    the CairngormEvent which was dispatched to invoke this command
 */
public function execute(cairngormEvent:CairngormEvent):void {
 
    var event:UpdateUserEvent = (cairngormEvent as UpdateUserEvent);
 
    _event = event;
 
    var delegate:IMetadataDelegate = new AccountDelegate(this);
 
    delegate.updateUser(event.UserToUpdate);
 
}
 
/**
 * Called as part of a successfull result from the delegate- update the local model with the
 * result
 * 
 * @parm data   the data passed back from the server
 */
public function onResult(event : * = null):void {            
 
    var updatedUser:User = (event.result as User);
 
    _event.updateUserCallback(updatedUser);
 
}

 

Here a callback function ‘updateUser’ is passed into the event that dispatched the Command. This callback references a method on a known Model- for example a Presentation Model associated with the View that dispatched it.  The Command is only required to call it with the returned data. 

The Command has no knowledge of the existence of a Model Locator, or anything about the View for that matter.

Calling getInstance() only once, if at all

With these strategies in place, it should be possible to inject Model dependencies only in the top-level parent View.  This injection can happen through either direct data binding or by an Inversion of Control approach like Spring Actionscript.  The binding of these Models can then cascade throughout the rest of the application.