/***************************************************** * * Copyright 2009 Adobe Systems Incorporated. All Rights Reserved. * ***************************************************** * The contents of this file are subject to the Mozilla Public License * Version 1.1 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * * The Initial Developer of the Original Code is Adobe Systems Incorporated. * Portions created by Adobe Systems Incorporated are Copyright (C) 2009 Adobe Systems * Incorporated. All Rights Reserved. * *****************************************************/ package org.osmf.netmocker { import flash.events.TimerEvent; import flash.net.NetConnection; import flash.net.NetStream; import flash.net.NetStreamPlayOptions; import flash.net.NetStreamPlayTransitions; import flash.utils.Timer; import org.osmf.net.NetStreamCodes; public class MockNetStream extends NetStream { /** * Constructor. **/ public function MockNetStream(connection:NetConnection) { super(connection); _connection = connection; // Intercept all NetStatusEvents dispatched from the base class. eventInterceptor = new NetStatusEventInterceptor(this); playheadTimer = new Timer(TIMER_DELAY); playheadTimer.addEventListener(TimerEvent.TIMER, onPlayheadTimer); } /** * The expected duration of the stream, in seconds. Necessary so that * this mock stream class knows when to dispatch the events related * to a stream completing. The default is zero. **/ public function set expectedDuration(value:Number):void { this._expectedDuration = value; } public function get expectedDuration():Number { return _expectedDuration; } /** * The expected total number of bytes of the stream. Applies to progressive * media only. The default is 0. **/ public function set expectedBytesTotal(value:uint):void { this._expectedBytesTotal = value; } public function get expectedBytesTotal():uint { return _expectedBytesTotal; } /** * The expected duration of the stream when it's a subclip, in seconds. * This is different from expectedDuration when the stream being played * is a subclip. The default is NaN. **/ public function set expectedSubclipDuration(value:Number):void { this._expectedSubclipDuration = value; } public function get expectedSubclipDuration():Number { return _expectedSubclipDuration; } /** * The expected width of the stream, in pixels. Necessary so that * this mock stream class knows the dimensions to include in the * onMetaData callback. The default is zero. **/ public function set expectedWidth(value:Number):void { this._expectedWidth = value; } public function get expectedWidth():Number { return _expectedWidth; } /** * The expected array of cue points. Necessary so that this * mock stream class can call the in-stream callback * onCuePoint with the data you expect. * * Each value in the array should be an object * with the following properties: * */ public function set expectedCuePoints(value:Array):void { this._expectedCuePoints = value; } public function get expectedCuePoints():Array { return this._expectedCuePoints; } /** * The expected height of the stream, in pixels. Necessary so that * this mock stream class knows the dimensions to include in the * onMetaData callback. The default is zero. **/ public function set expectedHeight(value:Number):void { this._expectedHeight = value; } public function get expectedHeight():Number { return _expectedHeight; } /** * An Array of EventInfos, representing the events that are expected * to be dispatched when the playhead has passed a certain position. **/ public function set expectedEvents(value:Array):void { this._expectedEvents = value; } public function get expectedEvents():Array { return _expectedEvents; } // Overrides // override public function get bufferLength():Number { return bufferLengthSet ? _bufferLength : super.bufferLength; } public function set bufferLength(value:Number):void { bufferLengthSet = true; _bufferLength = value; } override public function get bytesLoaded():uint { return _bytesLoaded; } override public function get bytesTotal():uint { // The bytesTotal value doesn't "register" until playback begins. return (playing || elapsedTime > 0) ? _expectedBytesTotal : 0; } override public function get time():Number { // Return value is in seconds. return playing ? (elapsedTime + (flash.utils.getTimer() - absoluteTimeAtLastPlay))/1000 : elapsedTime; } override public function close():void { playing = false; elapsedTime = 0; playheadTimer.stop(); } override public function play(...arguments):void { // The flash player sets the bufferTime to a 0.1 minimum for VOD (http://). if (arguments != null && arguments.length > 0 && arguments[0].toString().substr(0,4) == "http") { isProgressive = true; bufferTime = bufferTime < .1 ? .1 : bufferTime; } else { isProgressive = false; _expectedBytesTotal = 0; } commonPlay(); } override public function play2(nso:NetStreamPlayOptions):void { if (nso.transition == NetStreamPlayTransitions.SWITCH) { var infos:Array = [ {"code":NetStreamCodes.NETSTREAM_PLAY_TRANSITION, "details":nso.streamName, "level":LEVEL_STATUS} ]; eventInterceptor.dispatchNetStatusEvents(infos, EVENT_DELAY); var newTimer:Timer = new Timer(350, 1); switchCompleteTimers.push(newTimer); newTimer.addEventListener(TimerEvent.TIMER_COMPLETE, sendSwitchCompleteMsg); newTimer.start(); } else { isProgressive = false; _expectedBytesTotal = 0; commonPlay(); } } override public function pause():void { if (playing) { elapsedTime += ((flash.utils.getTimer() - absoluteTimeAtLastPlay) /1000); } playing = false; playheadTimer.stop(); var infos:Array = [ {"code":NetStreamCodes.NETSTREAM_PAUSE_NOTIFY, "level":LEVEL_STATUS} , {"code":NetStreamCodes.NETSTREAM_BUFFER_FLUSH, "level":LEVEL_STATUS} ]; eventInterceptor.dispatchNetStatusEvents(infos, EVENT_DELAY); } override public function resume():void { absoluteTimeAtLastPlay = flash.utils.getTimer(); playing = true; playheadTimer.start(); var infos:Array = [ {"code":NetStreamCodes.NETSTREAM_UNPAUSE_NOTIFY, "level":LEVEL_STATUS} , {"code":NetStreamCodes.NETSTREAM_PLAY_START, "level":LEVEL_STATUS} , {"code":NetStreamCodes.NETSTREAM_BUFFER_FULL, "level":LEVEL_STATUS} ]; eventInterceptor.dispatchNetStatusEvents(infos, EVENT_DELAY); } override public function seek(offset:Number):void { // Offset is in seconds. if (offset >= 0 && offset <= normalizedExpectedDuration) { // Reset the fake cue point logic this.lastFiredCuePointTime = -1; //elapsedTime = offset; if (playing) { absoluteTimeAtLastPlay = flash.utils.getTimer(); } var infos:Array = [ {"code":NetStreamCodes.NETSTREAM_SEEK_NOTIFY, "level":LEVEL_STATUS} , {"code":NetStreamCodes.NETSTREAM_PLAY_START, "level":LEVEL_STATUS} , {"code":NetStreamCodes.NETSTREAM_BUFFER_FULL, "level":LEVEL_STATUS} ]; eventInterceptor.dispatchNetStatusEvents(infos, EVENT_DELAY); // There's a bug in NetStream (FP-1705) where NetStream.time // doesn't get updated until after the NetStream.Seek.Notify // event is dispatched. We mirror this bug here. var timer:Timer = new Timer(300, 1); timer.addEventListener(TimerEvent.TIMER, onNetStreamSeekBugTimer); timer.start(); function onNetStreamSeekBugTimer(event:TimerEvent):void { timer.removeEventListener(TimerEvent.TIMER, onNetStreamSeekBugTimer); elapsedTime = offset; } } else { // TODO } } // Internals // private function onPlayheadTimer(event:TimerEvent):void { var infos:Array; if (time >= normalizedExpectedDuration) { elapsedTime = normalizedExpectedDuration; playing = false; playheadTimer.stop(); // For progressive, the NetStream.Play.Stop event is fired upon // completion. For streaming, the NetStream.Play.Complete event // is fired to onPlayStatus upon completion (and you might get // a number of NetStream.Play.Stop events during playback). // infos = [ {"code":NetStreamCodes.NETSTREAM_PLAY_STOP, "level":LEVEL_STATUS} , {"code":NetStreamCodes.NETSTREAM_BUFFER_FLUSH, "level":LEVEL_STATUS} , {"code":NetStreamCodes.NETSTREAM_BUFFER_EMPTY, "level":LEVEL_STATUS} ]; eventInterceptor.dispatchNetStatusEvents(infos); if (isProgressive == false) { this.client.onPlayStatus({code:NetStreamCodes.NETSTREAM_PLAY_COMPLETE}); } } else { infos = getInfosForPosition(time); if (infos.length > 0) { eventInterceptor.dispatchNetStatusEvents(infos); } // Call in-stream onCuePoint if we passed an expected cue point. if (expectedCuePoints.length > 0) { for each (var info:Object in expectedCuePoints) { if ((time >= info.time) && (info.time > this.lastFiredCuePointTime)) { // This will throw a Reference error if onCuePoint is not // implemented on the client object. No reason to eat that // exception here because this is an error. It means the caller // added expected cue points on a media element with no // possible way of detecting them. this.client.onCuePoint(info); this.lastFiredCuePointTime = info.time; break; } } } } _bytesLoaded = Math.min(_expectedBytesTotal, _bytesLoaded + _expectedBytesTotal / 4); } private function getInfosForPosition(position:Number):Array { var infos:Array = []; for (var i:int = _expectedEvents.length; i > 0; i--) { var eventInfo:EventInfo = _expectedEvents[i-1]; if (position >= eventInfo.position) { infos.push( {"code":eventInfo.code, "level":eventInfo.level} ); // Remove the eventInfo, we don't want to dispatch // it twice. _expectedEvents.splice(i-1, 1); } } return infos; } private function get normalizedExpectedDuration():Number { return isNaN(expectedSubclipDuration) ? expectedDuration : expectedSubclipDuration; } private function commonPlay():void { if (expectedDuration != 0) { var info:Object = {}; info["duration"] = expectedDuration; if (expectedWidth > 0) { info["width"] = expectedWidth; } if (expectedHeight > 0) { info["height"] = expectedHeight; } if (expectedCuePoints != null && expectedCuePoints.length > 0) { info["cuePoints"] = expectedCuePoints; } try { client.onMetaData(info); } catch (e:ReferenceError) { // Swallow, there's no such property on the client // and that's OK. } } absoluteTimeAtLastPlay = flash.utils.getTimer(); playing = true; playheadTimer.start(); var infos:Array = [ {"code":NetStreamCodes.NETSTREAM_PLAY_RESET, "level":LEVEL_STATUS} , {"code":NetStreamCodes.NETSTREAM_PLAY_START, "level":LEVEL_STATUS} , {"code":NetStreamCodes.NETSTREAM_BUFFER_FULL, "level":LEVEL_STATUS} ]; eventInterceptor.dispatchNetStatusEvents(infos, EVENT_DELAY); } private function sendSwitchCompleteMsg(event:TimerEvent):void { var oldTimer:Timer = switchCompleteTimers.shift(); oldTimer.removeEventListener(TimerEvent.TIMER, sendSwitchCompleteMsg); this.client.onPlayStatus({code:NetStreamCodes.NETSTREAM_PLAY_TRANSITION_COMPLETE}); } private var _connection:NetConnection; private var eventInterceptor:NetStatusEventInterceptor; private var _expectedDuration:Number = 0; private var _expectedBytesTotal:uint = 0; private var _expectedSubclipDuration:Number = NaN; private var _expectedWidth:Number = 0; private var _expectedHeight:Number = 0; private var _expectedEvents:Array = []; private var _expectedCuePoints:Array = []; private var lastFiredCuePointTime:int = -1; private var bufferLengthSet:Boolean = false; private var _bufferLength:Number; private var playheadTimer:Timer; private var switchCompleteTimers:Vector. = new Vector.; private var playing:Boolean = false; private var elapsedTime:Number = 0; // seconds private var absoluteTimeAtLastPlay:Number = 0; // milliseconds private var isProgressive:Boolean; private var _bytesLoaded:uint = 0; private static const TIMER_DELAY:int = 100; private static const EVENT_DELAY:int = 100; private static const LEVEL_STATUS:String = "status"; private static const LEVEL_ERROR:String = "error"; } }