Friday, March 4, 2011

Adding Accessibility to the REOPS Player

OSMF and the REOPS player

REOPS uses the dynamic streaming capabilities of the Adobe Open Source Media Framework (OSMF) to deploy a configurable, re-skinnable video player for the web. At runtime, an XML file is used to configure layout and streaming video locations while custom skins are loaded through an exported swf. I chose to implement this application for a recent government project, but had to re-engineer the code to make the player compliant with section 508 -- specifically, making it accessible for people with assisting devices. Additionally, I added a component within the REOPS skinning framework to dynamically load a "preroll" or "poster" image prior to video play. The entire project can be downloaded here. It can also be deployed with Adobe Flash by creating a 768 x 600 fla with "Main" as the class for the stage.

Implementing the REOPS player

My first step was to implement and familiarize myself with the REOPS player. I used FlashDevelop to create a project based on the demos provided in the REOPS download. I chose to use the Lunar skin, but had to fuss with it to export an swf that worked correctly for me. Most of my troubles were solved by making sure the fla is in the right place when exporting, and re-exporting the skin when the REOPS code changes.

I used aDesigner to do a quick check on the player's accessibility and found that none of the controls were labeled. I talked about using this tool in my previous post. This is the script aDesigner said the screen reader gets for my basic REOPS implementation with the Lunar skin:

Flash movie start
2 Button
6 Button
0:02
7:50
9 Button
/
11 Button
12 Button
Flash movie end

For a person who has no visual reference, this is not very useful. Having properly labeled controls is just as vital for a non-sighted user to manipulate the content as for a sighted person. With the Flash Accessibility class, the buttons can be labeled for those using a screen reader to fulfill 508 requirements. Since these objects are supplied by a skin swf, that is where I started my redesign.

Updating the Lunar Skin

The REOPS skinning system allows you to create different visual "skins" for the same components (like the play/pause button) which are then loaded in from a swf that is referenced in the XML config file. This is a separate swf from the final swf which will be embedded in an HTML page. I had some fits and starts getting the Lunar skin that came with the download to work correctly in my project. After repairing a few TextFields (whose alphas were "0" or had no characters embedded), I ended up discarding the "_code_" component from the Lunar.fla library and set a relative path to the src folder by going to Publish Settings -> Flash (tab) -> Settings (actionscript 3.0) -> source path (tab). There, I added a folder for the relative path "../../../src" -- allowing the fla to find the objects' classes when exporting from the skins folder.

I adjusted some of the Properties class export settings for the movie clips in the Lunar.fla library. For instance, "PlayButton" was given a class of com.realeyes.osmfplayer.controls.PlayButton with a base class of com.realeyes.osmfplayer.controls.ToggleButton, because that's what PlayButton.as is extending. I did this for the other buttons (including the scrubber) that extended from ToggleButton as well. It is essential to remember that, when you alter certain classes, the skin must be re-exported before you create the final swf. Otherwise, instantiations of the skin elements won't have the same methods and variables.

Adding a "Poster Image"

One of the requirements for my video project was not a facet of the REOPS player -- that of a "poster image," or an image presented in lieu of any video content. Instead of the video immediately loading and playing, the client requested a still image from the video accompanied by a "start" button.

To accomplish this, I created a "PosterImage" symbol in my Lunar skin fla file. This MovieClip is comprised of a VideoPlayButton and a black "background" MovieClip. The Play Button extends com.realeyes.osmfplayer.controls.ToggleButton, as you can see in its library Properties window, meaning it will fire a ToggleEvent when clicked.

The PosterImage is extended from SkinElementBase just like all the other components in the skin. In the config file (reops_config.xml), I can add XML tags for the poster image to pass on to the REOPS app:

reops_config.xml

<skinElement id="posterImage"
elementClass="com.realeyes.osmfplayer.controls.PosterImage"
initMethod="initPosterImageInstance"
hAdjust="0" vAdjust="0"
scaleMode="STRETCH"
hAlign="CENTER"
vAlign="CENTER"
autoPosition="true"
imageURL="assets/images/poster.png"
imageDescription="Picture of the National Library of Medicine" />

