no commit message
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
package org.mangui.hls.stream {
|
||||
import org.mangui.hls.constant.HLSPlayStates;
|
||||
import org.mangui.hls.constant.HLSSeekStates;
|
||||
import org.mangui.hls.controller.BufferThresholdController;
|
||||
import org.mangui.hls.demux.ID3Tag;
|
||||
import org.mangui.hls.event.HLSError;
|
||||
import org.mangui.hls.event.HLSEvent;
|
||||
import org.mangui.hls.event.HLSPlayMetrics;
|
||||
import org.mangui.hls.flv.FLVTag;
|
||||
import org.mangui.hls.HLS;
|
||||
import org.mangui.hls.HLSSettings;
|
||||
|
||||
//import by.blooddy.crypto.Base64;
|
||||
import com.hurlant.util.Base64;
|
||||
|
||||
import flash.events.Event;
|
||||
import flash.events.NetStatusEvent;
|
||||
import flash.events.TimerEvent;
|
||||
import flash.net.NetConnection;
|
||||
import flash.net.NetStream;
|
||||
import flash.net.NetStreamAppendBytesAction;
|
||||
import flash.net.NetStreamInfo;
|
||||
import flash.net.NetStreamPlayOptions;
|
||||
import flash.utils.ByteArray;
|
||||
import flash.utils.Timer;
|
||||
|
||||
CONFIG::LOGGING {
|
||||
import org.mangui.hls.utils.Log;
|
||||
}
|
||||
/** Class that overrides standard flash.net.NetStream class, keeps the buffer filled, handles seek and play state
|
||||
*
|
||||
* play state transition :
|
||||
* FROM TO condition
|
||||
* HLSPlayStates.IDLE HLSPlayStates.PLAYING_BUFFERING idle => play()/play2() called
|
||||
* HLSPlayStates.IDLE HLSPlayStates.PAUSED_BUFFERING idle => seek() called
|
||||
* HLSPlayStates.PLAYING_BUFFERING HLSPlayStates.PLAYING buflen > minBufferLength
|
||||
* HLSPlayStates.PAUSED_BUFFERING HLSPlayStates.PAUSED buflen > minBufferLength
|
||||
* HLSPlayStates.PLAYING HLSPlayStates.PLAYING_BUFFERING buflen < lowBufferLength
|
||||
* HLSPlayStates.PAUSED HLSPlayStates.PAUSED_BUFFERING buflen < lowBufferLength
|
||||
*
|
||||
* seek state transition :
|
||||
*
|
||||
* FROM TO condition
|
||||
* HLSSeekStates.IDLE/SEEKED HLSSeekStates.SEEKING play()/play2()/seek() called
|
||||
* HLSSeekStates.SEEKING HLSSeekStates.SEEKED upon first FLV tag appending after seek
|
||||
* HLSSeekStates.SEEKED HLSSeekStates.IDLE upon playback complete or stop() called
|
||||
*/
|
||||
public class HLSNetStream extends NetStream {
|
||||
/** Reference to the framework controller. **/
|
||||
private var _hls : HLS;
|
||||
/** reference to buffer threshold controller */
|
||||
private var _bufferThresholdController : BufferThresholdController;
|
||||
/** FLV Tag Buffer . **/
|
||||
private var _streamBuffer : StreamBuffer;
|
||||
/** Timer used to check buffer and position. **/
|
||||
private var _timer : Timer;
|
||||
/** Current playback state. **/
|
||||
private var _playbackState : String;
|
||||
/** Current seek state. **/
|
||||
private var _seekState : String;
|
||||
/** current playback level **/
|
||||
private var _currentLevel : int;
|
||||
/** Netstream client proxy */
|
||||
private var _client : HLSNetStreamClient;
|
||||
/** skipped fragment duration **/
|
||||
private var _skippedDuration : Number;
|
||||
/** watched duration **/
|
||||
private var _watchedDuration : Number;
|
||||
/** dropped frames counter **/
|
||||
private var _droppedFrames : Number;
|
||||
/** last NetStream.time, used to check if playback is over **/
|
||||
private var _lastNetStreamTime : Number;
|
||||
|
||||
/** Create the buffer. **/
|
||||
public function HLSNetStream(connection : NetConnection, hls : HLS, streamBuffer : StreamBuffer) : void {
|
||||
super(connection);
|
||||
super.bufferTime = 0.1;
|
||||
_hls = hls;
|
||||
_skippedDuration = _watchedDuration = _droppedFrames = _lastNetStreamTime = 0;
|
||||
_bufferThresholdController = new BufferThresholdController(hls);
|
||||
_streamBuffer = streamBuffer;
|
||||
_playbackState = HLSPlayStates.IDLE;
|
||||
_seekState = HLSSeekStates.IDLE;
|
||||
_timer = new Timer(100, 0);
|
||||
_timer.addEventListener(TimerEvent.TIMER, _checkBuffer);
|
||||
_client = new HLSNetStreamClient();
|
||||
_client.registerCallback("onHLSFragmentChange", onHLSFragmentChange);
|
||||
_client.registerCallback("onHLSFragmentSkipped", onHLSFragmentSkipped);
|
||||
_client.registerCallback("onID3Data", onID3Data);
|
||||
super.client = _client;
|
||||
}
|
||||
|
||||
public function onHLSFragmentChange(level : int, seqnum : int, cc : int, duration : Number, audio_only : Boolean, program_date : Number, width : int, height : int, auto_level : Boolean, customTagNb : int, id3TagNb : int, ... tags) : void {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("playing fragment(level/sn/cc):" + level + "/" + seqnum + "/" + cc);
|
||||
}
|
||||
_currentLevel = level;
|
||||
var customTagArray : Array = new Array();
|
||||
var id3TagArray : Array = new Array();
|
||||
for (var i : uint = 0; i < customTagNb; i++) {
|
||||
customTagArray.push(tags[i]);
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("custom tag:" + tags[i]);
|
||||
}
|
||||
}
|
||||
for (i = customTagNb; i < tags.length; i+=4) {
|
||||
var id3Tag : ID3Tag = new ID3Tag(tags[i],tags[i+1],tags[i+2],tags[i+3]);
|
||||
id3TagArray.push(id3Tag);
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("id3 tag:" + id3Tag);
|
||||
}
|
||||
}
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_PLAYING, new HLSPlayMetrics(level, seqnum, cc, duration, audio_only, program_date, width, height, auto_level, customTagArray,id3TagArray)));
|
||||
}
|
||||
|
||||
|
||||
public function onHLSFragmentSkipped(level : int, seqnum : int,duration : Number) : void {
|
||||
CONFIG::LOGGING {
|
||||
Log.warn("skipped fragment(level/sn/duration):" + level + "/" + seqnum + "/" + duration);
|
||||
}
|
||||
_skippedDuration+=duration;
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_SKIPPED, duration));
|
||||
}
|
||||
|
||||
// function is called by SCRIPT in FLV
|
||||
public function onID3Data(data : ByteArray) : void {
|
||||
// we dump the content as base64 to get it to the Javascript in the browser.
|
||||
// The client can use window.atob() to decode the ID3Data.
|
||||
var dump : String = Base64.encodeByteArray(data);
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("id3:" + dump);
|
||||
}
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.ID3_UPDATED, dump));
|
||||
}
|
||||
|
||||
/** timer function, check/update NetStream state, and append tags if needed **/
|
||||
private function _checkBuffer(e : Event) : void {
|
||||
var buffer : Number = this.bufferLength,
|
||||
minBufferLength : Number =_bufferThresholdController.minBufferLength,
|
||||
reachedEnd : Boolean = _streamBuffer.reachedEnd,
|
||||
liveLoadingStalled : Boolean = _streamBuffer.liveLoadingStalled;
|
||||
// Log.info("netstream/total:" + super.bufferLength + "/" + this.bufferLength);
|
||||
|
||||
if (_seekState != HLSSeekStates.SEEKING) {
|
||||
if (_playbackState == HLSPlayStates.PLAYING || _playbackState == HLSPlayStates.PLAYING_BUFFERING) {
|
||||
/* check if play head reached end of stream.
|
||||
this happens when
|
||||
playstate is PLAYING
|
||||
AND last fragment has been loaded,
|
||||
either because we reached end of VOD or because live loading stalled ...
|
||||
AND NetStream is almost empty(less than 2s ... this is just for safety ...)
|
||||
AND StreamBuffer is empty(it means that last fragment tags have been appended in NetStream)
|
||||
AND playhead is not moving anymore (NetStream.time not changing overtime)
|
||||
*/
|
||||
if((reachedEnd || liveLoadingStalled) &&
|
||||
bufferLength <= 2 &&
|
||||
_streamBuffer.bufferLength == 0 &&
|
||||
_lastNetStreamTime &&
|
||||
super.time == _lastNetStreamTime) {
|
||||
// playhead is not moving anymore ... append sequence end.
|
||||
super.appendBytesAction(NetStreamAppendBytesAction.END_SEQUENCE);
|
||||
super.appendBytes(new ByteArray());
|
||||
// have we reached end of playlist ?
|
||||
if(reachedEnd) {
|
||||
// stop timer, report event and switch to IDLE mode.
|
||||
_timer.stop();
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("reached end of VOD playlist, notify playback complete");
|
||||
}
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYBACK_COMPLETE));
|
||||
_setPlaybackState(HLSPlayStates.IDLE);
|
||||
_setSeekState(HLSSeekStates.IDLE);
|
||||
} else {
|
||||
// live loading stalled : flush buffer and restart playback
|
||||
CONFIG::LOGGING {
|
||||
Log.warn("loading stalled: restart playback");
|
||||
}
|
||||
// flush whole buffer before seeking
|
||||
_streamBuffer.flushBuffer();
|
||||
/* seek to force a restart of the playback session */
|
||||
seek(-1);
|
||||
}
|
||||
return;
|
||||
} else if (buffer <= 0.1 && !reachedEnd) {
|
||||
// playing and buffer <= 0.1 and not reachedEnd and not EOS, pause playback
|
||||
super.pause();
|
||||
// low buffer condition and play state. switch to play buffering state
|
||||
_setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
|
||||
}
|
||||
_lastNetStreamTime = super.time;
|
||||
}
|
||||
// if buffer len is below lowBufferLength, get into buffering state
|
||||
if (!reachedEnd && !liveLoadingStalled && buffer < _bufferThresholdController.lowBufferLength) {
|
||||
if (_playbackState == HLSPlayStates.PLAYING) {
|
||||
// low buffer condition and play state. switch to play buffering state
|
||||
_setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
|
||||
} else if (_playbackState == HLSPlayStates.PAUSED) {
|
||||
// low buffer condition and pause state. switch to paused buffering state
|
||||
_setPlaybackState(HLSPlayStates.PAUSED_BUFFERING);
|
||||
}
|
||||
}
|
||||
// if buffer len is above minBufferLength, get out of buffering state
|
||||
if (buffer >= minBufferLength || reachedEnd || liveLoadingStalled) {
|
||||
if (_playbackState == HLSPlayStates.PLAYING_BUFFERING) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("resume playback, minBufferLength/bufferLength:"+minBufferLength.toFixed(2) + "/" + buffer.toFixed(2));
|
||||
}
|
||||
// resume playback in case it was paused, this can happen if buffer was in really low condition (less than 0.1s)
|
||||
super.resume();
|
||||
_setPlaybackState(HLSPlayStates.PLAYING);
|
||||
} else if (_playbackState == HLSPlayStates.PAUSED_BUFFERING) {
|
||||
_setPlaybackState(HLSPlayStates.PAUSED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Return the current playback state. **/
|
||||
public function get playbackState() : String {
|
||||
return _playbackState;
|
||||
}
|
||||
|
||||
/** Return the current seek state. **/
|
||||
public function get seekState() : String {
|
||||
return _seekState;
|
||||
}
|
||||
|
||||
/** Return the current playback quality level **/
|
||||
public function get currentLevel() : int {
|
||||
return _currentLevel;
|
||||
}
|
||||
|
||||
/** append tags to NetStream **/
|
||||
public function appendTags(tags : Vector.<FLVTag>) : void {
|
||||
if (_seekState == HLSSeekStates.SEEKING) {
|
||||
/* this is our first injection after seek(),
|
||||
let's flush netstream now
|
||||
this is to avoid black screen during seek command */
|
||||
_watchedDuration += super.time;
|
||||
_droppedFrames += super.info.droppedFrames;
|
||||
_skippedDuration = 0;
|
||||
super.close();
|
||||
|
||||
// useHardwareDecoder was added in FP11.1, but this allows us to include the option in all builds
|
||||
try {
|
||||
super['useHardwareDecoder'] = HLSSettings.useHardwareVideoDecoder;
|
||||
} catch(e : Error) {
|
||||
// Ignore errors, we're running in FP < 11.1
|
||||
}
|
||||
|
||||
super.play(null);
|
||||
super.appendBytesAction(NetStreamAppendBytesAction.RESET_SEEK);
|
||||
// immediatly pause NetStream, it will be resumed when enough data will be buffered in the NetStream
|
||||
super.pause();
|
||||
// var otherCounter : int = 0;
|
||||
// for each (var tagBuffer0 : FLVTag in tags) {
|
||||
// switch(tagBuffer0.type) {
|
||||
// case FLVTag.AAC_HEADER:
|
||||
// case FLVTag.AVC_HEADER:
|
||||
// case FLVTag.DISCONTINUITY:
|
||||
// case FLVTag.METADATA:
|
||||
// CONFIG::LOGGING {
|
||||
// Log.info('inject type/dts/pts:' + tagBuffer0.typeString + '/' + tagBuffer0.dts + '/' + tagBuffer0.pts);
|
||||
// }
|
||||
// break;
|
||||
// default:
|
||||
// CONFIG::LOGGING {
|
||||
// if(otherCounter++< 5) {
|
||||
// Log.info('inject type/dts/pts:' + tagBuffer0.typeString + '/' + tagBuffer0.dts + '/' + tagBuffer0.pts);
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// append all tags
|
||||
//var otherCounter : int = 0;
|
||||
for each (var tagBuffer : FLVTag in tags) {
|
||||
// switch(tagBuffer.type) {
|
||||
// case FLVTag.AAC_HEADER:
|
||||
// case FLVTag.AVC_HEADER:
|
||||
// case FLVTag.DISCONTINUITY:
|
||||
// case FLVTag.METADATA:
|
||||
// otherCounter = 0;
|
||||
// CONFIG::LOGGING {
|
||||
// Log.info('inject type/dts/pts:' + tagBuffer.typeString + '/' + tagBuffer.dts + '/' + tagBuffer.pts);
|
||||
// }
|
||||
// break;
|
||||
// default:
|
||||
// CONFIG::LOGGING {
|
||||
// if(otherCounter++< 5) {
|
||||
// Log.info('inject type/dts/pts:' + tagBuffer.typeString + '/' + tagBuffer.dts + '/' + tagBuffer.pts);
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// CONFIG::LOGGING {
|
||||
// Log.debug2('inject type/dts/pts:' + tagBuffer.typeString + '/' + tagBuffer.dts + '/' + tagBuffer.pts);
|
||||
// }
|
||||
try {
|
||||
if (tagBuffer.type == FLVTag.DISCONTINUITY) {
|
||||
super.appendBytesAction(NetStreamAppendBytesAction.RESET_BEGIN);
|
||||
super.appendBytes(FLVTag.getHeader());
|
||||
}
|
||||
super.appendBytes(tagBuffer.data);
|
||||
} catch (error : Error) {
|
||||
var hlsError : HLSError = new HLSError(HLSError.TAG_APPENDING_ERROR, null, tagBuffer.type + ": " + error.message);
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
||||
}
|
||||
}
|
||||
if (_seekState == HLSSeekStates.SEEKING) {
|
||||
// dispatch event to mimic NetStream behaviour
|
||||
dispatchEvent(new NetStatusEvent(NetStatusEvent.NET_STATUS, false, false, {code:"NetStream.Seek.Notify", level:"status"}));
|
||||
_setSeekState(HLSSeekStates.SEEKED);
|
||||
}
|
||||
}
|
||||
|
||||
/** Change playback state. **/
|
||||
private function _setPlaybackState(state : String) : void {
|
||||
if (state != _playbackState) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug('[PLAYBACK_STATE] from ' + _playbackState + ' to ' + state);
|
||||
}
|
||||
_playbackState = state;
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYBACK_STATE, _playbackState));
|
||||
}
|
||||
}
|
||||
|
||||
/** Change seeking state. **/
|
||||
private function _setSeekState(state : String) : void {
|
||||
if (state != _seekState) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug('[SEEK_STATE] from ' + _seekState + ' to ' + state);
|
||||
}
|
||||
_seekState = state;
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.SEEK_STATE, _seekState));
|
||||
}
|
||||
}
|
||||
|
||||
/* also include skipped duration in get time() so that play position will match fragment position */
|
||||
override public function get time() : Number {
|
||||
return super.time+_skippedDuration;
|
||||
}
|
||||
|
||||
/* return nb of dropped Frames since session started */
|
||||
public function get droppedFrames() : Number {
|
||||
return super.info.droppedFrames + _droppedFrames;
|
||||
}
|
||||
|
||||
/** Return total watched time **/
|
||||
public function get watched() : Number {
|
||||
return super.time + _watchedDuration;
|
||||
}
|
||||
|
||||
override public function play(...args) : void {
|
||||
var _playStart : Number;
|
||||
if (args.length >= 2) {
|
||||
_playStart = Number(args[1]);
|
||||
} else {
|
||||
_playStart = -1;
|
||||
}
|
||||
CONFIG::LOGGING {
|
||||
Log.info("HLSNetStream:play(" + _playStart + ")");
|
||||
}
|
||||
seek(_playStart);
|
||||
_setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
|
||||
}
|
||||
|
||||
override public function play2(param : NetStreamPlayOptions) : void {
|
||||
CONFIG::LOGGING {
|
||||
Log.info("HLSNetStream:play2(" + param.start + ")");
|
||||
}
|
||||
seek(param.start);
|
||||
_setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
|
||||
}
|
||||
|
||||
/** Pause playback. **/
|
||||
override public function pause() : void {
|
||||
CONFIG::LOGGING {
|
||||
Log.info("HLSNetStream:pause");
|
||||
}
|
||||
if (_playbackState == HLSPlayStates.PLAYING) {
|
||||
super.pause();
|
||||
_setPlaybackState(HLSPlayStates.PAUSED);
|
||||
} else if (_playbackState == HLSPlayStates.PLAYING_BUFFERING) {
|
||||
super.pause();
|
||||
_setPlaybackState(HLSPlayStates.PAUSED_BUFFERING);
|
||||
}
|
||||
}
|
||||
|
||||
/** Resume playback. **/
|
||||
override public function resume() : void {
|
||||
CONFIG::LOGGING {
|
||||
Log.info("HLSNetStream:resume");
|
||||
}
|
||||
if (_playbackState == HLSPlayStates.PAUSED) {
|
||||
super.resume();
|
||||
_setPlaybackState(HLSPlayStates.PLAYING);
|
||||
} else if (_playbackState == HLSPlayStates.PAUSED_BUFFERING) {
|
||||
// dont resume NetStream here, it will be resumed by Timer. this avoids resuming playback while seeking is in progress
|
||||
_setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
|
||||
}
|
||||
}
|
||||
|
||||
/** get Buffer Length **/
|
||||
override public function get bufferLength() : Number {
|
||||
return netStreamBufferLength + _streamBuffer.bufferLength;
|
||||
}
|
||||
|
||||
/** get Back Buffer Length **/
|
||||
override public function get backBufferLength() : Number {
|
||||
return _streamBuffer.backBufferLength;
|
||||
}
|
||||
|
||||
public function get netStreamBufferLength() : Number {
|
||||
if (_seekState == HLSSeekStates.SEEKING) {
|
||||
return 0;
|
||||
} else {
|
||||
return super.bufferLength;
|
||||
}
|
||||
}
|
||||
|
||||
/** Start playing data in the buffer. **/
|
||||
override public function seek(position : Number) : void {
|
||||
CONFIG::LOGGING {
|
||||
Log.info("HLSNetStream:seek(" + position + ")");
|
||||
}
|
||||
_streamBuffer.seek(position);
|
||||
_setSeekState(HLSSeekStates.SEEKING);
|
||||
/* if HLS playback state was in PAUSED or IDLE state before seeking,
|
||||
* switch to paused buffering state
|
||||
* otherwise, switch to playing buffering state
|
||||
*/
|
||||
switch(_playbackState) {
|
||||
case HLSPlayStates.IDLE:
|
||||
case HLSPlayStates.PAUSED:
|
||||
case HLSPlayStates.PAUSED_BUFFERING:
|
||||
_setPlaybackState(HLSPlayStates.PAUSED_BUFFERING);
|
||||
break;
|
||||
case HLSPlayStates.PLAYING:
|
||||
case HLSPlayStates.PLAYING_BUFFERING:
|
||||
_setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
/* always pause NetStream while seeking, even if we are in play state
|
||||
* in that case, NetStream will be resumed during next call to appendTags()
|
||||
*/
|
||||
super.pause();
|
||||
_timer.start();
|
||||
}
|
||||
|
||||
public override function set client(client : Object) : void {
|
||||
_client.delegate = client;
|
||||
}
|
||||
|
||||
public override function get client() : Object {
|
||||
return _client.delegate;
|
||||
}
|
||||
|
||||
/** Stop playback. **/
|
||||
override public function close() : void {
|
||||
CONFIG::LOGGING {
|
||||
Log.info("HLSNetStream:close");
|
||||
}
|
||||
super.close();
|
||||
_watchedDuration = _skippedDuration = _lastNetStreamTime = _droppedFrames = 0;
|
||||
_streamBuffer.stop();
|
||||
_timer.stop();
|
||||
_setPlaybackState(HLSPlayStates.IDLE);
|
||||
_setSeekState(HLSSeekStates.IDLE);
|
||||
}
|
||||
|
||||
public function dispose_() : void {
|
||||
close();
|
||||
_timer.removeEventListener(TimerEvent.TIMER, _checkBuffer);
|
||||
_bufferThresholdController.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user