package com.almerblank.flex.components.video
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.events.AsyncErrorEvent;
import flash.events.Event;
import flash.events.NetStatusEvent;
import flash.events.ProgressEvent;
import flash.events.SecurityErrorEvent;
import flash.events.TimerEvent;
import flash.geom.Rectangle;
import flash.media.SoundTransform;
import flash.media.Video;
import flash.net.NetConnection;
import flash.net.NetStream;
import flash.net.ObjectEncoding;
import flash.utils.Timer;
import mx.containers.Canvas;
import mx.controls.Image;
import mx.events.FlexEvent;
import mx.events.VideoEvent;
import mx.utils.ObjectUtil;
/**
* The VideoBitmapDisplay class works almost identically to the VideoDisplay class to
* display video. It provides extra functionality and extendability that the VideoDisplay
* class does not.
* @author Omar Gonzalez
*/
[Bindable]
public class VideoBitmapDisplay extends Canvas
{
// VideoBitmapDisplay properties
private var _videoHeight:int = 240;
private var _videoWidth:int = 320;
private var _playing:Boolean = false;
private var _maintainAspectRatio:Boolean = false;
private var _scaleContent:Boolean = true;
private var _smoothing:Boolean = true;
private var _volume:Number = .75;
private var _playheadUpdateInterval:int = 50;
private var _source:String = null;
private var _autoPlay:Boolean = true;
private var _autoRewind:Boolean = true;
private var _autoBandwidthDetection:Boolean = false;
private var _bufferTime:Number = 3;
private var _idleTimeout:Number = 300000;
private var _cuePointManagerClass:Class = null;
private var _cuePoints:Array = null;
private var _live:Boolean = false;
private var _playheadTime:Number;
private var _progressInterval:int = 250;
private var _totalTime:Number;
private var _backgroundColor:Number = 0x000000;
private var _clearEndScreen:Boolean = true;
private var _playheadManager:Timer;
private var _progressManager:Timer;
private var _bytesLoaded:Number;
private var _bytesTotal:Number;
private var _bufferEmpty:Boolean = true;
private var _downloadComplete:Boolean = false;
private var _sourceChanged:Boolean = false;
// video objects
private var _netConn:NetConnection = null;
private var _connected:Boolean = false;
private var _videoStream:NetStream = null;
private var _video:Video;
private var _sound:SoundTransform;
private var _refreshManager:Timer;
private var _streamName:String;
// display objects
private var _display:Image;
public var bitmapData:BitmapData;
private var _videoBitmap:Bitmap;
/**
* Class constructor.
*/
public function VideoBitmapDisplay()
{
super();
super.addEventListener ( FlexEvent.CREATION_COMPLETE, init );
}
/**
* Retrieves the totalTime from the metadata in the video
* when it is received.
*
* @param info
*
*/
public function onMetaData(info:Object):void
{
//trace("metadata: duration=" + info.duration + " width=" + info.width + " height=" + info.height + " framerate=" + info.framerate);
totalTime = Number ( info.duration );
}
/**
* Handles cuePoint info from the video.
*
* @param info
*
*/
public function onCuePoint(info:Object):void
{
trace("cuepoint: time=" + info.time + " name=" + info.name + " type=" + info.type);
}
/**
* Handles AsyncErrorEvent to prevent rte.
*
* @param e
*
*/
private function asyncErrorHandler(e:AsyncErrorEvent):void
{
return;
}
/**
* The connStatus()
method handles status
* events dispatched by the NetConnection object.
*
* @param event
*
*/
private function connStatus (event:NetStatusEvent):void
{
switch ( event.info.code )
{
case 'NetConnection.Connect.Success' :
_connected = true;
if (!live )
{
openStream();
}
else if ( live )
{
subscribe(); // this custom method is used to subscribe to the FMS application.
}
break;
case 'NetConnection.Connect.Failed' :
case 'NetConnection.Connect.Rejected' :
openConnection();
break;
case 'NetConnection.Connect.Closed' :
_connected = false;
default : break ;
}
}
/**
* This method is used to subscribe to a specific CDN's FMS server.
* This could/should change for your specific implementations of
* live video. The onFCSSubscribe()
method is also
* an expected callback of this CDN to complete the connection.
*
*/
private function subscribe():void
{
//var res:Responder = new Responder( onFCSubscribe );
_netConn.call( "FCSubscribe", null, _streamName );
}
/**
* Callback method called by the server on the client of the
* NetConnection object. This method is specific to the CDN
* this class was implemented for. You could change this for
* your specific implementation.
*
* @param info
*
*/
public function onFCSubscribe ( info:Object ):void
{
openStream();
}
private function securityErrorHandler(event:SecurityErrorEvent):void
{
trace("securityErrorHandler: " + event);
}
/**
* This method opens the NetConnection object, has
* some specific logic for connection to FMS2, which
* uses AMF0 to communicate.
*
*/
private function openConnection():void
{
_netConn = new NetConnection();
_netConn.addEventListener( NetStatusEvent.NET_STATUS, connStatus );
_netConn.addEventListener( SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler );
if ( !live )
{
_netConn.connect( null );
}
else if ( live )
{
_netConn.client = this;
_netConn.objectEncoding = ObjectEncoding.AMF0; // used to connect to FMS2
_netConn.connect( source );
}
}
/**
* This method opens the NetStream object, and
* sets up the sound and video display. It also
* autoplays the video, if autoPlay is true.
*/
private function openStream():void
{
_videoStream = new NetStream( _netConn );
_videoStream.client = this;
_videoStream.bufferTime = _bufferTime;
_videoStream.addEventListener( NetStatusEvent.NET_STATUS, streamStatus );
_videoStream.addEventListener( AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler );
_video = new Video( _videoWidth, _videoHeight );
_video.smoothing = _smoothing;
_video.width = _videoWidth;
_video.height = _videoHeight;
_video.attachNetStream( _videoStream );
updateSound();
createVideoDisplay();
// dispatch video ready event.
var e:VideoEvent = new VideoEvent ( VideoEvent.READY, true );
dispatchEvent( e );
if ( _autoPlay )
{
play();
}
}
/**
* Updates the sound on the video stream.
*/
private function updateSound():void
{
var s:SoundTransform = new SoundTransform();
s.volume = _volume;
_videoStream.soundTransform = s;
}
/**
* Creates the video display, using an Image object.
*
*/
private function createVideoDisplay():void
{
_display = new Image();
_display.width = _videoWidth;
_display.height = _videoHeight;
_display.setStyle('backgroundColor', 0x000000);
_display.setStyle('backgroundAlpha', 1);
// Create the BitmapData
bitmapData = new BitmapData( _videoWidth, _videoHeight, false, 0x000000 );
// Create the Bitmap display object
_videoBitmap = new Bitmap( bitmapData );
_videoBitmap.width = _videoWidth;
_videoBitmap.height = _videoHeight;
// attach bitmap to image display
_display.source = _videoBitmap;
// add to stage
addChild ( _display );
}
/**
* Handles the creationComplete event for this component.
*
* @param event
*
*/
private function init ( event:FlexEvent ):void
{
setStyle('backgroundColor', backgroundColor);
_playheadManager = new Timer ( _playheadUpdateInterval );
_playheadManager.addEventListener( TimerEvent.TIMER, updatePlayhead );
_progressManager = new Timer ( _progressInterval );
_progressManager.addEventListener( TimerEvent.TIMER, updateProgress );
openConnection();
}
/**
* Updates the playback progress and dispatches a ProgressEvent.PROGRESS
* event.
*
* @param event
*
*/
private function updateProgress ( event:TimerEvent ):void
{
_bytesLoaded = _videoStream.bytesLoaded;
_bytesTotal = _videoStream.bytesTotal;
var e:ProgressEvent = new ProgressEvent ( ProgressEvent.PROGRESS, true );
e.bytesLoaded = _videoStream.bytesLoaded;
e.bytesTotal = _videoStream.bytesTotal;
dispatchEvent( e );
if ( e.bytesLoaded == e.bytesTotal )
{
_progressManager.stop();
_downloadComplete = true;
}
}
/**
* Updates the _playheadTime and dispatches a
* VideoEvent.PLAYHEAD_UPDATE event.
*
* @param event
*
*/
private function updatePlayhead ( event:TimerEvent ):void
{
//trace('updating playhead...' );
_playheadTime = _videoStream.time;
var e:VideoEvent = new VideoEvent ( VideoEvent.PLAYHEAD_UPDATE, true );
e.playheadTime = _videoStream.time;
dispatchEvent( e );
}
/**
* Handles the stream status events. Custom logic for
* end of stream can be added in the NetStream.Buffer.Empty
* switch case.
*
* @param event
*
*/
private function streamStatus (event:NetStatusEvent):void
{
switch ( event.info.code )
{
case 'NetStream.Play.Start' :
_refreshManager = new Timer ( _playheadUpdateInterval );
_refreshManager.addEventListener( TimerEvent.TIMER, updateDisplay );
_refreshManager.start();
_playing = true;
break;
case 'NetStream.Buffer.Full' :
_bufferEmpty = false;
break;
case 'NetStream.Buffer.Empty' :
_bufferEmpty = true;
if ( _videoStream.bufferLength < .1 && ((Math.floor( _totalTime ) - Math.floor( _playheadTime )) <= 1) )
{
//trace("video ended...");
playing = false;
if ( _autoRewind )
_videoStream.seek( 0 );
if ( _clearEndScreen )
clearVideo();
// dispatch videoComplete event.
var e:VideoEvent = new VideoEvent ( VideoEvent.COMPLETE, true );
e.playheadTime = _videoStream.time;
dispatchEvent( e );
}
break;
case 'NetStream.Buffer.Flush' :
break;
default : break ;
}
}
/**
* Updates the display using the BitmapData.draw() method.
*
* @param e
*
*/
private function updateDisplay( e:Event ):void
{
if ( _video && _playing )
{
_video.attachNetStream( null );
bitmapData.draw( _video );
_video.attachNetStream( _videoStream );
}
}
// PUBLIC METHODS
/**
* Stops playback of video.
*/
public function stop():void
{
playing = false;
_videoStream.seek( 0 );
}
/**
* Closes the video stream, stopping the
* download if not complete.
*/
public function close():void
{
playing = false;
if (_videoStream)
{
_videoStream.close();
}
}
/**
* Pauses playback of video.
*/
public function pause():void
{
playing = false;
}
/**
* Plays the video loaded.
*/
public function play():void
{
if ( _videoStream )
{
if ( _videoStream.time > 0 && !_bufferEmpty )
{
playing = true;
}
else if ( _videoStream.time == 0 )
{
if ( _sourceChanged )
{
if ( !live )
{
_videoStream.play( _source );
_playheadManager.start();
_progressManager.start();
_sourceChanged = false;
}
else if ( live )
{
trace( ' source changed for live ' );
_videoStream.play( _streamName );
_playheadManager.start();
_progressManager.start();
_sourceChanged = false;
}
}
else if ( _downloadComplete && !live )
{
playing = true;
}
}
}
}
/**
* Clears the video left at the end of the video
* playback with a black screen.
*/
public function clearVideo():void
{
trace('clearing video...');
var rect:Rectangle = new Rectangle ( 0, 0, _videoWidth, _videoHeight );
bitmapData.fillRect( rect, 0x00000000 );
}
// GETTERS / SETTERS
/**
* The playing property can be used to toggle playback.
* It starts and stops the bitmap drawing to the screen when
* the video is not playing.
*
* @param isPlaying
*
*/
public function set playing ( isPlaying:Boolean ):void
{
_playing = isPlaying;
if ( _playing && _videoStream )
{
_videoStream.resume();
_playheadManager.start();
}
else if ( !_playing && _videoStream )
{
_videoStream.pause();
_playheadManager.stop();
}
//isPlaying ? _videoStream.play() :_videoStream.pause();
if ( _playing && _refreshManager )
{
_refreshManager.start()
}
else if ( _refreshManager )
{
_refreshManager.stop();
}
}
public function get playing ():Boolean
{
return _playing;
}
public function set bufferTime( time:Number ):void
{
_bufferTime = time;
if ( _videoStream )
_videoStream.bufferTime = time;
}
public function get bufferTime():Number
{
return _bufferTime;
}
public function get volume():Number
{
return _volume;
}
public function set volume( value:Number ):void
{
_volume = value;
if ( _videoStream )
updateSound();
}
public function set totalTime(time:Number):void
{
_totalTime = time;
}
public function get totalTime():Number
{
return _totalTime;
}
public function set backgroundColor ( color:Number ):void
{
_backgroundColor = color;
}
public function get backgroundColor():Number
{
return _backgroundColor;
}
public function set source ( value:String ):void
{
if ( _videoStream && playing )
{
playing = false;
_videoStream.close();
}
_source = value;
_sourceChanged = true;
if ( _videoStream )
{
openConnection();
}
getStreamName();
if ( _videoStream && _autoPlay )
play();
}
private function getStreamName():void
{
if ( source.search( "/" ) > -1 )
{
var urlParts:Array = source.split("/");
_streamName = urlParts[urlParts.length - 1]; // grabs last bit of rtmp:// url, from end of string to first "/"
// trace('_streamName = ' + _streamName );
}
}
public function get source():String
{
return _source;
}
public function set autoPlay( play:Boolean ):void
{
_autoPlay = play;
}
public function get autoPlay():Boolean
{
return _autoPlay;
}
public function set playheadTime( time:Number ):void
{
_playheadTime = time;
_videoStream.seek( time );
}
public function get playheadTime():Number
{
if ( _videoStream )
{
return _videoStream.time;
}
else return 0;
}
public function get bytesLoaded():Number
{
return _bytesLoaded;
}
public function get bytesTotal():Number
{
return _bytesTotal;
}
public function get playheadUpdateInterval():Number
{
return _playheadUpdateInterval;
}
public function get progressInterval():Number
{
return _progressInterval;
}
public function set playheadUpdateInterval( value:Number ):void
{
_playheadUpdateInterval = value;
}
public function set progressInterval( value:Number ):void
{
_progressInterval = value;
}
public function set live(on:Boolean):void
{
_live = on;
}
public function get live():Boolean
{
return _live;
}
/**
* The set width setter is overridden to update the
* _videoWidth property.
*
* @param value
*
*/
override public function set width(value:Number):void
{
super.width = value;
_videoWidth = value;
}
/**
* The set height setter is overridden to update the
* _videoHeight property.
*
* @param value
*
*/
override public function set height(value:Number):void
{
super.height = value;
_videoHeight = value;
}
}
}