The properties "imageURL" and "imageDescription" are custom variables for PosterImage; the latter will be what the Accessibility class will send assisting devices as the poster image's name. This setup allows images to be swapped in with out recompiling. The other variables in the tag instruct layout of the component just like the others. I captured a still of the video to use as the poster image, poster.png.

Updating the Code for Accessibility

The REOPS class files needed additional code to include the poster image and add accessibility. The first task was to get my AccessibilityManager singleton instantiated. This class sets off a timeout to ensure an accurate check of whether an assisting device is being used -- the resulting boolean value effectively becomes a global variable.

REOPS.as
/////////////////////////////////////////////
// CONSTRUCTOR
/////////////////////////////////////////////

public function REOPS( p_loaderInfoParams:Object = null )
{
accMgr = AccessibilityManager.getInstance();// starts Accessible Device Check
...
}

Next, I dug into the the main view of the REOPS application, SkinContainer. This class instantiates components (like the control bar, loading indicator, closed-caption field -- and now the poster image) from the skin swf, informed by properties taken from the reops_config.xml file.

The control bar is the first component built. The ControlBar constructor grabs the same instance of AccessibilityManager (assigned to accMgr ), then instantiates the controls. Each control has an initialization function where its Accessibility information is created. With the ReLunar skin, the Play/Pause button is the first initialized:

ControlBar.as

protected function _initPlayPauseButton():void {
playPause_mc.addEventListener(ToggleButtonEvent.TOGGLE_BUTTON_CLICK, _onPlayPauseClick);
accMgr.addAccessibility(playPause_mc, "play pause");
_addControlItem(playPause_mc);
}

The addAcessibility method in my AccessibilityManager singleton gives an appropriate Accessibility.name property to the MovieClip . This is done for all other controls. Not all controls in ControlBar.as are featured or initialized in the Lunar (or my ReLunar) skin. Be aware that, while I added accessibility code to all skin objects, I only really tested the usability for objects instantiated with the ReLunar skin.

For instance, I realized I could improve the usability for the "current time" readout by putting currentTime_txt inside a Sprite with an Accessibility.name property to give non-sighted users context for the digits to follow:

ControlBar.as

protected function _initCurrentTimeText():void {
_addControlItem(currentTime_txt);
var spCurrentTimeContainer:Sprite = new Sprite();
spCurrentTimeContainer.x =0;
spCurrentTimeContainer.y = 0;
addChild(spCurrentTimeContainer);
spCurrentTimeContainer.addChild(currentTime_txt);
accMgr.addAccessibility(spCurrentTimeContainer, "elapsed time", "displays elapsed time of video", false);
spCurrentTimeContainer.tabEnabled = false;
}

The container of the TextField also receives a tabEnabled = false setting. Tab-enabling is also removed from progress_mc, the parent of the scrubber:

ControlBar.as

protected function _initProgressBar():void {
accMgr.addAccessibility(progress_mc, "", "", false);// make silent
progress_mc.tabEnabled = false;
progress_mc.scrubber_mc.focusRect= false;// disables tabbing with arrow keys and eliminates yellow border, which is redrawn by focus manager
accMgr.addAccessibility(progress_mc.scrubber_mc, "scrubber", "use left and right arrow key to scrub video forward and back", false);
_addControlItem(progress_mc);
}

The progress_mc is rendered "silent" to assisting devices, while the scrubber gets a name of "scrubber" and an automatic tab-enabling. The scrubber_mc also has focusRect disabled, allowing the up/down arrow keys, usually reserved for tabbing, to instead scrub the video forward or backward when the scrubber is focused. The focus rectangle is redrawn by the focusChangeHandler(), triggered by a listener added when all ControlBar controls have been checked in _checkControls(). A listener is also added here to trigger controlBarKeyHandler(), which examines keystrokes. The same strategy is used for volume_mc , with the help of _initVolumeButton() -- up/down arrows are reserved for increasing/decreasing the volume when the mute/unmute button is focused.

