Wednesday, November 25th, 2009

CBC Radio 3 Case Study

Category: Articles, Editorial, Prototype, Showcase

>Phil Rabin of CBC Radio 3 has kindly written a guest post on his experience creating a fantastic Web interface for the station that uses Flash for audio, but a full HTML experience that maintains state from page to page.

cbcradio3

CBC Radio 3 is a community, radio station and user-generated independent music library which is a small department of the Canadian Broadcasting Corporation. When the CBC Radio 3 web team was called upon to rebuild the site we were confronted with the technical problem of having an uninterrupted music experience for our users. The old design of the site (see image) achieved this by embedding a flash player in the body with the content being served through a statically positioned iframe in the center of the page. Radio 3′s content offerings were outgrowing the design so we went with a full page 1000px-wide layout with the player resting in the page. This created an obvious hurdle being that with a fresh page load comes a bad listening experience like myspace where a single wrong click breaks the audio. Also, not having popup player was a design decision that was made to give the website a more integrated feel.

We decided to completely removed flash from the UI equation and went full html/ajax because we found that it offered more flexibility and play with the page. The hardest part was figuring out a way to maintain state on each page load while keeping the audio continuous.

We went with an old-school frameset to create a type of inter-frame communication with the top level frameset acting as the orchestrator/bootstrapper. The visible “UI Controller” frame is completely blown out with the stateful player frame hidden from view.

The stateful player frame contains hidden swfs to handle playing audio and connecting to RTMP for our live streaming. All the communication in and out of flash is handled by a couple gateway javascript classes to abstract out the flash from the rest of the application.

Here’s an example of a communication gateway for wrapping the events coming to and from flash. The event objects are native flash event objects that get sent by Flash’s ExternalInterface and come in as JSON that can:

javascript
< view plain text >
  1. CBCR3.namespace("CBCR3.Player.External");
  2.  
  3. CBCR3.Player.External.RTMPGateway = Class.create(CBCR3.Commons.EventDispatcher, {
  4.      
  5.     initialize:function($super)
  6.     {
  7.         $super();        
  8.     },
  9.      
  10.     //Functions to receive events from flash    
  11.     sendStreamEvent:function(event)
  12.     {
  13.         this.dispatchEvent(event.type);
  14.     },
  15.    
  16.     sendMetaDataEvent:function(event)
  17.     {        
  18.         var metaData = new CBCR3.Player.Mappers.StreamMetaDataDtoMapper().mapCollection(event.metaData);        
  19.         this.dispatchEvent(CBCR3.Player.Events.RTMPStreamEvent.metaDataReceived, {metaData:metaData});
  20.     },
  21.        
  22.     //Functions to send commands from flash
  23.     sendStreamCommand:function(commandName, commandArgs)
  24.     {        
  25.         $(CBCR3.Player.Globals.rtmpPlayerId).streamCommand(commandName, commandArgs);
  26.     }
  27. });

A single instance of this gateway is always maintained in the application which is called by a sort of simple container like this:

javascript
< view plain text >
  1. ExternalInterface.call("CBCR3.Player.Application.IoC.getInstanceOf('rtmpGateway').sendStreamEvent", event);

An instance of the gateway has to be maintained by the player application because events coming from flash have no context. This way the application classes can subscribe to events coming from flash like this:

