Brightcove videoplayer accessibility plugin

Q2 2014
Javascript/Actionscript solution to Brightcove player accessibility problems

DOWNLOAD SRC

When I wrote this, Brightcove did have an “Accessible” template – which was not even remotely accessible. Accessible means I should be able to navigate the web page/application with my eyes closed, using only the keyboard, and access the same content in as smooth and efficient an experience as a visually-equipped user.

This solution isn’t perfect – I still received complaints from users about tab-accessibility inconsistency and video control button labels not changing on toggle, which I couldn’t control without access to the video src. Ultimately, we decided to move off of Brightcove as a video solution, but if you’re stuck with it, this will help significantly.

Tabbing into and out of Flash objects embedded on an HTML page is a common problem — if you can’t tab into an embedded application, it doesn’t exist to any user using a screenreader. The solution I stitched together uses a Class from Adobe to setup the tab ordering of clickable elements within the swf, in combination with some javascript by Richard England to handle focus into and out of the swf.

1. Create the swf that will become your custom Brightcove plugin. This is an empty .swf which, upon being added to the stage, will explore the heirarchy of the swf into which is has been imported, find the video controls, will pass those to the SWFFocus class. Below isthe Document class of your .fla.

package {
	import com.adobe.swffocus.SWFFocus;

	import flash.accessibility.AccessibilityProperties;
	import flash.accessibility.Accessibility;
	import flash.display.Sprite;
	import flash.events.Event;
//	import com.richardengland.utils.Tabbing;
	import flash.display.DisplayObjectContainer;
	import flash.display.DisplayObject;
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;


	public class SWFFocusFix extends Sprite {
		public function SWFFocusFix() {
			if (stage) {
				init();
			} else {
				addEventListener(Event.ADDED_TO_STAGE, init);
			}
		}

		private function init(event:Event = null):void {
			trace("SWFFocusFix.init");

			var player:Object = parent;
			var counter:Number = 0;

			var buttonContainer:Object;

			while (player.parent) {
				for (var i:uint = 0; i < player.numChildren; i++) {
					var child:DisplayObject = player.getChildAt(i);
					var childClass = getQualifiedClassName(child);

					if (childClass.indexOf("BEMLContainer") != -1) {
						//then this is where we want the tabbing focus to be
						buttonContainer = child;
					}

				}

				player = player.parent;
				counter++;

				if (buttonContainer) {
					break;
				}
			}

			traceDisplayList(DisplayObjectContainer(player));

			if (player) {
				trace("Found player.stage! | " + player.stage);
				if (player.stage) {
					if (hasEventListener(Event.ADDED_TO_STAGE)) {
						removeEventListener(Event.ADDED_TO_STAGE, init);
					}
					/*
					if (buttonContainer) {
					var tabbing = new Tabbing(DisplayObjectContainer(buttonContainer));
					}
					*/
					if (buttonContainer) {
						
							SWFFocus.init(buttonContainer.stage);
					}
				}
			} else {
				if (stage) {
					trace("Using local stage.");
					SWFFocus.init(stage);
				}
			}
		}

		private function traceDisplayList(container:DisplayObjectContainer, indentString:String = ""):void {
			var child:DisplayObject;
			for (var i:uint=0; i < container.numChildren; i++) {
				child = container.getChildAt(i);
				if (child.accessibilityProperties) {
					trace(child.accessibilityProperties.name, child.accessibilityProperties.description );
				}
				trace(indentString, child, child.name);


				if (container.getChildAt(i) is DisplayObjectContainer) {
					traceDisplayList(DisplayObjectContainer(child), indentString + "    ");
				}
				
				
				var childClass = getQualifiedClassName(child);
				//adjustment for the closedCaptions BG
				//if (child.name == "instance137" && childClass.indexOf("GlowingButton") != -1) {
					if (childClass.indexOf("GlowingButton") != -1){
						if (getQualifiedClassName(child.parent.parent.parent).indexOf("CaptionControls") != -1){
							trace("******* removed child " + child.name);
							//child.parent.removeChild(child);
							child.alpha = 0;
							
						}
				}

			}
		}

	}
}

2. In Brightcove VideoCloud, select the player you are embedding, click “Edit” and check the option “Enable Javascript/Actionscript API’s”:

Upload the swf to your host, and include a reference in your Brightcove BEML template.


<SWFLoader depth="2" width="10" height="10" source="https://www.yourdomain.com/swf/SWFFocusFix.swf"/>

According to their documentation, it seems the XML node to import the plugin can be placed anywhere. I placed it within the video controls HBox and this is where it worked in my project, see complete BEML src:




