diff --git a/dist/video-js.swf b/dist/video-js.swf index 2b743568..d4d70c65 100644 Binary files a/dist/video-js.swf and b/dist/video-js.swf differ diff --git a/sandbox/videojs.html.example b/sandbox/videojs.html.example index c76ce0ee..b6864e8f 100644 --- a/sandbox/videojs.html.example +++ b/sandbox/videojs.html.example @@ -4,13 +4,13 @@ Video.js | HTML5 Video Player - + - + diff --git a/src/VideoJS.as b/src/VideoJS.as index 3e3d8bba..3002b7a9 100644 --- a/src/VideoJS.as +++ b/src/VideoJS.as @@ -5,7 +5,7 @@ package{ import com.videojs.structs.ExternalEventName; import com.videojs.structs.ExternalErrorEventName; import com.videojs.Base64; - + import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageScaleMode; @@ -55,6 +55,7 @@ package{ _app.model.stageRect = new Rectangle(0, 0, stage.stageWidth, stage.stageHeight); // add content-menu version info + var _ctxVersion:ContextMenuItem = new ContextMenuItem("VideoJS Flash Component v" + VERSION, false, false); var _ctxAbout:ContextMenuItem = new ContextMenuItem("Copyright © 2014 Brightcove, Inc.", false, false); var _ctxMenu:ContextMenu = new ContextMenu(); @@ -69,6 +70,7 @@ package{ try{ ExternalInterface.addCallback("vjs_appendBuffer", onAppendBufferCalled); ExternalInterface.addCallback("vjs_echo", onEchoCalled); + ExternalInterface.addCallback("vjs_endOfStream", onEndOfStreamCalled); ExternalInterface.addCallback("vjs_getProperty", onGetPropertyCalled); ExternalInterface.addCallback("vjs_setProperty", onSetPropertyCalled); ExternalInterface.addCallback("vjs_autoplay", onAutoplayCalled); @@ -185,6 +187,10 @@ package{ private function onEchoCalled(pResponse:* = null):*{ return pResponse; } + + private function onEndOfStreamCalled():*{ + _app.model.endOfStream(); + } private function onGetPropertyCalled(pPropertyName:String = ""):*{ @@ -274,8 +280,11 @@ package{ return null; } - private function onSetPropertyCalled(pPropertyName:String = "", pValue:* = null):void{ + private function onSetPropertyCalled(pPropertyName:String = "", pValue:* = null):void{ switch(pPropertyName){ + case "duration": + _app.model.duration = Number(pValue); + break; case "mode": _app.model.mode = String(pValue); break; diff --git a/src/com/videojs/VideoJSModel.as b/src/com/videojs/VideoJSModel.as index 4a9e42e8..5b4ec2eb 100644 --- a/src/com/videojs/VideoJSModel.as +++ b/src/com/videojs/VideoJSModel.as @@ -10,7 +10,7 @@ package com.videojs{ import com.videojs.structs.ExternalEventName; import com.videojs.structs.PlaybackType; import com.videojs.structs.PlayerMode; - + import flash.events.Event; import flash.events.EventDispatcher; import flash.external.ExternalInterface; @@ -43,9 +43,9 @@ package com.videojs{ private var _rtmpConnectionURL:String = ""; private var _rtmpStream:String = ""; private var _poster:String = ""; - + private static var _instance:VideoJSModel; - + public function VideoJSModel(pLock:SingletonLock){ if (!pLock is SingletonLock) { throw new Error("Invalid Singleton access. Use VideoJSModel.getInstance()!"); @@ -64,11 +64,12 @@ package com.videojs{ } return _instance; } - + public function get mode():String{ return _mode; } - public function set mode(pMode:String):void{ + + public function set mode(pMode:String):void { switch(pMode){ case PlayerMode.VIDEO: _mode = pMode; @@ -84,32 +85,36 @@ package com.videojs{ public function get jsEventProxyName():String{ return _jsEventProxyName; } - public function set jsEventProxyName(pName:String):void{ + public function set jsEventProxyName(pName:String):void { _jsEventProxyName = cleanEIString(pName); } public function get jsErrorEventProxyName():String{ return _jsErrorEventProxyName; } - public function set jsErrorEventProxyName(pName:String):void{ + public function set jsErrorEventProxyName(pName:String):void { _jsErrorEventProxyName = cleanEIString(pName); } public function get stageRect():Rectangle{ return _stageRect; } - public function set stageRect(pRect:Rectangle):void{ + public function set stageRect(pRect:Rectangle):void { _stageRect = pRect; } - public function appendBuffer(bytes:ByteArray):void{ + public function appendBuffer(bytes:ByteArray):void { _provider.appendBuffer(bytes); } + + public function endOfStream():void { + _provider.endOfStream(); + } public function get backgroundColor():Number{ return _backgroundColor; } - public function set backgroundColor(pColor:Number):void{ + public function set backgroundColor(pColor:Number):void { if(pColor < 0){ _backgroundColor = 0; } @@ -122,7 +127,7 @@ package com.videojs{ public function get backgroundAlpha():Number{ return _backgroundAlpha; } - public function set backgroundAlpha(pAlpha:Number):void{ + public function set backgroundAlpha(pAlpha:Number):void { if(pAlpha < 0){ _backgroundAlpha = 0; } @@ -134,7 +139,7 @@ package com.videojs{ public function get videoReference():Video{ return _videoReference; } - public function set videoReference(pVideo:Video):void{ + public function set videoReference(pVideo:Video):void { _videoReference = pVideo; } @@ -148,7 +153,7 @@ package com.videojs{ public function get volume():Number{ return _volume; } - public function set volume(pVolume:Number):void{ + public function set volume(pVolume:Number):void { if(pVolume >= 0 && pVolume <= 1){ _volume = pVolume; } @@ -167,11 +172,17 @@ package com.videojs{ } return 0; } + + public function set duration(value:Number):void { + if(_provider && _provider is HTTPVideoProvider) { + (_provider as HTTPVideoProvider).duration = value; + } + } public function get autoplay():Boolean{ return _autoplay; } - public function set autoplay(pValue:Boolean):void{ + public function set autoplay(pValue:Boolean):void { _autoplay = pValue; } @@ -181,7 +192,7 @@ package com.videojs{ } return _src; } - public function set src(pValue:String):void{ + public function set src(pValue:String):void { _src = pValue; _rtmpConnectionURL = ""; _rtmpStream = ""; @@ -199,7 +210,7 @@ package com.videojs{ public function get rtmpConnectionURL():String{ return _rtmpConnectionURL; } - public function set rtmpConnectionURL(pURL:String):void{ + public function set rtmpConnectionURL(pURL:String):void { _src = ""; _rtmpConnectionURL = pURL; } @@ -207,7 +218,7 @@ package com.videojs{ public function get rtmpStream():String{ return _rtmpStream; } - public function set rtmpStream(pValue:String):void{ + public function set rtmpStream(pValue:String):void { _src = ""; _rtmpStream = pValue; broadcastEventExternally(ExternalEventName.ON_SRC_CHANGE, _src); @@ -235,7 +246,7 @@ package com.videojs{ * @param pValue * */ - public function set srcFromFlashvars(pValue:String):void{ + public function set srcFromFlashvars(pValue:String):void { _src = pValue; _currentPlaybackType = PlaybackType.HTTP initProvider(); @@ -251,7 +262,7 @@ package com.videojs{ public function get poster():String{ return _poster; } - public function set poster(pValue:String):void{ + public function set poster(pValue:String):void { _poster = pValue; broadcastEvent(new VideoJSEvent(VideoJSEvent.POSTER_SET)); } @@ -278,7 +289,7 @@ package com.videojs{ public function get muted():Boolean{ return (_volume == 0); } - public function set muted(pValue:Boolean):void{ + public function set muted(pValue:Boolean):void { if(pValue){ var __lastSetVolume:Number = _lastSetVolume; volume = 0; @@ -314,14 +325,14 @@ package com.videojs{ public function get preload():Boolean{ return _preload; } - public function set preload(pValue:Boolean):void{ + public function set preload(pValue:Boolean):void { _preload = pValue; } public function get loop():Boolean{ return _loop; } - public function set loop(pValue:Boolean):void{ + public function set loop(pValue:Boolean):void { _loop = pValue; } @@ -404,7 +415,7 @@ package com.videojs{ * @param e * */ - public function broadcastEvent(e:Event):void{ + public function broadcastEvent(e:Event):void { dispatchEvent(e); } @@ -413,7 +424,7 @@ package com.videojs{ * @param args * */ - public function broadcastEventExternally(... args):void{ + public function broadcastEventExternally(... args):void { if(_jsEventProxyName != ""){ if(ExternalInterface.available){ var __incomingArgs:* = args as Array; @@ -428,7 +439,7 @@ package com.videojs{ * @param args * */ - public function broadcastErrorEventExternally(... args):void{ + public function broadcastErrorEventExternally(... args):void { if(_jsErrorEventProxyName != ""){ if(ExternalInterface.available){ var __incomingArgs:* = args as Array; @@ -442,7 +453,7 @@ package com.videojs{ * Loads the video in a paused state. * */ - public function load():void{ + public function load():void { if(_provider){ _provider.load(); } @@ -452,7 +463,7 @@ package com.videojs{ * Loads the video and begins playback immediately. * */ - public function play():void{ + public function play():void { if(_provider){ _provider.play(); } @@ -462,7 +473,7 @@ package com.videojs{ * Pauses video playback. * */ - public function pause():void{ + public function pause():void { if(_provider){ _provider.pause(); } @@ -472,7 +483,7 @@ package com.videojs{ * Resumes video playback. * */ - public function resume():void{ + public function resume():void { if(_provider){ _provider.resume(); } @@ -483,7 +494,7 @@ package com.videojs{ * @param pValue * */ - public function seekBySeconds(pValue:Number):void{ + public function seekBySeconds(pValue:Number):void { if(_provider){ _provider.seekBySeconds(pValue); } @@ -494,7 +505,7 @@ package com.videojs{ * @param pValue A float from 0 to 1 that represents the desired seek percent. * */ - public function seekByPercent(pValue:Number):void{ + public function seekByPercent(pValue:Number):void { if(_provider){ _provider.seekByPercent(pValue); } @@ -504,7 +515,7 @@ package com.videojs{ * Stops video playback, clears the video element, and stops any loading proceeses. * */ - public function stop():void{ + public function stop():void { if(_provider){ _provider.stop(); } @@ -539,7 +550,7 @@ package com.videojs{ return pString.replace(/[^A-Za-z0-9_.]/gi, ""); } - private function initProvider():void{ + private function initProvider():void { if(_provider){ _provider.die(); _provider = null; diff --git a/src/com/videojs/providers/HTTPAudioProvider.as b/src/com/videojs/providers/HTTPAudioProvider.as index a825225f..ea7f2b22 100644 --- a/src/com/videojs/providers/HTTPAudioProvider.as +++ b/src/com/videojs/providers/HTTPAudioProvider.as @@ -114,6 +114,10 @@ package com.videojs.providers{ public function appendBuffer(bytes:ByteArray):void{ throw "HTTPAudioProvider does not support appendBuffer"; } + + public function endOfStream():void{ + throw "HTTPAudioProvider does not support endOfStream"; + } public function get buffered():Number{ if(duration > 0){ @@ -441,4 +445,4 @@ package com.videojs.providers{ _model.broadcastEventExternally(ExternalEventName.ON_METADATA, _metadata); } } -} \ No newline at end of file +} diff --git a/src/com/videojs/providers/HTTPVideoProvider.as b/src/com/videojs/providers/HTTPVideoProvider.as index 7cc4e5c5..3656bbe3 100644 --- a/src/com/videojs/providers/HTTPVideoProvider.as +++ b/src/com/videojs/providers/HTTPVideoProvider.as @@ -4,14 +4,13 @@ package com.videojs.providers{ import com.videojs.events.VideoPlaybackEvent; import com.videojs.structs.ExternalErrorEventName; import com.videojs.structs.ExternalEventName; - import com.videojs.structs.PlaybackType; - import flash.events.EventDispatcher; import flash.events.NetStatusEvent; import flash.events.TimerEvent; import flash.media.Video; import flash.net.NetConnection; import flash.net.NetStream; + import flash.net.NetStreamAppendBytesAction; import flash.utils.ByteArray; import flash.utils.Timer; import flash.utils.getTimer; @@ -28,6 +27,22 @@ package com.videojs.providers{ private var _loadErrored:Boolean = false; private var _pauseOnStart:Boolean = false; private var _pausePending:Boolean = false; + /** + * The number of seconds between the logical start of the stream and the current zero + * playhead position of the NetStream. During normal, file-based playback this value should + * always be zero. When the NetStream is in data generation mode, seeking during playback + * resets the zero point of the stream to the seek target. To recover the playhead position + * in the logical stream, this value can be added to the NetStream reported time. + * + * @see http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/NetStream.html#play() + */ + private var _startOffset:Number = 0; + /** + * If true, an empty NetStream buffer should be interpreted as the end of the video. This + * is probably the case because the video data is being fed to the NetStream dynamically + * through appendBuffer, not for traditional file download video. + */ + private var _ending:Boolean = false; private var _videoReference:Video; /** @@ -36,7 +51,7 @@ package com.videojs.providers{ * we cache the intended time, and use it IN PLACE OF NetStream's time when the time accessor is hit. */ private var _pausedSeekValue:Number = -1; - + private var _src:Object; private var _metadata:Object; private var _isPlaying:Boolean = false; @@ -48,6 +63,7 @@ package com.videojs.providers{ private var _hasEnded:Boolean = false; private var _canPlayThrough:Boolean = false; private var _loop:Boolean = false; + private var _durationOverride:Number; private var _model:VideoJSModel; @@ -56,7 +72,6 @@ package com.videojs.providers{ _metadata = {}; _throughputTimer = new Timer(250, 0); _throughputTimer.addEventListener(TimerEvent.TIMER, onThroughputTimerTick); - } public function get loop():Boolean{ @@ -73,7 +88,7 @@ package com.videojs.providers{ return _pausedSeekValue; } else{ - return _ns.time; + return _startOffset + _ns.time; } } else{ @@ -84,11 +99,17 @@ package com.videojs.providers{ public function get duration():Number{ if(_metadata != null && _metadata.duration != undefined){ return Number(_metadata.duration); + } else if( _durationOverride && _durationOverride > 0 ) { + return _durationOverride; } else{ return 0; } } + + public function set duration(value:Number):void { + _durationOverride = value; + } public function get readyState():int{ // if we have metadata and a known duration @@ -139,17 +160,28 @@ package com.videojs.providers{ } } + public function appendBytesAction(action:String):void { + if(_ns) { + _ns.appendBytesAction(action); + } + } + public function appendBuffer(bytes:ByteArray):void{ _ns.appendBytes(bytes); } + + public function endOfStream():void{ + _ending = true; + } public function get buffered():Number{ - if(duration > 0){ + // _src.path == null when in data generation mode + if(_ns && _src.path == null) + { + return _startOffset + _ns.bufferLength + _ns.time; + } else if(duration > 0){ return (_ns.bytesLoaded / _ns.bytesTotal) * duration; - } - else if (_ns){ - return _ns.bufferLength + _ns.time; - } else{ + } else { return 0; } } @@ -274,23 +306,29 @@ package com.videojs.providers{ } public function seekBySeconds(pTime:Number):void{ - if(_isPlaying){ - if(duration != 0 && pTime <= duration){ - _isSeeking = true; - _throughputTimer.stop(); - if(_isPaused){ - _pausedSeekValue = pTime; - } - _ns.seek(pTime); - _isBuffering = true; + if(_isPlaying) + { + _isSeeking = true; + _throughputTimer.stop(); + if(_isPaused) + { + _pausedSeekValue = pTime; } } - else if(_hasEnded){ - _ns.seek(pTime); + else if(_hasEnded) + { _isPlaying = true; _hasEnded = false; - _isBuffering = true; } + + if(_src.path === null) + { + _startOffset = pTime; + } + + _ns.seek(pTime); + _isBuffering = true; + } public function seekByPercent(pPercent:Number):void{ @@ -388,6 +426,7 @@ package com.videojs.providers{ _ns = null; } _ns = new NetStream(_nc); + _ns.inBufferSeek = true; _ns.addEventListener(NetStatusEvent.NET_STATUS, onNetStreamStatus); _ns.client = this; _ns.bufferTime = .5; @@ -464,6 +503,13 @@ package com.videojs.providers{ } _loadStarted = true; break; + + case "NetStream.SeekStart.Notify": + if(_src.path === null) { + appendBytesAction(NetStreamAppendBytesAction.RESET_SEEK); + } + _model.broadcastEventExternally(ExternalEventName.ON_SEEK_START); + break; case "NetStream.Buffer.Full": _pausedSeekValue = -1; @@ -482,12 +528,28 @@ package com.videojs.providers{ case "NetStream.Buffer.Empty": // should not fire if ended/paused. issue #38 if(!_isPlaying){ return; } + + // reaching the end of the buffer after endOfStream has been called means we've + // hit the end of the video + if (_ending) { + _ending = false; + _isPlaying = false; + _isPaused = true; + _hasEnded = true; + _model.broadcastEvent(new VideoPlaybackEvent(VideoPlaybackEvent.ON_STREAM_CLOSE, {info:e.info})); + _model.broadcastEventExternally(ExternalEventName.ON_PAUSE); + _model.broadcastEventExternally(ExternalEventName.ON_PLAYBACK_COMPLETE); + + _startOffset = 0; + _pausedSeekValue = 0; + break; + } + _isBuffering = true; _model.broadcastEventExternally(ExternalEventName.ON_BUFFER_EMPTY); break; case "NetStream.Play.Stop": - if(!_loop){ _isPlaying = false; _isPaused = true; @@ -503,7 +565,7 @@ package com.videojs.providers{ _throughputTimer.stop(); _throughputTimer.reset(); break; - + case "NetStream.Seek.Notify": _isPlaying = true; _isSeeking = false; @@ -514,8 +576,7 @@ package com.videojs.providers{ _loadStartTimestamp = getTimer(); _throughputTimer.reset(); _throughputTimer.start(); - - break; + break; case "NetStream.Play.StreamNotFound": _loadErrored = true; @@ -539,6 +600,7 @@ package com.videojs.providers{ } public function onMetaData(pMetaData:Object):void{ + _metadata = pMetaData; if(pMetaData.duration != undefined){ _isLive = false; diff --git a/src/com/videojs/providers/IProvider.as b/src/com/videojs/providers/IProvider.as index 0345e9c2..523c949f 100644 --- a/src/com/videojs/providers/IProvider.as +++ b/src/com/videojs/providers/IProvider.as @@ -30,6 +30,13 @@ package com.videojs.providers{ * @param bytes the ByteArray of data to append. */ function appendBuffer(bytes:ByteArray):void; + + /** + * Indicates that no further bytes will appended to the source + * buffer. After this method has been called, reaching the end + * of buffered input is equivalent to the end of the media. + */ + function endOfStream():void; /** * Should return an interger that reflects the closest parallel to @@ -168,4 +175,4 @@ package com.videojs.providers{ function die():void; } -} \ No newline at end of file +} diff --git a/src/com/videojs/providers/RTMPVideoProvider.as b/src/com/videojs/providers/RTMPVideoProvider.as index e9ea25c8..b6d329e5 100644 --- a/src/com/videojs/providers/RTMPVideoProvider.as +++ b/src/com/videojs/providers/RTMPVideoProvider.as @@ -137,6 +137,10 @@ package com.videojs.providers{ public function appendBuffer(bytes:ByteArray):void{ throw "RTMPVideoProvider does not support appendBuffer"; } + + public function endOfStream():void{ + throw "RTMPVideoProvider does not support endOfStream"; + } public function get buffered():Number{ if(duration > 0){