diff --git a/src/as/com/threerings/flash/video/FlvVideoPlayer.as b/src/as/com/threerings/flash/video/FlvVideoPlayer.as index 70df38c0..d0fe3a4f 100644 --- a/src/as/com/threerings/flash/video/FlvVideoPlayer.as +++ b/src/as/com/threerings/flash/video/FlvVideoPlayer.as @@ -16,6 +16,7 @@ import flash.events.TimerEvent; import flash.geom.Point; +import flash.media.SoundTransform; import flash.media.Video; import flash.net.NetConnection; @@ -31,8 +32,11 @@ public class FlvVideoPlayer extends EventDispatcher { public function FlvVideoPlayer () { - _videoChecker = new Timer(100); - _videoChecker.addEventListener(TimerEvent.TIMER, handleVideoCheck); + _sizeChecker = new Timer(100); + _sizeChecker.addEventListener(TimerEvent.TIMER, handleSizeCheck); + + _positionChecker = new Timer(250); + _positionChecker.addEventListener(TimerEvent.TIMER, handlePositionCheck); } /** @@ -54,10 +58,11 @@ public class FlvVideoPlayer extends EventDispatcher _netStream.client = { onMetaData: handleMetaData }; + _netStream.soundTransform = new SoundTransform(_volume); _netStream.addEventListener(NetStatusEvent.NET_STATUS, handleStreamNetStatus); _vid.attachNetStream(_netStream); - _videoChecker.start(); + _sizeChecker.start(); _netStream.play(url); _netStream.pause(); // TODO Does this work? @@ -104,37 +109,45 @@ public class FlvVideoPlayer extends EventDispatcher // from VideoPlayer public function getDuration () :Number { - return NaN; // TODO + return _duration; } // from VideoPlayer public function getPosition () :Number { - return NaN; // TODO + if (_netStream != null) { + return _netStream.time; + } + return NaN; } // from VideoPlayer public function seek (position :Number) :void { - // TODO + if (_netStream != null) { + _netStream.seek(position); + } } // from VideoPlayer public function getVolume () :Number { - return 1; // TODO + return _volume; } // from VideoPlayer public function setVolume (volume :Number) :void { - // TODO + _volume = Math.max(0, Math.min(1, volume)); + if (_netStream != null) { + _netStream.soundTransform = new SoundTransform(_volume); + } } // from VideoPlayer public function unload () :void { - _videoChecker.reset(); + _sizeChecker.reset(); _vid.attachNetStream(null); if (_netStream != null) { @@ -155,24 +168,29 @@ public class FlvVideoPlayer extends EventDispatcher /** * Check to see if we now know the dimensions of the video. */ - protected function handleVideoCheck (event :TimerEvent) :void + protected function handleSizeCheck (event :TimerEvent) :void { if (_vid.videoWidth == 0 || _vid.videoHeight == 0) { return; // not known yet! } // stop the checker timer - _videoChecker.stop(); + _sizeChecker.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); + log.debug("====> size known: " + _size); dispatchEvent(new ValueEvent(VideoPlayerCodes.SIZE, _size.clone())); } + protected function handlePositionCheck (event :TimerEvent) :void + { + dispatchEvent(new ValueEvent(VideoPlayerCodes.POSITION, getPosition())); + } + protected function handleNetStatus (event :NetStatusEvent) :void { var info :Object = event.info; @@ -196,10 +214,19 @@ public class FlvVideoPlayer extends EventDispatcher protected function handleStreamNetStatus (event :NetStatusEvent) :void { - if (event.info.code == "NetStream.Play.Stop") { + log.debug("NetStatus", "level", event.info.level, "code", event.info.code); + + switch (event.info.code) { + case "NetStream.Play.Stop": + // rewind to the beginning _netStream.seek(0); _netStream.pause(); updateState(VideoPlayerCodes.STATE_PAUSED); + break; + + case "NetStream.Seek.Notify": + handlePositionCheck(null); + break; } } @@ -238,12 +265,31 @@ public class FlvVideoPlayer extends EventDispatcher } // TODO: total duration is info.duration + + if ("duration" in info) { + _duration = Number(info.duration); + if (!isNaN(_duration)) { + dispatchEvent(new ValueEvent(VideoPlayerCodes.DURATION, _duration)); + } + } } protected function updateState (newState :int) :void { + const oldState :int = _state; + _state = newState; dispatchEvent(new ValueEvent(VideoPlayerCodes.STATE, newState)); + + if (_state == VideoPlayerCodes.STATE_PLAYING) { + _positionChecker.start(); + + } else { + _positionChecker.reset(); + if (oldState == VideoPlayerCodes.STATE_PLAYING) { + handlePositionCheck(null); + } + } } protected const log :Log = Log.getLog(this); @@ -259,10 +305,16 @@ public class FlvVideoPlayer extends EventDispatcher /** Our size, null until known. */ protected var _size :Point; + protected var _duration :Number = NaN; + + protected var _volume :Number = 1; + /** 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; + protected var _sizeChecker :Timer; + + protected var _positionChecker :Timer; } } diff --git a/src/as/com/threerings/flash/video/SimpleVideoDisplay.as b/src/as/com/threerings/flash/video/SimpleVideoDisplay.as index 0881075b..259950b6 100644 --- a/src/as/com/threerings/flash/video/SimpleVideoDisplay.as +++ b/src/as/com/threerings/flash/video/SimpleVideoDisplay.as @@ -38,6 +38,8 @@ public class SimpleVideoDisplay extends Sprite _player = player; _player.addEventListener(VideoPlayerCodes.STATE, handlePlayerState); _player.addEventListener(VideoPlayerCodes.SIZE, handlePlayerSize); + _player.addEventListener(VideoPlayerCodes.DURATION, handlePlayerDuration); + _player.addEventListener(VideoPlayerCodes.POSITION, handlePlayerPosition); _player.addEventListener(VideoPlayerCodes.ERROR, handlePlayerError); addChild(_player.getDisplay()); @@ -68,6 +70,36 @@ public class SimpleVideoDisplay extends Sprite // and update the HUD location (even if not currently showing) _hud.x = NATIVE_WIDTH/2; _hud.y = NATIVE_HEIGHT/2; + + _track = new Sprite(); + _track.x = PAD - _hud.x; + _track.y = NATIVE_HEIGHT - PAD - TRACK_HEIGHT - _hud.y; + g = _track.graphics; + g.beginFill(0x000000, .7); + g.lineStyle(1, 0xFFFFFF); + g.drawRect(0, 0, TRACK_WIDTH, TRACK_HEIGHT); + g.endFill(); + //g.drawRect(0, 0, TRACK_WIDTH - 2, TRACK_HEIGHT - 2); + // _track is not added to _hud until we know the duration + + const trackMask :Shape = new Shape(); + g = trackMask.graphics; + g.beginFill(0xFFFFFF); + g.drawRect(0, 0, TRACK_WIDTH, TRACK_HEIGHT); + _track.addChild(trackMask); + _track.mask = trackMask; + + _knob = new Sprite(); + _knob.y = TRACK_HEIGHT / 2; + g = _knob.graphics; + g.lineStyle(1, 0xFFFFFF); + g.beginFill(0x000099); + g.drawCircle(0, 0, TRACK_HEIGHT/2 - 1); + // _knob is not added to _track until we know the position + + _track.addEventListener(MouseEvent.CLICK, handleTrackClick); + _knob.addEventListener(MouseEvent.MOUSE_DOWN, handleKnobDown); + _knob.addEventListener(MouseEvent.MOUSE_UP, handleKnobUp); } /** @@ -109,6 +141,36 @@ public class SimpleVideoDisplay extends Sprite } } + protected function handleTrackClick (event :MouseEvent) :void + { + event.stopImmediatePropagation(); + + // see if we can seek to a position + const dur :Number = _player.getDuration(); + if (isNaN(dur)) { + log.debug("Duration is NaN, track click dropped."); + return; + } + + var perc :Number = event.localX / TRACK_WIDTH; + perc = Math.max(0, Math.min(1, perc)); + log.debug("Seek", "x", event.localX, "perc", perc, "pos", (perc * dur)); + + _player.seek(perc * dur); + } + + protected function handleKnobDown (event :MouseEvent) :void + { + event.stopImmediatePropagation(); + // TODO + } + + protected function handleKnobUp (event :MouseEvent) :void + { + event.stopImmediatePropagation(); + // TODO + } + protected function handlePlayerState (event :ValueEvent) :void { redrawHUD(); @@ -129,6 +191,20 @@ public class SimpleVideoDisplay extends Sprite dispatchEvent(event); } + protected function handlePlayerDuration (event :ValueEvent) :void + { + _hud.addChild(_track); + } + + protected function handlePlayerPosition (event :ValueEvent) :void + { + const pos :Number = Number(event.value); + _knob.x = (pos / _player.getDuration()) * TRACK_WIDTH; + if (_knob.parent == null) { + _track.addChild(_knob); + } + } + protected function handlePlayerError (event :ValueEvent) :void { // TODO.. maybe just redispatch @@ -153,41 +229,42 @@ public class SimpleVideoDisplay extends Sprite 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 { + if ((state != VideoPlayerCodes.STATE_READY) && + (state != VideoPlayerCodes.STATE_PLAYING) && + (state != VideoPlayerCodes.STATE_PAUSED)) { // draw something loading-like + // TODO animated swf 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(); + return; + } + + // draw a nice circle + g.beginFill(0x000000, .7); + 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 { // READY or PAUSED + 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(); } } @@ -196,8 +273,17 @@ public class SimpleVideoDisplay extends Sprite protected var _player :VideoPlayer; protected var _hud :Sprite; + + protected var _track :Sprite; + + /** The seek selector knob, positioned on the hud. */ + protected var _knob :Sprite; /** Our mask, also defines our boundaries for clicking. */ protected var _mask :Shape; + + protected static const PAD :int = 10; + protected static const TRACK_HEIGHT :int = 20; + protected static const TRACK_WIDTH :int = NATIVE_WIDTH - (PAD * 2); } }