javascript
< view plain text >
  1. CBCR3.Player.Players.RTMPPlayer = Class.create(CBCR3.Commons.EventDispatcher, {
  2.     initialize:function($super, rtmpGateway, thumbLookupService)
  3.     {
  4.         $super();    
  5.          
  6.         this.rtmpGateway = rtmpGateway;        
  7.  
  8.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.metaDataReceived, this.rtmpMetaDataReceivedHandler.bind(this));        
  9.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.ready, this.streamReadyHandler.bind(this));
  10.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.connecting, this.streamConnectingHandler.bind(this));
  11.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.streaming, this.streamStreamingHandler.bind(this));
  12.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.connected, this.streamConnectedHandler.bind(this));
  13.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.metaDataConnected, this.rtmpMetaDataConnectedHandler.bind(this));
  14.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.failed, this.rtmpMetaDataFailedHandler.bind(this));
  15.  
  16.  
  17.     },
  18.  
  19.     streamReadyHandler:function(event)
  20.     {
  21.         //handle stream event
  22.     },
  23.  
  24.     streamConnectingHandler:function(event)
  25.     {        
  26.         //handle connecting event
  27.     },
  28.  
  29.     streamConnectedHandler:function(event)
  30.     {
  31.         //handle connected event
  32.     },
  33.  
  34.     rtmpMetaDataConnectedHandler:function(event)
  35.     {
  36.        //handle meta deta connected event
  37.     },
  38.  
  39.     rtmpMetaDataReceivedHandler:function(event)
  40.     {        
  41.         //handle meta data event etc etc
  42.     }
  43. );

At the core, audio is always played by Flash. The swfs broadcast events, such as audio head position and download progress of mp3s, and connection, streaming, meta data events from RTMP. Those events get passed on the instance of the hidden stateful player.

Since the server frame is only loaded once when the site first loads, an instance of the stateful server player is instantiated for the entire session on the site. On each client frame page load, the server player instance is “injected” into the visible client UI controller by the “bootstrapper” top frame. State is maintained in that instance which allows for the controller to query the state of that object and reestablish everything like which track is playing, progress, time, thumbs up or down status, shuffle, play mode (stream or individual mp3 and playlists), etc. Everything had to be covered like if an mp3 was in mid-load when someone browsed to a new page, the loading progress had to pickup on the next page. Here’s a example of the bootstrapper code contained in the frameset:

javascript
< view plain text >
  1. CBCR3.namespace("CBCR3.Player.Application");
  2.  
  3. CBCR3.Player.Application.R3PlayerBootStrap = Class.create({
  4.  
  5.     serverFrame:null,
  6.     clientFrame:null,
  7.  
  8.     autoStart:true,
  9.     permalink:null,
  10.  
  11.     initialize:function(preferences)
  12.     {        
  13.         this.autoStart = preferences.autoStart;
  14.         this.permalink = preferences.permalink;        
  15.     },
  16.      
  17.     setServerFrame:function(serverFrame) {
  18.         this.serverFrame = serverFrame;
  19.     },    
  20.          
  21.     setClientFrame:function(clientFrame) {
  22.         this.clientFrame = clientFrame;        
  23.     },
  24.      
  25.      
  26.     //TRY LOAD PLAYER
  27.     loadPlayer:function()
  28.     {        
  29.         if(!this.clientFrame || !this.serverFrame)
  30.             return;
  31.  
  32.         //both frames are loaded at this point
  33.         if(this.serverFrame.getPlayerInstance() == null)
  34.             this.initializePlayer();
  35.         else
  36.             this.resumePlayer();
  37.     },
  38.      
  39.     initializePlayer:function()
  40.     {
  41.         this.serverFrame.initPlayer();
  42.  
  43.         this.clientFrame.checkEnvironment();
  44.         var masterPlayerInstance = this.serverFrame.getPlayerInstance();
  45.         this.clientFrame.loadPlayer(masterPlayerInstance);
  46.         masterPlayerInstance.addEventListener("stateInitEvent:streamPlayerLoaded", this.streamPlayerLoadedHandler.bind(this));
  47.         masterPlayerInstance.addEventListener("stateInitEvent:playlistPlayerLoaded", this.playlistPlayerLoadedHandler.bind(this));
  48.     },
  49.      
  50.     resumePlayer:function()
  51.     {
  52.         this.clientFrame.loadPlayer(this.serverFrame.getPlayerInstance());
  53.         this.clientFrame.resumePlayer();        
  54.     },
  55.      
  56.     streamPlayerLoadedHandler:function(event)
  57.     {
  58.         if(this.autoStart && this.permalink.include("/stream/"))
  59.             this.clientFrame.getPlayerInstance().stream(this.permalink);
  60.     },
  61.      
  62.     playlistPlayerLoadedHandler: function(event)
  63.     {
  64.         if(this.autoStart && this.permalink.include("/play/"))
  65.             this.clientFrame.getPlayerInstance().playlist(this.permalink);
  66.     }
  67. });

