/***************************************************** * * 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.net.httpstreaming { import __AS3__.vec.Vector; import flash.events.Event; import flash.events.IOErrorEvent; import flash.events.NetStatusEvent; import flash.events.ProgressEvent; import flash.events.SecurityErrorEvent; import flash.events.TimerEvent; import flash.net.NetConnection; import flash.net.NetStream; import flash.net.NetStreamPlayOptions; import flash.net.NetStreamPlayTransitions; import flash.net.URLLoader; import flash.net.URLLoaderDataFormat; import flash.net.URLStream; import flash.utils.ByteArray; import flash.utils.IDataInput; import flash.utils.Timer; import org.osmf.events.DVRStreamInfoEvent; import org.osmf.events.HTTPStreamingFileHandlerEvent; import org.osmf.events.HTTPStreamingIndexHandlerEvent; import org.osmf.net.NetStreamCodes; import org.osmf.net.httpstreaming.dvr.DVRInfo; import org.osmf.net.httpstreaming.flv.FLVHeader; import org.osmf.net.httpstreaming.flv.FLVParser; import org.osmf.net.httpstreaming.flv.FLVTag; import org.osmf.net.httpstreaming.flv.FLVTagScriptDataObject; import org.osmf.net.httpstreaming.flv.FLVTagVideo; import org.osmf.net.httpstreaming.flv.FLVTagAudio; [Event(name="DVRStreamInfo", type="org.osmf.events.DVRStreamInfoEvent")] CONFIG::LOGGING { import org.osmf.logging.Logger; } CONFIG::FLASH_10_1 { import flash.net.NetStreamAppendBytesAction; } [ExcludeClass] /** * * @private * * HTTPNetStream is a NetStream subclass which can accept input via the * appendBytes method. In general, the assumption is that a large media * file is broken up into a number of smaller fragments. * * There are two important aspects of working with an HTTPNetStream: * 1) How to map a specific playback time to the media file fragment * which holds the media for that time. * 2) How to unmarshal the data from a media file fragment so that it can * be fed to the NetStream as TCMessages. * * The former is the responsibility of HTTPStreamingIndexHandlerBase, * the latter the responsibility of HTTPStreamingFileHandlerBase. */ public class HTTPNetStream extends NetStream { /** * Constructor. * * @param connection The NetConnection to use. * @param indexHandler Object which exposes the index, which maps * playback times to media file fragments. * @param fileHandler Object which canunmarshal the data from a * media file fragment so that it can be fed to the NetStream as * TCMessages. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion OSMF 1.0 */ public function HTTPNetStream ( connection:NetConnection , indexHandler:HTTPStreamingIndexHandlerBase , fileHandler:HTTPStreamingFileHandlerBase ) { super(connection); _savedBytes = new ByteArray(); this.indexHandler = indexHandler; this.fileHandler = fileHandler; indexHandler.addEventListener(HTTPStreamingIndexHandlerEvent.NOTIFY_INDEX_READY, onIndexReady); indexHandler.addEventListener(HTTPStreamingIndexHandlerEvent.NOTIFY_RATES, onRates); indexHandler.addEventListener(HTTPStreamingIndexHandlerEvent.REQUEST_LOAD_INDEX, onRequestLoadIndexFile); indexHandler.addEventListener(HTTPStreamingIndexHandlerEvent.NOTIFY_ERROR, onIndexError); indexHandler.addEventListener(HTTPStreamingIndexHandlerEvent.NOTIFY_SEGMENT_DURATION, onSegmentDurationFromIndexHandler); indexHandler.addEventListener(HTTPStreamingIndexHandlerEvent.NOTIFY_SCRIPT_DATA, onScriptDataFromIndexHandler); indexHandler.addEventListener(DVRStreamInfoEvent.DVRSTREAMINFO, onDVRStreamInfo); // removed NOTIFY_TIME_BIAS fileHandler.addEventListener(HTTPStreamingFileHandlerEvent.NOTIFY_SEGMENT_DURATION, onSegmentDurationFromFileHandler); fileHandler.addEventListener(HTTPStreamingFileHandlerEvent.NOTIFY_SCRIPT_DATA, onScriptDataFromFileHandler); fileHandler.addEventListener(HTTPStreamingFileHandlerEvent.NOTIFY_ERROR, onErrorFromFileHandler); mainTimer = new Timer(MAIN_TIMER_INTERVAL); mainTimer.addEventListener(TimerEvent.TIMER, onMainTimer); mainTimer.start(); this.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); } /** * Whether HTTPNetStream implements enhanced seek on the client side. * * Enhanced seek allows for keyframe-accurate seeking. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion OSMF 1.0 */ public function set enhancedSeek(value:Boolean):void { _enhancedSeekEnabled = value; } public function get enhancedSeek():Boolean { return _enhancedSeekEnabled; } /** * Getters/(setters if applicable) of a bunch of properties related to the quality of service. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion OSMF 1.0 */ public function get downloadRatio():Number { return _lastDownloadRatio; } public function set qualityLevel(value:int):void { if (_manualSwitchMode) { setQualityLevel(value); } else { throw new Error("qualityLevel cannot be set to this value at this time"); } } public function get qualityLevel():int { return _qualityLevel; } public function get manualSwitchMode():Boolean { return _manualSwitchMode; } public function set manualSwitchMode(value:Boolean):void { _manualSwitchMode = value; } /** * Initialization info for the HTTPStreamingIndexHandlerBase. * * If specified, this will be passed to the index handler's * initialize method when playback is initiated. Otherwise, * the argument to play (or play2) will be used. **/ public function get indexInfo():HTTPStreamingIndexInfoBase { return _indexInfo; } public function set indexInfo(value:HTTPStreamingIndexInfoBase):void { _indexInfo = value; } // new functionality public function DVRGetStreamInfo(streamName:Object):void { if (indexIsReady) { // TODO: should there be indexHandler.DVRGetStreamInfo() to re-trigger the event? } else { // TODO: should there be a guard to protect the case where indexIsReady is not yet true BUT play has already been called, so we are in an // "initializing but not yet ready" state? This is only needed if the caller is liable to call DVRGetStreamInfo and then, before getting the // event back, go ahead and call play() indexHandler.dvrGetStreamInfo(_indexInfo != null ? _indexInfo : streamName); } } // Overrides // /** * The arguments to this method can mirror the arguments to the * superclass's method: * 1) media file * 2) URL * 3) name/start/len/reset * a) Subclips * b) Live * c) Resetting playlist * * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion OSMF 1.0 */ override public function play(...args):void { if (args.length < 1) { throw new Error("HTTPStream.play() requires at least one argument"); } // Signal to the base class that we're entering Data Generation Mode. super.play(null); // Playback is considered to start when we first append some bytes. /* dispatchEvent ( new NetStatusEvent ( NetStatusEvent.NET_STATUS , false , false , {code:NetStreamCodes.NETSTREAM_PLAY_START, level:"status"} ) ); */ _signalPlayStartPending = true; // Before we feed any TCMessages to the Flash Player, we must feed // an FLV header first. // var header:FLVHeader = new FLVHeader(); var headerBytes:ByteArray = new ByteArray(); header.write(headerBytes); attemptAppendBytes(headerBytes); // Initialize ourselves and the index handler. // setState(HTTPStreamingState.INIT); _initialTime = -1; _seekTime = -1; indexIsReady = false; indexHandler.initialize(_indexInfo != null ? _indexInfo : args[0]); if (args.length >= 2) { _seekTarget = Number(args[1]); if (_seekTarget < 0) { if (_dvrInfo != null) { _seekTarget = _dvrInfo.startTime; } else { _seekTarget = 0; // FMS behavior, mimic -1 or -2 being passed in } } } else { // This is the start of playback, so no seek. _seekTarget = 0; } if (args.length >= 3) { _playForDuration = Number(args[2]); } else { _playForDuration = -1; } _unpublishNotifyPending = false; } private function signalPlayStart():void { dispatchEvent ( new NetStatusEvent ( NetStatusEvent.NET_STATUS , false , false , {code:NetStreamCodes.NETSTREAM_PLAY_START, level:"status"} ) ); } /** * @private */ override public function play2(param:NetStreamPlayOptions):void { if (param.transition == NetStreamPlayTransitions.RESET) { // XXX Need to reset playback if we're already playing. // Is this done via seek? // The only difference between play and play2 for the RESET // case is that play2 might start at a specific quality level. setQualityLevelForStreamName(param.streamName); play(param.streamName, param.start, param.len); } else if (param.transition == NetStreamPlayTransitions.SWITCH) { setQualityLevelForStreamName(param.streamName); } else { // Not sure which other modes we should add support for. super.play2(param); } } /** * @private */ override public function seek(offset:Number):void { // (change to override seek rather than do this based on seek notify event) // can't do this unless you're already playing (for instance, you can't leave INIT to go to SEEK)! // XXX need to double-check to see if there's more guards needed here if(offset < 0) { offset = 0; // FMS rule. Seek to <0 is same as seeking to zero. } if (_state != HTTPStreamingState.INIT) // can't seek before playback starts { if(_initialTime < 0) { _seekTarget = offset + 0; // this covers the "don't know initial time" case, rare } else { _seekTarget = offset + _initialTime; } _seekTime = -1; // but _initialTime stays known setState(HTTPStreamingState.SEEK); super.seek(offset); } _unpublishNotifyPending = false; } /** * @private */ override public function close():void { indexIsReady = false; switch (_state) { case HTTPStreamingState.PLAY: case HTTPStreamingState.PLAY_START_NEXT: case HTTPStreamingState.PLAY_START_SEEK: _urlStreamVideo.close(); // immediate abort } setState(HTTPStreamingState.HALT); mainTimer.stop(); dispatchEvent ( new NetStatusEvent ( NetStatusEvent.NET_STATUS , false , false , {code:NetStreamCodes.NETSTREAM_PLAY_STOP, level:"status"} ) ); // XXX might need to do other things here super.close(); } /** * @private */ override public function get time():Number { if(_seekTime >= 0 && _initialTime >= 0) { _lastValidTimeTime = (super.time + _seekTime) - _initialTime; // we remember what we say when time is valid, and just spit that back out any time we don't have valid data. This is probably the right answer. // the only thing we could do better is also run a timer to ask ourselves what it is whenever it might be valid and save that, just in case the // user doesn't ask... but it turns out most consumers poll this all the time in order to update playback position displays } return _lastValidTimeTime; } // Internal // private function setState(value:String):void { _prevState = _state; _state = value; } private function insertScriptDataTag(tag:FLVTagScriptDataObject, first:Boolean = false):void { if (!_insertScriptDataTags) { _insertScriptDataTags = new Vector.(); } if (first) { _insertScriptDataTags.unshift(tag); // push front } else { _insertScriptDataTags.push(tag); } } private function flvTagHandler(tag:FLVTag):Boolean { // this is the new common FLVTag Parser's tag handler var i:int; if (_insertScriptDataTags) { for (i = 0; i < _insertScriptDataTags.length; i++) { var t:FLVTagScriptDataObject; var bytes:ByteArray; t = _insertScriptDataTags[i]; t.timestamp = tag.timestamp; bytes = new ByteArray(); t.write(bytes); _flvParserProcessed += bytes.length; attemptAppendBytes(bytes); } _insertScriptDataTags = null; } if (_playForDuration >= 0) { if (_initialTime >= 0) // until we know this, we don't know where to stop, and if we're enhanced-seeking then we need that logic to be what sets this up { var currentTime:Number = (tag.timestamp / 1000.0) + _fileTimeAdjustment; if (currentTime > (_initialTime + _playForDuration)) { setState(HTTPStreamingState.STOP); _flvParserDone = true; if (_seekTime < 0) { _seekTime = _playForDuration + _initialTime; // FMS behavior... the time is always the final time, even if we seek to past it // XXX actually, FMS actually lets exactly one frame though at that point and that's why the time gets to be what it is // XXX that we don't exactly mimic that is also why setting a duration of zero doesn't do what FMS does (plays exactly that one still frame) } return false; } } } if (_enhancedSeekTarget < 0) { if (_initialTime < 0) { if (_dvrInfo != null) { _initialTime = _dvrInfo.startTime; } else { _initialTime = (tag.timestamp / 1000.0) + _fileTimeAdjustment; } } if (_seekTime < 0) { _seekTime = (tag.timestamp / 1000.0) + _fileTimeAdjustment; } } else // doing enhanced seek { if (tag is FLVTagVideo) { if (_flvParserIsSegmentStart) { var _muteTag:FLVTagVideo = new FLVTagVideo(); _muteTag.timestamp = tag.timestamp; // may get overwritten, ok _muteTag.codecID = FLVTagVideo(tag).codecID; // same as in use _muteTag.frameType = FLVTagVideo.FRAME_TYPE_INFO; _muteTag.infoPacketValue = FLVTagVideo.INFO_PACKET_SEEK_START; // and start saving, with this as the first... _enhancedSeekTags = new Vector.(); _enhancedSeekTags.push(_muteTag); _flvParserIsSegmentStart = false; } if ((tag.timestamp / 1000.0) + _fileTimeAdjustment >= _enhancedSeekTarget) { _enhancedSeekTarget = -1; _seekTime = (tag.timestamp / 1000.0) + _fileTimeAdjustment; if(_initialTime < 0) { _initialTime = _seekTime; } var _unmuteTag:FLVTagVideo = new FLVTagVideo(); _unmuteTag.timestamp = tag.timestamp; // may get overwritten, ok _unmuteTag.codecID = (_enhancedSeekTags[0]).codecID; // take the codec ID of the corresponding SEEK_START _unmuteTag.frameType = FLVTagVideo.FRAME_TYPE_INFO; _unmuteTag.infoPacketValue = FLVTagVideo.INFO_PACKET_SEEK_END; _enhancedSeekTags.push(_unmuteTag); // twiddle and dump for (i=0; i<_enhancedSeekTags.length; i++) { var vTag:FLVTagVideo; vTag = _enhancedSeekTags[i]; //vTag.timestamp = tag.timestamp; if (vTag.codecID == FLVTagVideo.CODEC_ID_AVC && vTag.avcPacketType == FLVTagVideo.AVC_PACKET_TYPE_NALU) { // for H.264 we need to move the timestamp forward but the composition time offset backwards to compensate var adjustment:int = tag.timestamp - vTag.timestamp; // how far we are adjusting var compTime:int = vTag.avcCompositionTimeOffset; compTime = vTag.avcCompositionTimeOffset; compTime -= adjustment; // do the adjustment vTag.avcCompositionTimeOffset = compTime; // save adjustment vTag.timestamp = tag.timestamp; // and adjust the timestamp forward } else { // the simple case vTag.timestamp = tag.timestamp; } bytes = new ByteArray(); vTag.write(bytes); _flvParserProcessed += bytes.length; attemptAppendBytes(bytes); } _enhancedSeekTags = null; // and append this one bytes = new ByteArray(); tag.write(bytes); _flvParserProcessed += bytes.length; attemptAppendBytes(bytes); if (_playForDuration >= 0) { return true; // need to continue seeing the tags, and can't shortcut because we're being dropped off mid-segment } _flvParserDone = true; return false; // and end of parsing (caller must dump rest, unparsed) } // past enhanced seek target else { _enhancedSeekTags.push(tag); } } // is video else if (tag is FLVTagScriptDataObject) { // ScriptDataObject tags simply pass through with unadjusted timestamps rather than discarding or saving for later bytes = new ByteArray(); tag.write(bytes); _flvParserProcessed += bytes.length; attemptAppendBytes(bytes); } // else tag is FLVTagAudio, which we discard, unless... else if (tag is FLVTagAudio) { var aTag:FLVTagAudio = tag as FLVTagAudio; if (aTag.isCodecConfiguration) // need to pass this through? (ex. AAC AudioConfig message) { // yes, can never skip initialization... bytes = new ByteArray(); tag.write(bytes); _flvParserProcessed += bytes.length; attemptAppendBytes(bytes); } } return true; } // enhanced seek // finally, pass this one on to appendBytes... bytes = new ByteArray(); tag.write(bytes); _flvParserProcessed += bytes.length; attemptAppendBytes(bytes); // probably done seeing the tags, unless we are in playForDuration mode... if (_playForDuration >= 0) { if (_segmentDuration >= 0 && _flvParserIsSegmentStart) { // if the segmentDuration has been reported, it is possible that we might be able to shortcut // but we need to be careful that this is the first tag of the segment, otherwise we don't know what duration means in relation to the tag timestamp _flvParserIsSegmentStart = false; // also used by enhanced seek, but not generally set/cleared for everyone. be careful. currentTime = (tag.timestamp / 1000.0) + _fileTimeAdjustment; if (currentTime + _segmentDuration >= (_initialTime + _playForDuration)) { // it stops somewhere in this segment, so we need to keep seeing the tags return true; } else { // stop is past the end of this segment, can shortcut and stop seeing tags _flvParserDone = true; return false; } } else { return true; // need to continue seeing the tags because either we don't have duration, or started mid-segment so don't know what duration means } } // else not in playForDuration mode... _flvParserDone = true; return false; } /** * All decision making code for MBR switching happens in this method. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion OSMF 1.0 */ private function autoAdjustQuality(seeking:Boolean):void { if (!_manualSwitchMode) { if (seeking) { // if we are in auto-switch, then the default is that we go to lowest rate to fulfill seek... fastest video buffer fill that way, plus for some forward seek index // strategies (patent-pending) we don't even have the data we need to upswitch until we get the low-rate data // however, some people want to be able to control this setQualityLevel(0); return; // to avoid indenting rest of function for the else case } // auto-adjust quality level aka bitrate at this point // XXX IMPORTANT NOTE: WE AREN'T YET SETTING BUFFER TIME UPWARDS IN NON-SEEK SITUATIONS. NEED TO DO THIS SO RUNNING DRY IS LESS PAINFUL. // we work in ratios so that we can cancel kbps out of all the equations // the lastDownloadRatio is "playback time of last segment downloaded" / "amount of time it took to download that whole segment, from request to finished" // the switchRatio[proposed] is "claimed rate of proposed quality" / "claimed rate of current quality" // // there are exactly four cases we need to deal with, and since I'm not an optimist, I'll start from the worst case: // 1. the lastDownloadRatio is <1 and < switchRatio[current-1]: Bandwidth is way down, Switch to lowest rate immediately (even if there's an intermediate that might work). // 2. the lastDownloadRatio is <1 but >= switchRatio[current-1]: We should be able to keep going if we go down one level, do it // 3. the lastDownloadRatio is >= 1 but < switchRatio[current+1] OR no available rate is higher than current: Steady state where we like to be. Don't touch any knobs. // 4. the lastDownloadRatio is >= 1 and > switchRatio[current+1]: We can go up to rate n where n is the highest n for which lastDownloadRatio is still > switchRatio[n] // (but see caution about high lastDownloadRatio caused by cached response) // // XXX note: we don't currently do this, but we can hold off loading for a bit if and only if we are in state 3 AND the lastDownloadRatio is significantly >= 1 // (in addition to holding off loading if bufferLength is growing too far) // note: there is a danger that lastDownloadRatio is absurdly high because it is reflecting cached data. If that is detected, then in case 4 the switch up // should only be a single quality level upwards rather than seeking the top rate instantly... just in case even one level up is actually too high a rate in reality // // XXX this is also where we could look at dropped-frame history and drop a rate level as well, if necessary. not yet implemented. // // so on to the code... var proposedLevel:int; var switchRatio:Number; if (_lastDownloadRatio < 1.0) { // case 1 and 2 // first check to see if we are even able to switch down if (qualityLevel > 0) { // we are proposedLevel = qualityLevel - 1; switchRatio = _qualityRates[proposedLevel] / _qualityRates[qualityLevel]; if (_lastDownloadRatio < switchRatio) { setQualityLevel(0); // case 1, switch to lowest } else { setQualityLevel(proposedLevel); // case 2, down by one } } // else, already at lowest level } // case 1&2 else { // case 3 and 4 // first check to see if we are able to switch up if (qualityLevel < _numQualityLevels - 1) { proposedLevel = qualityLevel + 1; switchRatio = _qualityRates[proposedLevel] / _qualityRates[qualityLevel]; if (_lastDownloadRatio < switchRatio) { // case 3, don't touch anything. we're where we like to be. (well, actually, we like to be at the highest level with bandwidth to spare, but not everyone has that) } else { // is the last download ratio suspiciously high (cached data), or has aggressive upswitch been turned off? if (_lastDownloadRatio > 100.0 || !_aggressiveUpswitch) // XXX 100.0 s/b constant value { // keep proposed level of +1 } else { // seek better proposed level while (++proposedLevel < _numQualityLevels) { switchRatio = _qualityRates[proposedLevel] / _qualityRates[qualityLevel]; if (_lastDownloadRatio < switchRatio) break; // found one that's too high } --proposedLevel; } setQualityLevel(proposedLevel); } } // else already at highest level, can't up-switch } // case 3&4 } // !manualSwitch } private function byteSource(input:IDataInput, numBytes:Number):IDataInput { if (numBytes < 0) { return null; } if (numBytes) { if (_savedBytes.bytesAvailable + input.bytesAvailable < numBytes) { return null; } } else { if (_savedBytes.bytesAvailable + input.bytesAvailable < 1) { return null; } } if (_savedBytes.bytesAvailable) { var needed:int = numBytes - _savedBytes.bytesAvailable; if (needed > 0) { input.readBytes(_savedBytes, _savedBytes.length, needed); } return _savedBytes; } _savedBytes.length = 0; return input; } private function processAndAppend(inBytes:ByteArray):uint { var bytes:ByteArray; var processed:uint = 0; if (!inBytes) { return 0; } if (_flvParser) { inBytes.position = 0; // rewind _flvParserProcessed = 0; _flvParser.parse(inBytes, true, flvTagHandler); // common handler for FLVTags, parser consumes everything each time just as appendBytes does when in pass-through processed += _flvParserProcessed; if(!_flvParserDone) { // the common parser has more work to do in-path return processed; } else { // the common parser is done, so flush whatever is left and then pass through the rest of the segment bytes = new ByteArray(); _flvParser.flush(bytes); _flvParser = null; // and now we're done with it } } else { bytes = inBytes; } // now, 'bytes' is either what came in or what we massaged above // (ES is now part of unified parser) processed += bytes.length; if (_state != HTTPStreamingState.STOP) // we might exit this state { attemptAppendBytes(bytes); } return processed; } private function onMainTimer(timerEvent:TimerEvent):void { var bytes:ByteArray; CONFIG::LOGGING { if (_state != previouslyLoggedState) { logger.debug("State = " + _state); previouslyLoggedState = _state; } } switch (_state) { case HTTPStreamingState.INIT: // do nothing, but not an error to be here. could make timer run slower if we wanted. _seekAfterInit = true; break; // SEEK case case HTTPStreamingState.SEEK: switch (_prevState) { case HTTPStreamingState.PLAY: case HTTPStreamingState.PLAY_START_NEXT: case HTTPStreamingState.PLAY_START_SEEK: _urlStreamVideo.close(); // immediate abort break; default: // already not open break; } _dataAvailable = false; _savedBytes.length = 0; // correct? XXX if (_enhancedSeekEnabled) { _enhancedSeekTarget = _seekTarget; // XXX perhaps could just reuse _seekTarget? // XXX there is potentially an H.264 depth issue here, where we need to do a --i to pick up enough more frames to render. must revisit. } setState(HTTPStreamingState.LOAD_SEEK); break; // LOAD cases case HTTPStreamingState.LOAD_WAIT: // XXX this delay needs to shrink proportionate to the last download ratio... when we're close to or under 1, it needs to be no delay at all // XXX unless the bufferLength is longer (this ties into how fast switching can happen vs. timeliness of dispatch to cover jitter in loading) // XXX for now, we have a simplistic dynamic handler, in that if downloads are going poorly, we are a bit more aggressive about prefetching if (this._lastDownloadRatio < 2.0) // XXX this needs to be more linear, and/or settable { if (this.bufferLength < Math.max(7.5, this.bufferTime)) // XXX need to make settable { setState(HTTPStreamingState.LOAD_NEXT); } } else { if (this.bufferLength < Math.max(3.75, this.bufferTime)) // XXX need to make settable { setState(HTTPStreamingState.LOAD_NEXT); } } break; case HTTPStreamingState.LOAD_NEXT: autoAdjustQuality(false); if (qualityLevelHasChanged) { bytes = fileHandler.flushFileSegment(_savedBytes.bytesAvailable ? _savedBytes : null); processAndAppend(bytes); // XXX for testing, putting this reporting here, but it really needs to be more informative and thus generated up in the autoAdjustQuality code var info:Object = new Object(); info.code = NetStreamCodes.NETSTREAM_PLAY_TRANSITION_COMPLETE; info.level = "status"; var sdoTag:FLVTagScriptDataObject = new FLVTagScriptDataObject(); sdoTag.objects = ["onPlayStatus", info]; insertScriptDataTag(sdoTag); qualityLevelHasChanged = false; } setState(HTTPStreamingState.LOAD); break; case HTTPStreamingState.LOAD_SEEK: // seek always must flush per contract if (!_seekAfterInit) { bytes = fileHandler.flushFileSegment(_savedBytes.bytesAvailable ? _savedBytes : null); // processAndAppend(bytes); // XXX this might be unneccessary as we are about to RESET } CONFIG::FLASH_10_1 { appendBytesAction(NetStreamAppendBytesAction.RESET_SEEK); } if (!_seekAfterInit) { autoAdjustQuality(true); } _seekAfterInit = false; setState(HTTPStreamingState.LOAD); break; case HTTPStreamingState.LOAD: var nextRequest:HTTPStreamRequest; if (_signalPlayStartPending) { signalPlayStart(); _signalPlayStartPending = false; } // XXX the double test of _prevState in here is a little weird... might want to factor differently _segmentDuration = -1; // we now track whether or not this has been reported yet for this segment by the Index or File handler switch (_prevState) { case HTTPStreamingState.LOAD_SEEK: case HTTPStreamingState.LOAD_SEEK_RETRY_WAIT: nextRequest = indexHandler.getFileForTime(_seekTarget, qualityLevel); break; case HTTPStreamingState.LOAD_NEXT: case HTTPStreamingState.LOAD_NEXT_RETRY_WAIT: nextRequest = indexHandler.getNextFile(qualityLevel); break; default: throw new Error("in HTTPStreamState.LOAD with unknown _prevState " + _prevState); break; } if (nextRequest != null && nextRequest.urlRequest != null) { _loadComplete = false; CONFIG::LOGGING { logger.debug("loading: " + nextRequest.urlRequest.url.toString()); } _urlStreamVideo.load(nextRequest.urlRequest); date = new Date(); _lastDownloadStartTime = date.getTime(); switch (_prevState) { case HTTPStreamingState.LOAD_SEEK: case HTTPStreamingState.LOAD_SEEK_RETRY_WAIT: setState(HTTPStreamingState.PLAY_START_SEEK); break; case HTTPStreamingState.LOAD_NEXT: case HTTPStreamingState.LOAD_NEXT_RETRY_WAIT: setState(HTTPStreamingState.PLAY_START_NEXT); break; default: throw new Error("in HTTPStreamState.LOAD(2) with unknown _prevState " + _prevState); break; } } else if(nextRequest != null && nextRequest.retryAfter >= 0) { date = new Date(); _retryAfterWaitUntil = date.getTime() + (1000.0 * nextRequest.retryAfter); switch (_prevState) { case HTTPStreamingState.LOAD_SEEK: case HTTPStreamingState.LOAD_SEEK_RETRY_WAIT: setState(HTTPStreamingState.LOAD_SEEK_RETRY_WAIT); break; case HTTPStreamingState.LOAD_NEXT: case HTTPStreamingState.LOAD_NEXT_RETRY_WAIT: setState(HTTPStreamingState.LOAD_NEXT_RETRY_WAIT); break; default: throw new Error("in HTTPStreamState.LOAD(3) with unknown _prevState " + _prevState); break; } } else { bytes = fileHandler.flushFileSegment(_savedBytes.bytesAvailable ? _savedBytes : null); processAndAppend(bytes); setState(HTTPStreamingState.STOP); if (nextRequest != null && nextRequest.unpublishNotify) { _unpublishNotifyPending = true; } } break; case HTTPStreamingState.LOAD_SEEK_RETRY_WAIT: case HTTPStreamingState.LOAD_NEXT_RETRY_WAIT: var date:Date = new Date(); if (date.getTime() > _retryAfterWaitUntil) { setState(HTTPStreamingState.LOAD); } break; case HTTPStreamingState.PLAY_START_NEXT: fileHandler.beginProcessFile(false, 0); setState(HTTPStreamingState.PLAY_START_COMMON); break; case HTTPStreamingState.PLAY_START_SEEK: fileHandler.beginProcessFile(true, _seekTarget); setState(HTTPStreamingState.PLAY_START_COMMON); break; case HTTPStreamingState.PLAY_START_COMMON: // need to run the common FLVParser? if (_initialTime < 0 || _seekTime < 0 || _insertScriptDataTags || _enhancedSeekTarget >= 0 || _playForDuration >= 0) { if (_enhancedSeekTarget >= 0 || _playForDuration >= 0) { _flvParserIsSegmentStart = true; // warning, this isn't generally set/cleared, just used by these two cooperating things } _flvParser = new FLVParser(false); _flvParserDone = false; } setState(HTTPStreamingState.PLAY); break; case HTTPStreamingState.PLAY: var endSegment:Boolean = false; if (_dataAvailable) { var processLimit:int = 65000*4; // XXX needs to be settable var processed:int = 0; if (_enhancedSeekTarget >= 0) { processLimit = 0; // override slow-load } /* if(_bytesAvailable < fetchBytes) { // in the past, we simply returned in this case, figuring it was cheaper to just wait for enough to accumulate // we might want to revisit after performance testing fetchBytes = _bytesAvailable; } */ var input:IDataInput = null; _dataAvailable = false; while (_state == HTTPStreamingState.PLAY && (input = byteSource(_urlStreamVideo, fileHandler.inputBytesNeeded))) { bytes = fileHandler.processFileSegment(input); // XXX need to deal with end of file issues processed += processAndAppend(bytes); if (processLimit > 0 && processed >= processLimit) { _dataAvailable = true; break; } } if(_state != HTTPStreamingState.PLAY) break; // XXX if the reason we bailed is that we didn't have enough bytes, then if loadComplete we need to consume the rest into our save buffer // OR, if we don't do cross-segment saving then we simply need to ensure that we don't return but simply fall through to a later case // for now, we do the latter (also see below) if (_loadComplete && !input) { endSegment = true; } } else { if (_loadComplete) { endSegment = true; } } if (endSegment) { // then save any leftovers for the next segment round. if this is a kind of filehandler that needs that, they won't suck dry in onEndSegment. if (_urlStreamVideo.bytesAvailable) { _urlStreamVideo.readBytes(_savedBytes); } else { _savedBytes.length = 0; // just to be sure } setState(HTTPStreamingState.END_SEGMENT); } break; case HTTPStreamingState.END_SEGMENT: // give fileHandler a crack at any remaining data bytes = fileHandler.endProcessFile(_savedBytes.bytesAvailable ? _savedBytes : null); processAndAppend(bytes); _lastDownloadRatio = _segmentDuration / _lastDownloadDuration; // urlcomplete would have fired by now, otherwise we couldn't be done, and onEndSegment is the last possible chance to report duration if (_state != HTTPStreamingState.STOP && _state != HTTPStreamingState.HALT) { setState(HTTPStreamingState.LOAD_WAIT); } break; case HTTPStreamingState.STOP: var playCompleteInfo:Object = new Object(); playCompleteInfo.code = NetStreamCodes.NETSTREAM_PLAY_COMPLETE; playCompleteInfo.level = "status"; var playCompleteInfoSDOTag:FLVTagScriptDataObject = new FLVTagScriptDataObject(); playCompleteInfoSDOTag.objects = ["onPlayStatus", playCompleteInfo]; var tagBytes:ByteArray = new ByteArray(); playCompleteInfoSDOTag.write(tagBytes); CONFIG::FLASH_10_1 { appendBytesAction(NetStreamAppendBytesAction.END_SEQUENCE); appendBytesAction(NetStreamAppendBytesAction.RESET_SEEK); } attemptAppendBytes(tagBytes); setState(HTTPStreamingState.HALT); break; case HTTPStreamingState.HALT: // do nothing. timer could run slower in this state. break; default: throw new Error("HTTPStream cannot run undefined _state "+_state); break; } } private function onURLStatus(progressEvent:ProgressEvent):void { _dataAvailable = true; } private function onURLComplete(event:Event):void { var date:Date = new Date; _lastDownloadDuration = (date.getTime() - _lastDownloadStartTime) / 1000.0; _loadComplete = true; } private function onRequestLoadIndexFile(event:HTTPStreamingIndexHandlerEvent):void { var urlLoader:URLLoader = new URLLoader(event.request); var requestContext:Object = event.requestContext; if (event.binaryData) { urlLoader.dataFormat = URLLoaderDataFormat.BINARY; } else { urlLoader.dataFormat = URLLoaderDataFormat.TEXT; } urlLoader.addEventListener(Event.COMPLETE, onIndexLoadComplete); urlLoader.addEventListener(IOErrorEvent.IO_ERROR, onIndexURLError); urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onIndexURLError); function onIndexLoadComplete(innerEvent:Event):void { urlLoader.removeEventListener(Event.COMPLETE, onIndexLoadComplete); urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, onIndexURLError); urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onIndexURLError); indexHandler.processIndexData(urlLoader.data, requestContext); } function onIndexURLError(errorEvent:Event):void { CONFIG::LOGGING { logger.error("URLStream: " + _urlStreamVideo ); logger.error("index url error: " + errorEvent ); logger.error( "******* attempting to download the index file (bootstrap) caused error!" ); } urlLoader.removeEventListener(Event.COMPLETE, onIndexLoadComplete); urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, onIndexURLError); urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onIndexURLError); handleURLError(); } } private function onSegmentDurationFromFileHandler(event:HTTPStreamingFileHandlerEvent):void { _segmentDuration = event.segmentDuration; } private function onSegmentDurationFromIndexHandler(event:HTTPStreamingIndexHandlerEvent):void // TOOD: unify this with the above so we don't need to duplicate { _segmentDuration = event.segmentDuration; } private function onRates(event:HTTPStreamingIndexHandlerEvent):void { _qualityRates = event.rates; _streamNames = event.streamNames; _numQualityLevels = _qualityRates.length; } private function onIndexReady(event:HTTPStreamingIndexHandlerEvent):void { if (!indexIsReady) { if (event.live && _dvrInfo == null && !isNaN(event.offset)) { _seekTarget = event.offset; } _urlStreamVideo = new URLStream(); _urlStreamVideo.addEventListener(ProgressEvent.PROGRESS , onURLStatus , false, 0, true); _urlStreamVideo.addEventListener(Event.COMPLETE , onURLComplete , false, 0, true); _urlStreamVideo.addEventListener(IOErrorEvent.IO_ERROR , onVideoURLError , false, 0, true); _urlStreamVideo.addEventListener(SecurityErrorEvent.SECURITY_ERROR , onVideoURLError , false, 0, true); setState(HTTPStreamingState.SEEK); // was LOAD_SEEK, now want to pick up enhanced seek setup, if applicable. in the future, might want to change back? indexIsReady = true; } } private function onVideoURLError(event:Event):void { CONFIG::LOGGING { logger.error("URLStream: " + _urlStreamVideo ); logger.error("video error event: " + event ); logger.error( "******* attempting to download video fragment caused error event!" ); } handleURLError(); } private function handleURLError():void { // We map all URL errors to Play.StreamNotFound. dispatchEvent ( new NetStatusEvent ( NetStatusEvent.NET_STATUS , false , false , {code:NetStreamCodes.NETSTREAM_PLAY_STREAMNOTFOUND, level:"error"} ) ); } private function onScriptDataFromIndexHandler(event:HTTPStreamingIndexHandlerEvent):void { onScriptData(event.scriptDataObject, event.scriptDataFirst, event.scriptDataImmediate); } private function onScriptDataFromFileHandler(event:HTTPStreamingFileHandlerEvent):void { onScriptData(event.scriptDataObject, event.scriptDataFirst, event.scriptDataImmediate); // TODO: somehow figure out how to not need duplicate listeners } private function onErrorFromFileHandler(event:HTTPStreamingFileHandlerEvent):void { // We map file handler error to Play.NETSTREAM_PLAY_FILESTRUCTUREINVALID. setState(HTTPStreamingState.HALT); dispatchEvent ( new NetStatusEvent ( NetStatusEvent.NET_STATUS , false , false , {code:NetStreamCodes.NETSTREAM_PLAY_FILESTRUCTUREINVALID, level:"error"} ) ); } private function onScriptData(scriptDataObject:FLVTagScriptDataObject, scriptDataFirst:Boolean, scriptDataImmediate:Boolean):void { CONFIG::LOGGING { logger.debug("onScriptData called"); } if (scriptDataImmediate) { if (client) { if (client.hasOwnProperty(scriptDataObject.objects[0])) { client[scriptDataObject.objects[0]](scriptDataObject.objects[1]); // XXX note that we can only support a single argument for immediate dispatch } } } else { insertScriptDataTag(scriptDataObject, scriptDataFirst); } } private function onDVRStreamInfo(event:DVRStreamInfoEvent):void { _dvrInfo = event.info as DVRInfo; dispatchEvent(event.clone()); } /** * All errors from file index handler and file handler are passed to HTTPNetStream * via MediaErrorEvent. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion OSMF 1.0 */ private function onIndexError(event:HTTPStreamingIndexHandlerEvent):void { // We map all Index errors to Play.StreamNotFound. dispatchEvent ( new NetStatusEvent ( NetStatusEvent.NET_STATUS , false , false , {code:NetStreamCodes.NETSTREAM_PLAY_STREAMNOTFOUND, level:"error"} ) ); } private function setQualityLevel(value:int):void { if (value >= 0 && value < _numQualityLevels) { if (value != _qualityLevel) { _qualityLevel = value; qualityLevelHasChanged = true; dispatchEvent ( new NetStatusEvent ( NetStatusEvent.NET_STATUS , false , false , {code:NetStreamCodes.NETSTREAM_PLAY_TRANSITION, level:"status", details:_streamNames[value]} ) ); } } else { throw new Error("qualityLevel cannot be set to this value at this time"); } } private function setQualityLevelForStreamName(streamName:String):void { var level:int = -1; if (_streamNames != null) { for (var i:int = 0; i < _streamNames.length; i++) { if (streamName == _streamNames[i]) { level = i; break; } } } if (level != -1) { setQualityLevel(level); } } private function attemptAppendBytes(bytes:ByteArray):void { // Do nothing if this is not an Argo player. CONFIG::FLASH_10_1 { appendBytes(bytes); } } private function onNetStatus(event:NetStatusEvent):void { if (event.info.code == NetStreamCodes.NETSTREAM_BUFFER_EMPTY && _state == HTTPStreamingState.HALT) { finishStopProcess(); } } private function finishStopProcess():void { if (_unpublishNotifyPending) { dispatchEvent ( new NetStatusEvent ( NetStatusEvent.NET_STATUS , false , false , {code:NetStreamCodes.NETSTREAM_PLAY_UNPUBLISH_NOTIFY, level:"status"} ) ); _unpublishNotifyPending = false; } } private static const MAIN_TIMER_INTERVAL:int = 25; private var _indexInfo:HTTPStreamingIndexInfoBase = null; private var _numQualityLevels:int = 0; private var _qualityRates:Array; private var _streamNames:Array; private var _segmentDuration:Number = -1; private var _urlStreamVideo:URLStream = null; private var _loadComplete:Boolean = false; private var mainTimer:Timer; private var _dataAvailable:Boolean = false; private var _qualityLevel:int = 0; private var qualityLevelHasChanged:Boolean = false; private var _seekTarget:Number = -1; private var _lastDownloadStartTime:Number = -1; private var _lastDownloadDuration:Number; private var _lastDownloadRatio:Number = 0; private var _manualSwitchMode:Boolean = false; private var _aggressiveUpswitch:Boolean = true; // XXX needs a getter and setter, or to be part of a pluggable rate-setter private var indexHandler:HTTPStreamingIndexHandlerBase; private var fileHandler:HTTPStreamingFileHandlerBase; private var _totalDuration:Number = -1; private var _enhancedSeekTarget:Number = -1; // now in seconds, just like everything else private var _enhancedSeekEnabled:Boolean = false; private var _enhancedSeekTags:Vector.; private var _flvParserIsSegmentStart:Boolean = false; private var _savedBytes:ByteArray = null; private var _state:String = HTTPStreamingState.INIT; private var _prevState:String = null; private var _seekAfterInit:Boolean; private var indexIsReady:Boolean = false; private var _insertScriptDataTags:Vector. = null; private var _flvParser:FLVParser = null; // this is the new common FLVTag Parser private var _flvParserDone:Boolean = true; // signals that common parser has done everything and can be removed from path private var _flvParserProcessed:uint; private var _initialTime:Number = -1; // this is the timestamp derived at start-of-play (offset or not)... what FMS would call "0" private var _seekTime:Number = -1; // this is the timestamp derived at end-of-seek (enhanced or not)... what we need to add to super.time (assuming play started at zero) private var _fileTimeAdjustment:Number = 0; // this is what must be added (IN SECONDS) to the timestamps that come in FLVTags from the file handler to get to the index handler timescale // XXX an event to set the _fileTimestampAdjustment is needed private var _playForDuration:Number = -1; private var _lastValidTimeTime:Number = 0; private var _retryAfterWaitUntil:Number = 0; // millisecond timestamp (as per date.getTime) of when we retry next private var _dvrInfo:DVRInfo = null; private var _unpublishNotifyPending:Boolean = false; private var _signalPlayStartPending:Boolean = false; CONFIG::LOGGING { private static const logger:org.osmf.logging.Logger = org.osmf.logging.Log.getLogger("org.osmf.net.httpstreaming.HTTPNetStream"); private var previouslyLoggedState:String; } } }