<Runtime>
  <Labels>
    <label key="controls play">PLAY button labeled via BEML</label>
    <label key="controls pause">PAUSE button labeled via BEML</label>
  </Labels>
  <Theme name="Deluxe" style="Light"></Theme>
  <Layout boxType="vbox">
    <ChromelessVideoPlayer id="videoPlayer" useOverlayMenu="false">
      <ChromelessControls boxType="vbox" visible="{!videoPlayer.menu.open}" backgroundImage="https://www.yourdomain.com/images/bc_chromelessBG.png">
        <Spacer width="1"/>
        <HBox hAlign="center" height="10">
          <Playhead id="playhead" height="10" mediaController="{videoPlayer}" useTimeToolTip="true" enabled="{videoPlayer.video}"/>
        </HBox>
        <HBox hAlign="center" height="50">
          <HBox id="defaultView" gutter="0" padding="0" vAlign="middle" maxWidth="820">
            <ToggleButton id="playButton" showBack="false" tabIndex="0" width="65" iconName="play" toggledIconName="pause" accessibleName="acc play pause name" tooltip="controls play tooltip" toggledTooltip="controls pause tooltip" click="{videoPlayer.play()}" toggledClick="{videoPlayer.pause()}" toggled="{videoPlayer.playing}" enabled="{videoPlayer.video}"/>
            <Canvas height="50">
              <HBox gutter="4" vAlign="middle" padding="10">
                <Canvas width="40">
                  <Label id="durationLabel" y="-1" style="font-weight:bold;" tabIndex="1" text="{format(videoPlayer.mediaDuration, SecondsTimecodeFormatter)}" vAlign="middle" hAlign="left" alpha=".75"/>
                </Canvas>
              </HBox>
            </Canvas>
            <CaptionControls hoverMenu="true" id="captionsButton" showBack="false" tabIndex="2" width="50" height="50" accessibleName="Closed captions button. Use spacebar to toggle " iconName="cc-on" toggledIconName="cc-off" tooltip="acc cc showcaptions" toggledTooltip="no CC" click="{videoPlayer.enableCaptions(true)}" toggledClick="{videoPlayer.enableCaptions(false)}" toggled="{videoPlayer.captionsEnabled}" includeInLayout="{videoPlayer.captionsAvailable}"/>
            <VolumeControl id="volumeButton" showBack="false" tabIndex="3" width="50" height="50" accessibleName="acc volume name" mediaController="{videoPlayer}" useOverlayLayer="false" iconName="volume" mutedIconName="muted" tooltip="mute tooltip" mutedTooltip="unmute tooltip" useZeroWidth="true" horizontalPadding="10" popupGutter="3" popupHeight="100" sliderHeight="20"/>
            <ToggleButton id="fullscreenButton" showBack="false" tabIndex="4" width="50" height="50" accessibleName="acc fullscreen name" iconName="maximize" toggledIconName="minimize" tooltip="controls maximize tooltip" toggledTooltip="controls minimize tooltip" click="{videoPlayer.goFullScreen(true)}" toggledClick="{videoPlayer.goFullScreen(false)}" toggled="{videoPlayer.fullscreen}"/>
            <SWFLoader depth="2" width="10" height="10" source="https://www.yourdomain.com/swf/SWFFocusFix.swf"/>
          </HBox>
        </HBox>
      </ChromelessControls>
    </ChromelessVideoPlayer>
  </Layout>
</Runtime>



3. Add a crossdomain.xml policy file to the directory where you are hosting the swf.


<cross-domain-policy>
  <allow-access-from domain="*.yourdomain.com"/>
<allow-access-from domain="admin.brightcove.com" />
<allow-access-from domain="sadmin.brightcove.com" />
</cross-domain-policy>

4. Add a reference to the Javascript to your HTML page. Use <script> or ajax, whatever you please, as long as it is loaded by the time the user needs to tab into and out of the Brightcove player. stealFocus(){} is the function you must modify in order to determine where focus should go when the user has tabbed out of your flash application.




/**
* Cross browser tabbing solution/engine for Flash projects
* 
* Released under MIT license:
* http://www.opensource.org/licenses/mit-license.php
* 
* @author Richard England 2011
* @see http://www.richardengland.co.uk
* @version 0.1
*/


var lastAction = ""; //store last "click" selection (useful for toggle states for example - e.g. play/pause - remembers focus)
var index = 0;
var flashObj; // the flash player object
var firstInput; // the first input object
var shifted = false;

function playerReady( obj ){
	flashObj = obj;
	//get the flash object's "top" CSS value
	var topV = $("#flashplayer").offset().top;
	//add a form to the document and places it at same top offset as flash player object
	$("body").prepend('<form name="form1" id="form1" style="position:absolute; top:'+topV+'px; left:-1500px;"></form>');

	//flashObj returning no name for some reason?
	t = thisMovie("flashplayer");
	t.refreshTabbingList();
	
}

doOnce = false;
function stealFocus( shiftKeyDown ){
	//INSERT YOUR JS FUNCTIONS, or bind to window event:
	
	//are we tabbing backwards or forwards?
	if (shiftKeyDown){
		$(window).trigger("VIDEO_SHIFT_TAB");
	}else{
		$(window).trigger("VIDEO_TAB");
	}
	
	if(!doOnce){
		//alert("steal the focus");
		doOnce = true;
	}
	
}