We used Prototype/Scriptaculous as the base for the entire site. All the AJAX communication is handled with asp.net web services with scripting enabled. ASP.NET takes care of all the serialization of DTO’s (Data Transfer Object) into JSON which are specific to the player application.

All of the classes in the application are written using Prototype’s Class/inheritance model. Most of the classes subclass from a base EventDispatcher much like AS3, which is adapted from Matthew Foster’s example for Prototype and our own custom Event model. This allows for a nice separation of concerns and decoupled classes throughout the application and allows the UI Controller to add event listeners to custom events coming from the server player instance.

javascript
< view plain text >
  1. CBCR3.namespace("CBCR3.Commons");
  2.  
  3. CBCR3.Commons.EventDispatcher = Class.create({
  4.      
  5.     buildListenerChain:function()
  6.     {
  7.          
  8.         if(!this.listenerChain)
  9.             this.listenerChain = {};                                    
  10.      
  11.     },
  12.      
  13.     addEventListener:function(type, listener){
  14.                                    
  15.         if(!listener instanceof Function)
  16.             alert("Listener isn't a function");
  17.              
  18.         this.buildListenerChain();
  19.                  
  20.         if(!this.listenerChain[type])                          
  21.             this.listenerChain[type] = [listener];
  22.         else
  23.             this.listenerChain[type].push(listener);
  24.          
  25.     },
  26.          
  27.     hasEventListener:function(type)
  28.     {
  29.         return (typeof this.listenerChain[type] != "undefined");
  30.     },
  31.      
  32.     removeEventListener:function(type, listener)
  33.     {
  34.         if(!this.hasEventListener(type))
  35.         return false;
  36.      
  37.         for(var i = 0; i &lt; this.listenerChain[type].length; i++)
  38.             if(this.listenerChain[type][i] == listener)
  39.                 this.listenerChain.splice(i, 1);
  40.      
  41.     },
  42.      
  43.     clearEventListeners:function()
  44.     {
  45.         this.listenerChain = {};
  46.     },
  47.      
  48.     dispatchEvent:function(type, data, target)
  49.     {        
  50.         var    event = new CBCR3.Commons.Event(type, data, target || this);
  51.         this.buildListenerChain();
  52.      
  53.         if(!this.hasEventListener(type))
  54.             return false;
  55.      
  56.         this.listenerChain[type].any(function(funct){
  57.             return (funct(event) == false ? true : false);
  58.         });
  59.     }
  60. });

This also allows the UI Controller to unsubscribe from all events when the page unloads. This was key in memory management and so that we don’t get orphaned references to instances of the UI Controller.

The most difficult part of the whole player project was re-establishing state of the controller on every page load. We hoped that we could implement some sort of state-pattern with no luck. In the end, the UI controller contains a couple monster resume methods that we haven’t been able to abstract out of that class. We’d like to bring in some sort of MVC architecture that wires up the UI player view to a state object. Any suggestions would be welcome! Go check out the site and give us some feedback!

Dion: I then asked Phil about the CBCR3 library and he replied

CBCR3 is the base namespace for all th javascript controls and apps written for the site. Everything for the player is in CBCR3.Player, the concert calendar is CBCR3.Gigs, etc. We have a shared base lib which is in CBCR3.Commons.

An issue with Prototype that we had was some bug with including 1.6.1 in a frameset in Opera. So, right now the frameset is holding an older version of prototype while the frames have the latest. One thing that Prototype was seriously lacking was Date extensions. (like addDay, addMonth, addWeek) etc.
We ended up going with YUI’s DateMath widget for that which really smoothed out working with dates.

Most of the issues we had cross-browser stuff was with IE6 (no surprise), which were almost all related to CSS rendering bugs, and IE DOM manipulation problems. A big one was upon the dynamic removal of items from lists. IE has a real hard time refreshing the positions of items. We had to write methods like

