diff --git a/src/as/com/threerings/flash/video/FlvVideoPlayer.as b/src/as/com/threerings/flash/video/FlvVideoPlayer.as new file mode 100644 index 00000000..70df38c0 --- /dev/null +++ b/src/as/com/threerings/flash/video/FlvVideoPlayer.as @@ -0,0 +1,268 @@ +// +// $Id$ + +package com.threerings.flash.video { + +import flash.display.DisplayObject; + +import flash.events.AsyncErrorEvent; +import flash.events.IOErrorEvent; +import flash.events.ErrorEvent; +import flash.events.Event; +import flash.events.EventDispatcher; +import flash.events.NetStatusEvent; +import flash.events.SecurityErrorEvent; +import flash.events.TimerEvent; + +import flash.geom.Point; + +import flash.media.Video; + +import flash.net.NetConnection; +import flash.net.NetStream; + +import flash.utils.Timer; + +import com.threerings.util.Log; +import com.threerings.util.ValueEvent; + +public class FlvVideoPlayer extends EventDispatcher + implements VideoPlayer +{ + public function FlvVideoPlayer () + { + _videoChecker = new Timer(100); + _videoChecker.addEventListener(TimerEvent.TIMER, handleVideoCheck); + } + + /** + * Start playing a video! + */ + public function load (url :String) :void + { + _netCon = new NetConnection(); + _netCon.addEventListener(NetStatusEvent.NET_STATUS, handleNetStatus); + + // error handlers + _netCon.addEventListener(AsyncErrorEvent.ASYNC_ERROR, handleAsyncError); + _netCon.addEventListener(IOErrorEvent.IO_ERROR, handleIOError); + _netCon.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError); + + _netCon.connect(null); + _netStream = new NetStream(_netCon); + // pass in refs to some of our protected methods + _netStream.client = { + onMetaData: handleMetaData + }; + _netStream.addEventListener(NetStatusEvent.NET_STATUS, handleStreamNetStatus); + + _vid.attachNetStream(_netStream); + _videoChecker.start(); + + _netStream.play(url); + _netStream.pause(); // TODO Does this work? + updateState(VideoPlayerCodes.STATE_READY); + } + + // from VideoPlayer + public function getDisplay () :DisplayObject + { + return _vid; + } + + // from VideoPlayer + public function getState () :int + { + return _state; + } + + // from VideoPlayer + public function getSize () :Point + { + return _size.clone(); + } + + // from VideoPlayer + public function play () :void + { + if (_netStream != null) { + _netStream.resume(); + updateState(VideoPlayerCodes.STATE_PLAYING); + } + // TODO: throw an error if _netStream == null?? + } + + // from VideoPlayer + public function pause () :void + { + if (_netStream != null) { + _netStream.pause(); + updateState(VideoPlayerCodes.STATE_PAUSED); + } + } + + // from VideoPlayer + public function getDuration () :Number + { + return NaN; // TODO + } + + // from VideoPlayer + public function getPosition () :Number + { + return NaN; // TODO + } + + // from VideoPlayer + public function seek (position :Number) :void + { + // TODO + } + + // from VideoPlayer + public function getVolume () :Number + { + return 1; // TODO + } + + // from VideoPlayer + public function setVolume (volume :Number) :void + { + // TODO + } + + // from VideoPlayer + public function unload () :void + { + _videoChecker.reset(); + _vid.attachNetStream(null); + + if (_netStream != null) { + _netStream.close(); + _netStream.removeEventListener(NetStatusEvent.NET_STATUS, handleStreamNetStatus); + _netStream = null; + } + if (_netCon != null) { + _netCon.close(); + _netCon.removeEventListener(NetStatusEvent.NET_STATUS, handleNetStatus); + _netCon.removeEventListener(AsyncErrorEvent.ASYNC_ERROR, handleAsyncError); + _netCon.removeEventListener(IOErrorEvent.IO_ERROR, handleIOError); + _netCon.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError); + _netCon = null; + } + } + + /** + * Check to see if we now know the dimensions of the video. + */ + protected function handleVideoCheck (event :TimerEvent) :void + { + if (_vid.videoWidth == 0 || _vid.videoHeight == 0) { + return; // not known yet! + } + + // stop the checker timer + _videoChecker.stop(); + _size = new Point(_vid.videoWidth, _vid.videoHeight); + + // set up the width/height + _vid.width = _size.x; + _vid.height = _size.y; + + log.info("=================> size known: " + _size); + dispatchEvent(new ValueEvent(VideoPlayerCodes.SIZE, _size.clone())); + } + + protected function handleNetStatus (event :NetStatusEvent) :void + { + var info :Object = event.info; + if ("error" == info.level) { + log.warning("NetStatus error: " + info.code); + dispatchEvent(new ValueEvent(VideoPlayerCodes.ERROR, info.code)); + return; + } + // else info.level == "status" + switch (info.code) { + case "NetConnection.Connect.Success": + case "NetConnection.Connect.Closed": + // these status events we ignore + break; + + default: + log.info("NetStatus status: " + info.code); + break; + } + } + + protected function handleStreamNetStatus (event :NetStatusEvent) :void + { + if (event.info.code == "NetStream.Play.Stop") { + _netStream.seek(0); + _netStream.pause(); + updateState(VideoPlayerCodes.STATE_PAUSED); + } + } + + protected function handleAsyncError (event :AsyncErrorEvent) :void + { + redispatchError(event); + } + + protected function handleIOError (event :IOErrorEvent) :void + { + redispatchError(event); + } + + protected function handleSecurityError (event :SecurityErrorEvent) :void + { + redispatchError(event); + } + + /** + * Redispatch some error we received to our listeners. + */ + protected function redispatchError (event :ErrorEvent) :void + { + log.warning("got video error: " + event); + dispatchEvent(new ValueEvent(VideoPlayerCodes.ERROR, event.text)); + } + + /** + * Called when metadata (if any) is found in the video stream. + */ + protected function handleMetaData (info :Object) :void + { + log.info("Got video metadata:"); + for (var n :String in info) { + log.info(" " + n + ": " + info[n]); + } + + // TODO: total duration is info.duration + } + + protected function updateState (newState :int) :void + { + _state = newState; + dispatchEvent(new ValueEvent(VideoPlayerCodes.STATE, newState)); + } + + protected const log :Log = Log.getLog(this); + + protected var _vid :Video = new Video(); + + protected var _netCon :NetConnection; + + protected var _netStream :NetStream; + + protected var _state :int = VideoPlayerCodes.STATE_UNREADY; + + /** Our size, null until known. */ + protected var _size :Point; + + /** Checks the video every 100ms to see if the dimensions are now know. + * Yes, this is how to do it. We could trigger on ENTER_FRAME, but then + * we may not know the dimensions unless we're added on the display list, + * and we want this to work in the general case. */ + protected var _videoChecker :Timer; +} +} diff --git a/src/as/com/threerings/flash/video/SimpleVideoDisplay.as b/src/as/com/threerings/flash/video/SimpleVideoDisplay.as new file mode 100644 index 00000000..0881075b --- /dev/null +++ b/src/as/com/threerings/flash/video/SimpleVideoDisplay.as @@ -0,0 +1,203 @@ +// +// $Id# + +package com.threerings.flash.video { + +import flash.display.DisplayObject; +import flash.display.Graphics; +import flash.display.Shape; +import flash.display.Sprite; + +import flash.events.MouseEvent; + +import flash.geom.Point; + +import com.threerings.util.Log; +import com.threerings.util.ValueEvent; + +/** + * + * @eventType com.threerings.flash.video.VideoPlayerCodes.SIZE + */ +[Event(name="size", type="com.threerings.util.ValueEvent")] + +/** + * A simple video display. + */ +public class SimpleVideoDisplay extends Sprite +{ + public static const NATIVE_WIDTH :int = 320; + + public static const NATIVE_HEIGHT :int = 240; + + /** + * Create a video displayer. + */ + public function SimpleVideoDisplay (player :VideoPlayer) + { + _player = player; + _player.addEventListener(VideoPlayerCodes.STATE, handlePlayerState); + _player.addEventListener(VideoPlayerCodes.SIZE, handlePlayerSize); + _player.addEventListener(VideoPlayerCodes.ERROR, handlePlayerError); + + addChild(_player.getDisplay()); + + _mask = new Shape(); + this.mask = _mask; + addChild(_mask); + + addEventListener(MouseEvent.ROLL_OVER, handleRollOver); + addEventListener(MouseEvent.ROLL_OUT, handleRollOut); + + _hud = new Sprite(); + _hud.addEventListener(MouseEvent.CLICK, handleClick); + redrawHUD(); + + // create the mask... + var g :Graphics = _mask.graphics; + g.clear(); + g.beginFill(0xFFFFFF); + g.drawRect(0, 0, NATIVE_WIDTH, NATIVE_HEIGHT); + g.endFill(); + + g = this.graphics; + g.beginFill(0xFF000000); + g.drawRect(0, 0, NATIVE_WIDTH, NATIVE_HEIGHT); + g.endFill(); + + // and update the HUD location (even if not currently showing) + _hud.x = NATIVE_WIDTH/2; + _hud.y = NATIVE_HEIGHT/2; + } + + /** + * Stop playing our video. + */ + public function unload () :void + { + _player.unload(); + } + + protected function handleRollOver (event :MouseEvent) :void + { + showHUD(true); + } + + protected function handleRollOut (event :MouseEvent) :void + { + showHUD(false); + } + + protected function handleClick (event :MouseEvent) :void + { + // the buck stops here! + event.stopImmediatePropagation(); + + switch (_player.getState()) { + case VideoPlayerCodes.STATE_READY: + case VideoPlayerCodes.STATE_PAUSED: + _player.play(); + break; + + case VideoPlayerCodes.STATE_PLAYING: + _player.pause(); + break; + + default: + log.info("Click while player in unhandled state: " + _player.getState()); + break; + } + } + + protected function handlePlayerState (event :ValueEvent) :void + { + redrawHUD(); + } + + protected function handlePlayerSize (event :ValueEvent) :void + { + const size :Point = Point(event.value); + + const disp :DisplayObject = _player.getDisplay(); + // TODO: siggggghhhhh +// disp.scaleX = NATIVE_WIDTH / size.x; +// disp.scaleY = NATIVE_HEIGHT / size.y; + disp.width = NATIVE_WIDTH; + disp.height = NATIVE_HEIGHT; + + // and, we redispatch + dispatchEvent(event); + } + + protected function handlePlayerError (event :ValueEvent) :void + { + // TODO.. maybe just redispatch + log.warning("player error: " + event.value); + } + + protected function showHUD (show :Boolean) :void + { + if (show == (_hud.parent == null)) { + if (show) { + addChild(_hud); + } else { + removeChild(_hud); + } + } + } + + protected function redrawHUD () :void + { + const state :int = _player.getState(); + + const g :Graphics = _hud.graphics; + g.clear(); + + if (state == VideoPlayerCodes.STATE_READY || + state == VideoPlayerCodes.STATE_PLAYING || + state == VideoPlayerCodes.STATE_PAUSED) { + // draw a nice circle + g.beginFill(0x333333); + g.drawCircle(0, 0, 20); + g.endFill(); + g.lineStyle(2, 0xFFFFFF); + g.drawCircle(0, 0, 20); + + if (state == VideoPlayerCodes.STATE_PLAYING) { + g.lineStyle(2, 0x00FF00); + g.moveTo(-4, -10); + g.lineTo(-4, 10); + g.moveTo(4, -10); + g.lineTo(4, 10); + + } else { + g.lineStyle(0, 0, 0); + g.beginFill(0x00FF00); + g.moveTo(-4, -10); + g.lineTo(4, 0); + g.lineTo(-4, 10); + g.lineTo(-4, -10); + g.endFill(); + } + + } else { + // draw something loading-like + g.beginFill(0x000033); + g.drawCircle(5, 5, 10); + g.drawCircle(-5, 5, 10); + g.drawCircle(-5, -5, 10); + g.drawCircle(5, -5, 10); + g.endFill(); + } + } + + protected const log :Log = Log.getLog(this); + + protected var _player :VideoPlayer; + + protected var _hud :Sprite; + + /** Our mask, also defines our boundaries for clicking. */ + protected var _mask :Shape; +} +} diff --git a/src/as/com/threerings/flash/video/VideoPlayer.as b/src/as/com/threerings/flash/video/VideoPlayer.as new file mode 100644 index 00000000..1c0b59ce --- /dev/null +++ b/src/as/com/threerings/flash/video/VideoPlayer.as @@ -0,0 +1,98 @@ +// +// $Id$ + +package com.threerings.flash.video { + +import flash.display.DisplayObject; + +import flash.events.IEventDispatcher; + +import flash.geom.Point; + +import com.threerings.util.ValueEvent; + +/** + * + * @eventType com.threerings.flash.video.VideoPlayerCodes.STATE + */ +[Event(name="state", type="com.threerings.util.ValueEvent")] + +/** + * + * @eventType com.threerings.flash.video.VideoPlayerCodes.DURATION + */ +[Event(name="duration", type="com.threerings.util.ValueEvent")] + +/** + * + * @eventType com.threerings.flash.video.VideoPlayerCodes.POSITION + */ +[Event(name="position", type="com.threerings.util.ValueEvent")] + +/** + * + * @eventType com.threerings.flash.video.VideoPlayerCodes.SIZE + */ +[Event(name="size", type="com.threerings.util.ValueEvent")] + +/** + * + * @eventType com.threerings.flash.video.VideoPlayerCodes.ERROR + */ +[Event(name="error", type="com.threerings.util.ValueEvent")] + +/** + * Implemented by video playing backends. + */ +public interface VideoPlayer extends IEventDispatcher +{ + /** + * Get the actual visualization of the video. + */ + function getDisplay () :DisplayObject; + + /** + * @return a VideoPlayerCodes state constant. + */ + function getState () :int; + + /** + * Get the size of the video, or null if not yet known. + */ + function getSize () :Point; + + function play () :void; + + function pause () :void; + + /** + * Get the duration of the video, or NaN if not yet known. + */ + function getDuration () :Number; + + /** + * Get the position of the video, or NaN if not yet ready. + */ + function getPosition () :Number; + + /** + * Seek to the specified position. + */ + function seek (position :Number) :void; + + /** + * Set the volume, between 0-1. + */ + function setVolume (volume :Number) :void; + + /** + * Get the volume, from 0 to 1. + */ + function getVolume () :Number; + + /** + * Unload the video. + */ + function unload () :void; +} +} diff --git a/src/as/com/threerings/flash/video/VideoPlayerCodes.as b/src/as/com/threerings/flash/video/VideoPlayerCodes.as new file mode 100644 index 00000000..7aa35247 --- /dev/null +++ b/src/as/com/threerings/flash/video/VideoPlayerCodes.as @@ -0,0 +1,28 @@ +// +// $Id$ + +package com.threerings.flash.video { + +public class VideoPlayerCodes +{ + public static const STATE :String = "state"; + + public static const DURATION :String = "duration"; + + public static const POSITION :String = "position"; + + public static const SIZE :String = "size"; + + public static const ERROR :String = "error"; + + /** Indicates we're still loading or initializing. */ + public static const STATE_UNREADY :int = 0; + + /** Indicates we're ready to play. */ + public static const STATE_READY :int = 1; + + public static const STATE_PLAYING :int = 2; + + public static const STATE_PAUSED :int = 3; +} +}