function addFormField( name, accessName, accessDesc, tabIndex, theTop) {
	//trace("tabbing: addFormField");

	var id = document.getElementById("id").value;
	var accesskey = "";
	if(accessDesc == "") accessDesc = accessName;
	if(index == 0) accesskey = " accesskey='q' "; //for first item only - keyboard shortcut
	var field = $("#form1").append("<div class='temp' id='row" + id + "'><label for='txt" + id + "'>" + accessDesc + " <input tabindex='"+tabIndex+"' type='button' size='20' name='" + name + "' id='"+ name + "' value='"+accessName+"' alt='"+accessName+"' title='"+accessName+"' onfocus='sendFocus( this ); ' onclick='doAction( this );' " + accesskey + "></label></div>");
	
	//set the position so scroll will work properly with tabbing
	$("#row" + id).css( 
              { 
			  "position"	: "absolute",
              "top"			: theTop + "px" ,
              "display"		:"block" 
              }
	)
	id = (id - 1) + 2;

	document.getElementById("id").value = id;
	
}

function addTextField( name, accessName, accessDesc, tabIndex, theTop ) {
	//trace("tabbing: addTextField");

	var id = document.getElementById("id").value;
	if(accessDesc == "") accessDesc = accessName;
	$("#form1").append("<div class='temp' id='row" + id + "' style='position:absolute; top:" + theTop + "px; left:0px;'><label for='txt" + id + "'>" + accessDesc + " <input tabindex='"+tabIndex+"' type='text' size='20' name='" + name + "' id='"+ name + "' value='' alt='"+accessName+"' title='"+accessName+"' onfocus='sendFocus( this ); ' onclick='doAction( this );' onkeyup='doTextUpdate(this)' style='position:absolute;' ></div>");

	id = (id - 1) + 2;

	document.getElementById("id").value = id;
	
}

function removeAllFormFields() {
	//trace("tabbing: removeAllFormFields");
	$(".temp").remove();
	
	document.getElementById('form1').innerHTML = '<input type="hidden" id="id" value="1">';
}

function removeFormField(id) {

	$(id).remove();

}


//receives a new tabbing list object/array from Flash and generates the form by looping through the list
function newTabbingList( obj ){
	//trace("tabbing: newTabbingList");
//alert("newTabbingList");
	removeAllFormFields();

	var i = 0;
	var highestTabIndex = 0;
	var isLastActionStillAvailable = false;

	var lowestTabIndex = 10000;
	while(i < obj.length ) {
		if(obj[i].tabIndex < lowestTabIndex) {
			firstInput = obj[i].name;
			lowestTabIndex = obj[i].tabIndex;
		}
		if(lastAction == obj[i].name)  isLastActionStillAvailable = true;
		if(obj[i].isText){
			addTextField(obj[i].name, obj[i].accessibilityName, obj[i].accessibilityDesc, obj[i].tabIndex, obj[i].theY);
		} else {
			addFormField(obj[i].name, obj[i].accessibilityName, obj[i].accessibilityDesc, obj[i].tabIndex, obj[i].theY);
		}
		if(obj[i].tabIndex > highestTabIndex) highestTabIndex = obj[i].tabIndex;
		i++
	}
	
	
	if(lastAction != "" && isLastActionStillAvailable){
		
		$("#" + lastAction).focus();
	} else {
		
		$("#" + firstInput).focus();
	}

	addFormField("blank", "blank",  "blank", highestTabIndex + 1, 0);
	
	
	index = i;
}

function doPlayFocus(){
	//trace("tabbing: doPlayFocus");
	t = thisMovie("flashplayer");
	t.setFocusTo("playButton", false);
}

//sends text input back to text input in Flash object
function doTextUpdate( inputFocus ){
	
	id = inputFocus.id;
	t = thisMovie("flashplayer");
	t.sendTextUpdate (id , {txt: inputFocus.value});
	lastAction = id;
	
	//alert("doTextUpdate " + inputFocus.value);
}


function doAction( inputFocus ){
	//trace("tabbing: doAction");	
	id = inputFocus.id;
	t = thisMovie("flashplayer");
	t.sendTabAction (id , false);
	lastAction = id;
	
	//alert("doAction " + inputFocus.id);
	//$("#" + lastAction).focus();
}

function thisMovie(movieName) {
	 if (navigator.appName.indexOf("Microsoft") != -1) {
		 return window[movieName];
	 } else {
		 return document.getElementById(movieName);
	 }
 }
 
function sendFocus( inputFocus ) {
	//trace("tabbing: sendFocus");	
	id = inputFocus.id;
	t = thisMovie("flashplayer");
	if(id == "blank") {
		//fix for IE focus
		$("#" + firstInput).focus();
	} else {
		t.setFocusTo(id, false);
	}
	
	
}



Leave a Reply

Required fields are marked *