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}