javascript
< view plain text >
  1. myList.select("li").each( function(item){
  2.    li.setStyle({display:"none"});
  3.    li.setStyle({display:"block"});
  4. });

this would in effect “nudge” the browser and force it to update the position of the remaining DOM elements. In the end, we chose to drop IE6 support and to tell you the truth, we haven’t heard a single complaint about it!

Related Content:

Posted by Dion Almaer at 6:05 am
11 Comments

+++--
3 rating from 31 votes

11 Comments »

Comments feed TrackBack URI

Impressive on first sight, but iframes should not be revived. Worse enough, that google cal html widgets and others still use them.

Comment by gabel — November 25, 2009

I’m glad Ajaxian took the time to write about this site, I’ve been marveling at the design since they launched it a month ago. The only thing I’d like to see different is have the URL changed as you navigate through the site. Although you can get the permalink with a button on the page, I’d love to see it updated in the location bar as well. Oh the fun with iframes.

Comment by dgavey — November 25, 2009

Beautiful work.

I am a Canadian expat and I have to confess I never listened to CBC whilst in Canada but your site is going to get prime bookmark toolbar real estate now. You’ve seriously outclassed the BBC’s web radio efforts on a fraction of the budget. Well done!

Comment by srboisvert — November 25, 2009

A similar approach is adopted by http://www.whatpeopleplay.com/. In this case, however, the entire player (including the UI) is housed within the frame static. Although this solution is more dirty, I do not understand (for my stupidity) the need to load every time the UI and then re-initializing ..

Comment by gakag — November 25, 2009

Impressive work! I would never have conceived of using frames this way! It is also really awesome to see CBC doing some amazing work in-house now!

Comment by thomasjbradley — November 25, 2009

we recently built the website for an austrian radio station and were facing the same challenge as mentioned by phil rabin – having an uninterrupted music experience for our users. we went another way though, delivering fully ajax-driven content, having the player ui nicely stay in place. the advantages we came up with are as follows:
- no frames (our technical staff refused this)
- distinct urls for every content, allowing bookmarks
- no need to re-initialize the player ui
it’s still work in progress, but you might have a look at http://www.kronehit.at

Comment by schellmax — November 25, 2009

I really like this solution but am I the only one that has a problem with the fact that this renders page urls useless. As a user who finds a cool blog article, there is no way to copy the url? This is a HUGE usability problem.

Comment by louis — November 25, 2009

I would say that for this reason alone (lack of changing url’s), you should throw away such approach and start from scratch. A long article describing old, non-ajax and soon-to-be deprecated technology. HTML 5 does not have framesets exactly for usability reasons.

thomasbradley
“I would never have conceived of using frames this way” – nor should you. I can’t stress enough, how bad they are. Obviously schellmax’s technical staff knows this, as they’d rather rebuild entire page loading mechanism (changing urls, browser history, memory management, resource management etc.) instead of just using frames.

Comment by jx12345 — November 26, 2009

Bootstrapper. Hurrr

Comment by Darkimmortal — November 26, 2009

I also think the page interaction is kinda weird this way, the player is not visible at a certain time when the page is loaded, but the music is still playing.

I am a big fan of the way http://thesixtyone.com is doing their player. I don’t know about the quality of their Ajax implementation code wise, but their site works really well and still supports unique URL’s for individual songs.

Comment by cra5h — November 26, 2009

We desperately want to implement SWFAddress! It’s at the top of the priority list. Since we just relaunched we’ve been primarily concerned with ironing out bugs, and re-implementing things we left out in order to get the site out on time.

Getting SWFAddress properly implemented won’t be a trivial task because we have pretty complicated frame-rerouting logic to be able to enter the site with a permalink like http://radio3.cbc.ca/play/band/MSTRKRFT and have the right playlist and correct page load in the frame. It was mostly an issue of resources since we only have two developers here. Trust me, we want it more than anyone.

As far as loading all the page content via ajax, it’s definitly a clean solution. Maybe something to consider for the future, but in the mean time I think SWFAddress would solve the url issue.

Comment by philrabin — November 26, 2009

Leave a comment

You must be logged in to post a comment.