ControlBar.as
private var _focus:Sprite;// mc that has focus
private var focusBorder:FocusBorder; // a sprite with a yellow border created to surround _focus object

protected function focusChangeHandler(e:FocusEvent):void {
if (focusBorder) {
// remove old focusBorder before reseting _focus
focusBorder.parent.removeChild(focusBorder);
}

_focus = Sprite(e.relatedObject);
var border:FocusBorder = new FocusBorder(_focus.getBounds(this) );// create yellow rectangle around Sprite
_focus.addChild(border);
focusBorder = border;
}

protected function controlBarKeyHandler(e:KeyboardEvent):void {
var i:uint = 0;
var seekPercent:Number ;
switch (e.keyCode){
case 38: {
//vol up
if (_focus == volume_mc) {
for ( i = 0; i < 1; i++)
{
this.dispatchEvent(new ControlBarEvent(ControlBarEvent.VOLUME_UP));
}
}
break;
}
case 40: {
//vol down
if (_focus == volume_mc) {
for (i = 0; i < 5; i++)
{
this.dispatchEvent(new ControlBarEvent(ControlBarEvent.VOLUME_DOWN));
}
}
break;
}
case 37: {
//seek rewind
if (_focus ==progress_mc.scrubber_mc) {
for (i = 0; i < 1; i++)
{
seekPercent = (currentTime -5)/ _duration;
var cbSeekBackEvent:ControlBarEvent = new ControlBarEvent(ControlBarEvent.SEEK, 0, seekPercent);
this.dispatchEvent(cbSeekBackEvent);
}
}
break;
}
case 39: {
//seek forward
if (_focus ==progress_mc.scrubber_mc) {
for (i = 0; i < 1; i++)
{
seekPercent = (currentTime +5)/ _duration;
var cbSeekForwardEvent:ControlBarEvent = new ControlBarEvent(ControlBarEvent.SEEK, 0, seekPercent);
this.dispatchEvent(cbSeekForwardEvent);
}
}
break;
}
}
}

SkinContainer also runs an initialization method for each of its components if a corresponding method name is found in the config file's XML. In initControlBarInstance(), I added a superfluous reportAccessibility() call to simply trace the control bar's children and identify what accessibility properties were added in the output. Next, a checkAccessibility() call is made to see if the auto-hide property needs to be overridden. This registers the SkinContainer as a listener to be notified when the device check by the AccessibilityManager singleton is finally executed. In the event that the check already took place, found a device, and updated; accMgr.update() makes sure the accessibilityProperties are registered with the device and the auto-hide functionality is disabled.

SkinContainer.as

public function checkAccessibility():void {
if (accMgr.accCheck(this as IAccessibilityListener) == AccessibilityManager.ACTIVE){
overrideAutoHide();
} // if unchecked, this is registered as listener -- when check completes, Subject notifies via setAccessibilityActive()
}

public function setAccessibilityActive(accActive:Boolean):void {
if (accActive) {
overrideAutoHide();
} else {
autoHide = _controlBar.autoHide;
}
}

private function overrideAutoHide():void {
_controlBar.visible = true;
autoHide = false;
}

This results in the user of an assisting device always having the controls visible, no matter what the auto-hide property is in the XML configuration file.

The SkinContainer continues to instantiate the other components. The loading indicator and closed-caption elements are fine -- a device is just going to read their text, which is all that is pertinent for each. Finally, the newly-added PosterImage is instantiated. The PosterImage constructor gives accessibilityProperties and a click listener to the play button. Accessibility properties are added without the benefit of the accessibility singleton, because the AccessibilityManager instance is not going to be available when the skin is compiled. The AccessibilityManager is still going to update all properties if it finds a device.

PosterImage.as

