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; } } }