1/*****************************************************
2*
3*  Copyright 2009 Adobe Systems Incorporated.  All Rights Reserved.
4*
5*****************************************************
6*  The contents of this file are subject to the Mozilla Public License
7*  Version 1.1 (the "License"); you may not use this file except in
8*  compliance with the License. You may obtain a copy of the License at
9*  http://www.mozilla.org/MPL/
10*
11*  Software distributed under the License is distributed on an "AS IS"
12*  basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
13*  License for the specific language governing rights and limitations
14*  under the License.
15*
16*
17*  The Initial Developer of the Original Code is Adobe Systems Incorporated.
18*  Portions created by Adobe Systems Incorporated are Copyright (C) 2009 Adobe Systems
19*  Incorporated. All Rights Reserved.
20*
21*****************************************************/
22package org.osmf.net
23{
24	import __AS3__.vec.Vector;
25
26	import flash.errors.IllegalOperationError;
27	import flash.events.NetStatusEvent;
28	import flash.events.TimerEvent;
29	import flash.net.NetConnection;
30	import flash.net.NetStream;
31	import flash.net.NetStreamPlayOptions;
32	import flash.net.NetStreamPlayTransitions;
33	import flash.utils.Dictionary;
34	import flash.utils.Timer;
35	import flash.utils.getTimer;
36
37	import org.osmf.utils.OSMFStrings;
38
39	CONFIG::LOGGING
40	{
41	import org.osmf.logging.Logger;
42	import org.osmf.logging.Log;
43	}
44
45	/**
46	 * NetStreamSwitchManager is a default implementation of
47	 * NetStreamSwitchManagerBase.   It manages transitions between
48	 * multi-bitrate (MBR) streams using configurable switching rules.
49	 *
50	 *  @langversion 3.0
51	 *  @playerversion Flash 10
52	 *  @playerversion AIR 1.5
53	 *  @productversion OSMF 1.0
54	 **/
55	public class NetStreamSwitchManager extends NetStreamSwitchManagerBase
56	{
57		/**
58		 * Constructor.
59		 *
60		 * @param connection The NetConnection for the NetStream that will be managed.
61		 * @param netStream The NetStream to manage.
62		 * @param resource The DynamicStreamingResource that is playing in the NetStream.
63		 * @param metrics The provider of runtime metrics.
64		 * @param switchingRules The switching rules that this manager will use.
65		 *
66		 *  @langversion 3.0
67		 *  @playerversion Flash 10
68		 *  @playerversion AIR 1.5
69		 *  @productversion OSMF 1.0
70		 **/
71		public function NetStreamSwitchManager
72			( connection:NetConnection
73			, netStream:NetStream
74			, resource:DynamicStreamingResource
75			, metrics:NetStreamMetricsBase
76			, switchingRules:Vector.<SwitchingRuleBase>)
77		{
78			super();
79
80			this.connection = connection;
81			this.netStream = netStream;
82			this.dsResource = resource;
83			this.metrics = metrics;
84			this.switchingRules = switchingRules || new Vector.<SwitchingRuleBase>();
85
86			_currentIndex = Math.max(0, Math.min(maxAllowedIndex, dsResource.initialIndex));
87
88			checkRulesTimer = new Timer(RULE_CHECK_INTERVAL);
89			checkRulesTimer.addEventListener(TimerEvent.TIMER, checkRules);
90
91			failedDSI = new Dictionary();
92
93			// We set the bandwidth in both directions based on a multiplier applied to the bitrate level.
94			_bandwidthLimit = 1.4 * resource.streamItems[resource.streamItems.length-1].bitrate * 1000/8;
95
96			netStream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus);
97
98			// Make sure we get onPlayStatus first (by setting a higher priority)
99			// so that we can expose a consistent state to clients.
100			NetClient(netStream.client).addHandler(NetStreamCodes.ON_PLAY_STATUS, onPlayStatus, int.MAX_VALUE);
101		}
102
103		/**
104		 * @private
105		 */
106		override public function set autoSwitch(value:Boolean):void
107		{
108			super.autoSwitch = value;
109
110			CONFIG::LOGGING
111			{
112				debug("autoSwitch() - setting to " + value);
113			}
114
115			if (autoSwitch)
116			{
117				CONFIG::LOGGING
118				{
119					debug("autoSwitch() - starting check rules timer.");
120				}
121				checkRulesTimer.start();
122			}
123			else
124			{
125				CONFIG::LOGGING
126				{
127					debug("autoSwitch() - stopping check rules timer.");
128				}
129				checkRulesTimer.stop();
130			}
131		}
132
133		/**
134		 * @private
135		 */
136		override public function get currentIndex():uint
137		{
138			return _currentIndex;
139		}
140
141		/**
142		 * @private
143		 */
144		override public function get maxAllowedIndex():int
145		{
146			var count:int = dsResource.streamItems.length - 1;
147			return (count < super.maxAllowedIndex ? count : super.maxAllowedIndex);
148		}
149
150		/**
151		 * @private
152		 */
153		override public function set maxAllowedIndex(value:int):void
154		{
155			if (value > dsResource.streamItems.length)
156			{
157				throw new RangeError(OSMFStrings.getString(OSMFStrings.STREAMSWITCH_INVALID_INDEX));
158			}
159			super.maxAllowedIndex = value;
160			metrics.maxAllowedIndex = value;
161		}
162
163		/**
164		 * @private
165		 **/
166		override public function switchTo(index:int):void
167		{
168			if (!autoSwitch)
169			{
170				if (index < 0 || index > maxAllowedIndex)
171				{
172					throw new RangeError(OSMFStrings.getString(OSMFStrings.STREAMSWITCH_INVALID_INDEX));
173				}
174				else
175				{
176					CONFIG::LOGGING
177					{
178						debug("switchTo() - manually switching to index: " + index);
179					}
180
181					if (actualIndex == -1)
182					{
183						prepareForSwitching();
184					}
185					executeSwitch(index);
186				}
187			}
188			else
189			{
190				throw new IllegalOperationError(OSMFStrings.getString(OSMFStrings.STREAMSWITCH_STREAM_NOT_IN_MANUAL_MODE));
191			}
192		}
193
194		// Protected
195		//
196
197		/**
198		 * Override this method to provide additional decisioning around
199		 * allowing automatic switches to occur.  This method will be invoked
200		 * just prior to a switch request.  If false is returned, that switch
201		 * request will not take place.
202		 *
203		 * <p>By default, the implementation does the following:</p>
204		 * <p>1) When a switch down occurs, the stream being switched from has its
205		 * failed count incremented. If, when the switching rules are evaluated
206		 * again, a rule suggests switching up, since the stream previously
207		 * failed, it won't be tried again until a duration (30s) elapses. This
208		 * provides a better user experience by preventing a situation where
209		 * the switch up is attempted but then fails almost immediately.</p>
210		 * <p>2) Once a stream item has 3 failures, there will be no more
211		 * attempts to switch to it until an interval (5m) has expired.  At the
212		 * end of this interval, all failed counts are reset to zero.</p>
213		 *
214		 * @param newIndex The new index to switch to.
215		 **/
216		protected function canAutoSwitchNow(newIndex:int):Boolean
217		{
218			// If this stream has failed, we don't want to try it again until
219			// the wait period has elapsed
220			if (dsiFailedCounts[newIndex] >= 1)
221			{
222				var current:int = getTimer();
223				if (current - failedDSI[newIndex] < DEFAULT_WAIT_DURATION_AFTER_DOWN_SWITCH)
224				{
225					CONFIG::LOGGING
226					{
227						debug("canAutoSwitchNow() - ignoring switch request because index has " + dsiFailedCounts[newIndex]+" failure(s) and only "+ (current - failedDSI[newIndex])/1000 + " seconds have passed since the last failure.");
228					}
229					return false;
230				}
231			}
232			// If the requested index is currently locked out, then we don't
233			// allow the switch.
234			else if (dsiFailedCounts[newIndex] > DEFAULT_MAX_UP_SWITCHES_PER_STREAM_ITEM)
235			{
236				return false;
237			}
238
239			return true;
240		}
241
242		/**
243		 * The multiplier to apply to the maximum bandwidth for the client.  The
244		 * default is 140% of the highest bitrate stream.
245		 **/
246		protected final function get bandwidthLimit():Number
247		{
248			return _bandwidthLimit;
249		}
250		protected final function set bandwidthLimit(value:Number):void
251		{
252			_bandwidthLimit = value;
253		}
254
255		// Internals
256		//
257
258		/**
259		 * Executes the switch to the specified index.
260		 *
261		 *  @langversion 3.0
262		 *  @playerversion Flash 10
263		 *  @playerversion AIR 1.5
264		 *  @productversion OSMF 1.0
265		 */
266		private function executeSwitch(targetIndex:int):void
267		{
268			var nso:NetStreamPlayOptions = new NetStreamPlayOptions();
269
270			var playArgs:Object = NetStreamUtils.getPlayArgsForResource(dsResource);
271
272			nso.start = playArgs.start;
273			nso.len = playArgs.len;
274			nso.streamName = dsResource.streamItems[targetIndex].streamName;
275			nso.oldStreamName = oldStreamName;
276			nso.transition = NetStreamPlayTransitions.SWITCH;
277
278			CONFIG::LOGGING
279			{
280				debug("executeSwitch() - Switching to index " + (targetIndex) + " at " + Math.round(dsResource.streamItems[targetIndex].bitrate) + " kbps");
281			}
282
283			switching = true;
284
285			netStream.play2(nso);
286
287			oldStreamName = dsResource.streamItems[targetIndex].streamName;
288
289			if (targetIndex < actualIndex && autoSwitch)
290			{
291				// This is a failure for the current stream, so let's tag it as such.
292				incrementDSIFailedCount(actualIndex);
293
294				// Keep track of when it failed so we don't try it again for
295				// another failedItemWaitPeriod milliseconds to improve the
296				// user experience.
297				failedDSI[actualIndex] = getTimer();
298			}
299		}
300
301		/**
302		 * Checks all the switching rules. If a switching rule returns -1, it is
303		 * recommending no change.  If a switching rule returns a number greater than
304		 * -1 it is recommending a switch to that index. This method uses the lesser of
305		 * all the recommended indices that are greater than -1.
306		 *
307		 *  @langversion 3.0
308		 *  @playerversion Flash 10
309		 *  @playerversion AIR 1.5
310		 *  @productversion OSMF 1.0
311		 */
312		private function checkRules(event:TimerEvent):void
313		{
314			if (switchingRules == null || switching)
315			{
316				return;
317			}
318
319			var newIndex:int = int.MAX_VALUE;
320
321			for (var i:int = 0; i < switchingRules.length; i++)
322			{
323				var n:int =  switchingRules[i].getNewIndex();
324
325				if (n != -1 && n < newIndex)
326				{
327					newIndex = n;
328				}
329			}
330
331			if (	newIndex != -1
332				&& 	newIndex != int.MAX_VALUE
333				&&	newIndex != actualIndex
334			   )
335			{
336				newIndex = Math.min(newIndex, maxAllowedIndex);
337			}
338
339			if (	newIndex != -1
340				&& 	newIndex != int.MAX_VALUE
341				&&	newIndex != actualIndex
342				&&	!switching
343				&&	newIndex <= maxAllowedIndex
344				&&  canAutoSwitchNow(newIndex)
345			   )
346			{
347				CONFIG::LOGGING
348				{
349					debug("checkRules() - Calling for switch to " + newIndex + " at " + dsResource.streamItems[newIndex].bitrate + " kbps");
350				}
351				executeSwitch(newIndex);
352			}
353		}
354
355		private function onNetStatus(event:NetStatusEvent):void
356		{
357			CONFIG::LOGGING
358			{
359				debug("onNetStatus() - event.info.code=" + event.info.code);
360			}
361
362			switch (event.info.code)
363			{
364				case NetStreamCodes.NETSTREAM_PLAY_START:
365					if (actualIndex == -1)
366					{
367						prepareForSwitching();
368					}
369					else if (autoSwitch && checkRulesTimer.running == false)
370					{
371						checkRulesTimer.start();
372					}
373					break;
374				case NetStreamCodes.NETSTREAM_PLAY_TRANSITION:
375					switching  = false;
376					actualIndex = dsResource.indexFromName(event.info.details);
377					metrics.currentIndex = actualIndex;
378					lastTransitionIndex = actualIndex;
379					break;
380				case NetStreamCodes.NETSTREAM_PLAY_FAILED:
381					switching  = false;
382					break;
383				case NetStreamCodes.NETSTREAM_SEEK_NOTIFY:
384					switching  = false;
385					if (lastTransitionIndex >= 0)
386					{
387						_currentIndex = lastTransitionIndex;
388					}
389					break;
390				case NetStreamCodes.NETSTREAM_PLAY_STOP:
391					checkRulesTimer.stop();
392					CONFIG::LOGGING
393					{
394						debug("onNetStatus() - Stopping rules since server has stopped sending data");
395					}
396					break;
397			}
398		}
399
400		private function onPlayStatus(info:Object):void
401		{
402			CONFIG::LOGGING
403			{
404				debug("onPlayStatus() - info.code=" + info.code);
405			}
406
407			switch (info.code)
408			{
409				case NetStreamCodes.NETSTREAM_PLAY_TRANSITION_COMPLETE:
410					if (lastTransitionIndex >= 0)
411					{
412						_currentIndex = lastTransitionIndex;
413						lastTransitionIndex = -1;
414					}
415
416					CONFIG::LOGGING
417					{
418						debug("onPlayStatus() - Transition complete to index: " + currentIndex + " at " + Math.round(dsResource.streamItems[currentIndex].bitrate) + " kbps");
419					}
420
421					break;
422			}
423		}
424
425		/**
426		 * Prepare the manager for switching.  Note that this doesn't necessarily
427		 * mean a switch is imminent.
428		 **/
429		private function prepareForSwitching():void
430		{
431			initDSIFailedCounts();
432
433			metrics.resource = dsResource;
434
435			actualIndex = 0;
436			lastTransitionIndex = -1;
437
438			if ((dsResource.initialIndex >= 0) && (dsResource.initialIndex < dsResource.streamItems.length))
439			{
440				actualIndex = dsResource.initialIndex;
441			}
442
443			if (autoSwitch)
444			{
445				checkRulesTimer.start();
446			}
447
448			setThrottleLimits(dsResource.streamItems.length - 1);
449			CONFIG::LOGGING
450			{
451				debug("prepareForSwitching() - Starting with stream index " + actualIndex + " at " + Math.round(dsResource.streamItems[actualIndex].bitrate) + " kbps");
452			}
453			metrics.currentIndex = actualIndex;
454		}
455
456		private function initDSIFailedCounts():void
457		{
458			if (dsiFailedCounts != null)
459			{
460				dsiFailedCounts.length = 0;
461				dsiFailedCounts = null;
462			}
463
464			dsiFailedCounts = new Vector.<int>();
465			for (var i:int = 0; i < dsResource.streamItems.length; i++)
466			{
467				dsiFailedCounts.push(0);
468			}
469		}
470
471		private function incrementDSIFailedCount(index:int):void
472		{
473			dsiFailedCounts[index]++;
474
475			// Start the timer that clears the failed counts if one of them
476			// just went over the max failed count
477			if (dsiFailedCounts[index] > DEFAULT_MAX_UP_SWITCHES_PER_STREAM_ITEM)
478			{
479				if (clearFailedCountsTimer == null)
480				{
481					clearFailedCountsTimer = new Timer(DEFAULT_CLEAR_FAILED_COUNTS_INTERVAL, 1);
482					clearFailedCountsTimer.addEventListener(TimerEvent.TIMER, clearFailedCounts);
483				}
484
485				clearFailedCountsTimer.start();
486			}
487		}
488
489		private function clearFailedCounts(event:TimerEvent):void
490		{
491			clearFailedCountsTimer.removeEventListener(TimerEvent.TIMER, clearFailedCounts);
492			clearFailedCountsTimer = null;
493			initDSIFailedCounts();
494		}
495
496		private function setThrottleLimits(index:int):void
497		{
498			connection.call("setBandwidthLimit", null, _bandwidthLimit, _bandwidthLimit);
499		}
500
501		CONFIG::LOGGING
502		{
503		private function debug(...args):void
504		{
505			logger.debug(new Date().toTimeString() + ">>> NetStreamSwitchManager." + args);
506		}
507		}
508
509		private var netStream:NetStream;
510		private var dsResource:DynamicStreamingResource;
511		private var switchingRules:Vector.<SwitchingRuleBase>;
512		private var metrics:NetStreamMetricsBase;
513		private var checkRulesTimer:Timer;
514		private var clearFailedCountsTimer:Timer;
515		private var actualIndex:int = -1;
516		private var oldStreamName:String;
517		private var switching:Boolean;
518		private var _currentIndex:int;
519		private var lastTransitionIndex:int = -1;
520		private var connection:NetConnection;
521		private var dsiFailedCounts:Vector.<int>;		// This vector keeps track of the number of failures
522														// for each DynamicStreamingItem in the DynamicStreamingResource
523		private var failedDSI:Dictionary;
524		private var _bandwidthLimit:Number = 0;;
525
526		private static const RULE_CHECK_INTERVAL:Number = 500;	// Switching rule check interval in milliseconds
527		private static const DEFAULT_MAX_UP_SWITCHES_PER_STREAM_ITEM:int = 3;
528		private static const DEFAULT_WAIT_DURATION_AFTER_DOWN_SWITCH:int = 30000;
529		private static const DEFAULT_CLEAR_FAILED_COUNTS_INTERVAL:Number = 300000;	// default of 5 minutes for clearing failed counts on stream items
530
531		CONFIG::LOGGING
532		{
533			private static const logger:Logger = Log.getLogger("org.osmf.net.NetStreamSwitchManager");
534		}
535	}
536}