public function PosterImage() {
super();

playBtn.addEventListener(ToggleButtonEvent.TOGGLE_BUTTON_CLICK, dispatchStartEvent);
var accProps:AccessibilityProperties = new AccessibilityProperties();
accProps.name = "Start Video";
playBtn.accessibilityProperties = accProps;
}

The reops_config.xml file also declares a method "initPosterImageInstance" for PosterImage , which in turn calls _initPosterImageListeners().


SkinContainer.as

private function _initPosterImageListeners() :void {
_controlBar.visible = false;
_posterImage.addEventListener(PosterImageEvent.IMG_LOADED, onPosterImageStateChange);
_posterImage.addEventListener(PosterImageEvent.IMG_FAIL, onPosterImageStateChange);
_posterImage.addEventListener(PosterImageEvent.LOADING, onPosterImageStateChange);
_posterImage.addEventListener(PosterImageEvent.START, startVideo );
_posterImage.loadPosterImage();
}

The _controlBar is now invisible -- both to the screen and any screen reader. In addition, the _posterImage begins to load the image specified in the XML file and sets up a play button listener to call startVideo(). This method hides _posterImage and dispatches a ControlBarEvent.PLAY event. The loadPosterImage() call results in a bitmap added in doneLoad(), which is immediately scaled to the same dimensions of the "background" MovieClip and dumped in as a child. This allows layout changes to PosterImage stretch the bitmap accordingly. The posterImage object is also given the appropriate AccessibilityProperties:

PosterImage.as

protected function doneLoad(e:Event):void {
this.dispatchEvent(new PosterImageEvent(PosterImageEvent.IMG_LOADED));
trace("PosterImage :: DONE LOADING " + imageURL);
posterImage = Bitmap(loader.content);
posterImage.width = bg.width;
posterImage.height = bg.height;
bg.addChild(posterImage);
var accManager = AccessibilityManager.getInstance();
accManager.addAccessibility(bg, imageDescription);
accManager.update();
this.setChildIndex(playBtn, (this.numChildren - 1));// put play button on top
loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, doneLoad);
loader.contentLoaderInfo.removeEventListener(ProgressEvent.PROGRESS, loadingUpdate);
loader.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, loadingError);
loader = null;
}

Finally, the _onCurrentTimeChange() method is adjusted to return the poster image to "visible" just before the video completes and is wiped out:

SkinContainer.as

protected function _onCurrentTimeChange( p_evt:TimeEvent ):void{

...

if (percentPlayed > .999) {
seekTo(0);
_mediaPlayerCore.stop();
displayPosterImage();
}
}

The displayPosterImage() and hidePosterImage() methods simply swap visibility for _posterImage and _controlBar. The poster image returns at the end of the video with the button to allow replay.

Checking the New Implementation

These changes should result in a REOPS video player with accessible controls and a dynamically loaded poster image. If we deploy the re-engineered player, we'll see:

  • a poster image appears with a play button upon load
  • control bar controls are inaccessible with the poster image and start button up
  • the auto-hide functionality is disabled when an assisting device is present
  • tabbing the control bar is relugated to the play/pause, scrubber, mute/unmute, closed caption, and screen re-size buttons
  • with the scrubber focused, the left and right arrow scrubs the video forward and backward.
  • with the mute/unmute button focused, the up/down arrows control volume
  • the poster image and start button return upon video completion

Checking the accessible script with aDesigner, we see that, when the posterImage is up, our GUI summary is:

Accessible REOPS_Player Window
Flash movie start
Graphic Akami Logo: Powering a Better Internet Since 1998, Celebrating 10 Years
Start Video Button
Flash movie end

When the movie begins playing, refreshing the summary gives us:

Accessible REOPS_Player Window
Flash movie start
play pause Button
scrubber Button
Graphic elapsed time
0:03
Graphic total time
7:50
mute unmute Button
display closed caption Button
change video size Button
Flash movie end

The REOPS player now has accessible controls for people using assisting devices! Thanks to nils.thingvall@gmail.com and all the folks supporting REOPS.

Download Accessible REOPS Player project

No comments:

Post a Comment