From c933148a8bac6d9fcc799239bf5866f30a7b2989 Mon Sep 17 00:00:00 2001 From: Neil Rackett Date: Tue, 19 Apr 2016 09:16:47 +0100 Subject: [PATCH 1/9] Added support for alternative build path to build.bat via first parameter (%1); added build-temp.bat to build SWF/SWC into /bin-temp folder (ignored by repo) for testing --- build/build-temp.bat | 5 +++++ build/build.bat | 44 ++++++++++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 build/build-temp.bat diff --git a/build/build-temp.bat b/build/build-temp.bat new file mode 100644 index 00000000..0001b30e --- /dev/null +++ b/build/build-temp.bat @@ -0,0 +1,5 @@ +@echo off + +REM Using the -temp switch builds the SWF/SWC files into build-temp so they're ignore by git + +build ..\bin-temp \ No newline at end of file diff --git a/build/build.bat b/build/build.bat index 4444da2b..c776a4ca 100644 --- a/build/build.bat +++ b/build/build.bat @@ -7,6 +7,14 @@ IF "%JAVA_HOME%"=="" ( echo Please set JAVA_HOME to the path of the Java SDK && cls +IF "%1"=="" (set BIN_PATH=..\bin) ELSE (set BIN_PATH=%1) + +echo Building SWF/SWC files into %BIN_PATH% +echo. + +if not exist "%BIN_PATH%\release" mkdir "%BIN_PATH%\release" +if not exist "%BIN_PATH%\debug" mkdir "%BIN_PATH%\debug" + set OPT_DEBUG=-use-network=false ^ -debug=true ^ -library-path+=..\lib\blooddy_crypto.swc ^ @@ -22,7 +30,7 @@ echo Compiling bin\debug\flashls.swc... call "%FLEX_HOME%\bin\compc" ^ %OPT_DEBUG% ^ -include-sources ..\src\org\mangui\hls ^ - -output ..\bin\debug\flashls.swc ^ + -output %BIN_PATH%\debug\flashls.swc ^ -target-player=10.1 echo. @@ -30,80 +38,80 @@ echo Compiling bin\release\flashls.swc... call "%FLEX_HOME%\bin\compc" ^ %OPT_RELEASE% ^ -include-sources ..\src\org\mangui\hls ^ - -output ..\bin\release\flashls.swc ^ + -output %BIN_PATH%\release\flashls.swc ^ -target-player=10.1 echo. echo Compiling bin\release\flashlsChromeless.swf... call "%FLEX_HOME%\bin\mxmlc" ..\src\org\mangui\chromeless\ChromelessPlayer.as ^ -source-path+=..\src ^ - -o ..\bin\release\flashlsChromeless.swf ^ + -o %BIN_PATH%\release\flashlsChromeless.swf ^ %OPT_RELEASE% ^ -target-player=11.1 ^ -default-size 480 270 ^ -default-background-color=0x000000 -.\add-opt-in.py ..\bin\release\flashlsChromeless.swf +.\add-opt-in.py %BIN_PATH%\release\flashlsChromeless.swf echo. echo Compiling bin\debug\flashlsChromeless.swf... call "%FLEX_HOME%\bin\mxmlc" ..\src\org\mangui\chromeless\ChromelessPlayer.as ^ -source-path+=..\src ^ - -o ..\bin\debug\flashlsChromeless.swf ^ + -o %BIN_PATH%\debug\flashlsChromeless.swf ^ %OPT_DEBUG% ^ -target-player=11.1 ^ -default-size 480 270 ^ -default-background-color=0x000000 -REM .\add-opt-in.py ..\bin\debug\flashlsChromeless.swf +REM .\add-opt-in.py %BIN_PATH%\debug\flashlsChromeless.swf echo. echo Compiling bin\release\flashlsFlowPlayer.swf... call "%FLEX_HOME%\bin\mxmlc" ..\src\org\mangui\flowplayer\HLSPluginFactory.as ^ - -source-path+=..\src -o ..\bin\release\flashlsFlowPlayer.swf ^ + -source-path+=..\src -o %BIN_PATH%\release\flashlsFlowPlayer.swf ^ %OPT_RELEASE% ^ -library-path+=..\lib\flowplayer ^ -load-externs=..\lib\flowplayer\flowplayer-classes.xml ^ -target-player=11.1 -REM .\add-opt-in.py ..\bin\release\flashlsFlowPlayer.swf +REM .\add-opt-in.py %BIN_PATH%\release\flashlsFlowPlayer.swf echo. echo Compiling bin\debug\flashlsFlowPlayer.swf... call "%FLEX_HOME%\bin\mxmlc" ..\src\org\mangui\flowplayer\HLSPluginFactory.as ^ - -source-path+=..\src -o ..\bin\debug\flashlsFlowPlayer.swf ^ + -source-path+=..\src -o %BIN_PATH%\debug\flashlsFlowPlayer.swf ^ %OPT_DEBUG% ^ -library-path+=..\lib\flowplayer ^ -load-externs=..\lib\flowplayer\flowplayer-classes.xml ^ -target-player=11.1 -.\add-opt-in.py ..\bin\debug\flashlsFlowPlayer.swf +.\add-opt-in.py %BIN_PATH%\debug\flashlsFlowPlayer.swf echo. echo Compiling bin\release\flashlsOSMF.swf... call "%FLEX_HOME%\bin\mxmlc" ..\src\org\mangui\osmf\plugins\HLSDynamicPlugin.as ^ -source-path+=..\src ^ - -o ..\bin\release\flashlsOSMF.swf ^ + -o %BIN_PATH%\release\flashlsOSMF.swf ^ %OPT_RELEASE% ^ -library-path+=..\lib\osmf ^ -load-externs=..\lib\osmf\exclude-sources.xml ^ -target-player=10.1 -.\add-opt-in.py ..\bin\release\flashlsOSMF.swf +.\add-opt-in.py %BIN_PATH%\release\flashlsOSMF.swf echo. echo Compiling bin\debug\flashlsOSMF.swf... call "%FLEX_HOME%\bin\mxmlc" ..\src\org\mangui\osmf\plugins\HLSDynamicPlugin.as ^ -source-path+=..\src ^ - -o ..\bin\debug\flashlsOSMF.swf ^ + -o %BIN_PATH%\debug\flashlsOSMF.swf ^ %OPT_DEBUG% ^ -library-path+=..\lib\osmf ^ -load-externs=..\lib\osmf\exclude-sources.xml ^ -target-player=10.1 -.\add-opt-in.py ..\bin\debug\flashlsOSMF.swf +.\add-opt-in.py %BIN_PATH%\debug\flashlsOSMF.swf echo. echo Compiling bin\release\flashlsOSMF.swc... call "%FLEX_HOME%\bin\compc" ^ -include-sources ..\src\org\mangui\osmf ^ - -output ..\bin\release\flashlsOSMF.swc ^ + -output %BIN_PATH%\release\flashlsOSMF.swc ^ %OPT_RELEASE% ^ - -library-path+=..\bin\release\flashls.swc ^ + -library-path+=%BIN_PATH%\release\flashls.swc ^ -library-path+=..\lib\osmf ^ -target-player=10.1 ^ -external-library-path+=..\lib\osmf @@ -112,9 +120,9 @@ echo. echo Compiling bin\debug\flashlsOSMF.swc... call "%FLEX_HOME%\bin\compc" ^ -include-sources ..\src\org\mangui\osmf ^ - -output ..\bin\debug\flashlsOSMF.swc ^ + -output %BIN_PATH%\debug\flashlsOSMF.swc ^ %OPT_DEBUG% ^ - -library-path+=..\bin\debug\flashls.swc ^ + -library-path+=%BIN_PATH%\debug\flashls.swc ^ -library-path+=..\lib\osmf ^ -target-player=10.1 ^ -external-library-path+=..\lib\osmf From 31db762a54592656ffc3ecbf6f14d57cb235864f Mon Sep 17 00:00:00 2001 From: Neil Rackett Date: Tue, 19 Apr 2016 09:20:03 +0100 Subject: [PATCH 2/9] Fixed NetStream client passthrough (arguments now passed in original format instead of single array); added support for OSMF NetClient to HLSMediaElement --- src/org/mangui/hls/stream/HLSNetStreamClient.as | 6 +++--- src/org/mangui/osmf/plugins/HLSMediaElement.as | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/org/mangui/hls/stream/HLSNetStreamClient.as b/src/org/mangui/hls/stream/HLSNetStreamClient.as index 39e424af..9f3f92f6 100644 --- a/src/org/mangui/hls/stream/HLSNetStreamClient.as +++ b/src/org/mangui/hls/stream/HLSNetStreamClient.as @@ -31,13 +31,13 @@ var r : * = null; if (_callbacks && _callbacks.hasOwnProperty(methodName)) { - r = _callbacks[methodName](args); + r = _callbacks[methodName].apply(_callbacks, args); } if (_delegate && _delegate.hasOwnProperty(methodName)) { - r = _delegate[methodName](args); + r = _delegate[methodName].apply(_delegate, args); } - + return r; } diff --git a/src/org/mangui/osmf/plugins/HLSMediaElement.as b/src/org/mangui/osmf/plugins/HLSMediaElement.as index 1688943b..a13ccdca 100644 --- a/src/org/mangui/osmf/plugins/HLSMediaElement.as +++ b/src/org/mangui/osmf/plugins/HLSMediaElement.as @@ -11,10 +11,13 @@ import org.mangui.osmf.plugins.traits.*; import org.mangui.osmf.plugins.utils.ErrorManager; + import org.osmf.events.MediaError; + import org.osmf.events.MediaErrorEvent; import org.osmf.media.LoadableElementBase; import org.osmf.media.MediaElement; import org.osmf.media.MediaResourceBase; import org.osmf.media.videoClasses.VideoSurface; + import org.osmf.net.NetClient; import org.osmf.net.NetStreamAudioTrait; import org.osmf.net.StreamType; import org.osmf.net.StreamingURLResource; @@ -28,8 +31,6 @@ import org.osmf.traits.SeekTrait; import org.osmf.traits.TimeTrait; import org.osmf.utils.OSMFSettings; - import org.osmf.events.MediaError; - import org.osmf.events.MediaErrorEvent; CONFIG::LOGGING { import org.mangui.hls.utils.Log; @@ -44,11 +45,16 @@ public function HLSMediaElement(resource : MediaResourceBase, hls : HLS, duration : Number) { _hls = hls; + _hls.stream.client = new NetClient(); _defaultduration = duration; super(resource, new HLSNetLoader(hls)); _hls.addEventListener(HLSEvent.ERROR, _errorHandler); } - + + public function get client():Object { + return _hls.stream.client; + } + protected function createVideo() : Video { return new Video(); } From 7097f7054260124ffeffe41c9d7332b2c5eb98f7 Mon Sep 17 00:00:00 2001 From: Neil Rackett Date: Tue, 19 Apr 2016 09:22:34 +0100 Subject: [PATCH 3/9] Updated comments in build-temp.bat --- build/build-temp.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/build-temp.bat b/build/build-temp.bat index 0001b30e..c88d7670 100644 --- a/build/build-temp.bat +++ b/build/build-temp.bat @@ -1,5 +1,5 @@ @echo off -REM Using the -temp switch builds the SWF/SWC files into build-temp so they're ignore by git +REM Builds the SWF/SWC files into build-temp so they're ignore by git build ..\bin-temp \ No newline at end of file From e14d1ad405803d9429a461a10400b6acff3149c7 Mon Sep 17 00:00:00 2001 From: Neil Rackett Date: Tue, 19 Apr 2016 17:58:01 +0100 Subject: [PATCH 4/9] Initialized _levels property of FragmentLoader to prevent crash after "I/O Error while trying to load Playlist" --- src/org/mangui/hls/loader/FragmentLoader.as | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/mangui/hls/loader/FragmentLoader.as b/src/org/mangui/hls/loader/FragmentLoader.as index 5f64f9d1..184df31a 100644 --- a/src/org/mangui/hls/loader/FragmentLoader.as +++ b/src/org/mangui/hls/loader/FragmentLoader.as @@ -48,7 +48,7 @@ package org.mangui.hls.loader { /** next level (-1 if not defined yet) **/ private var _levelNext : int = -1; /** Reference to the manifest levels. **/ - private var _levels : Vector.; + private var _levels : Vector. = new Vector.(); // Initializing it here prevents crash after IOError loading playlist /** Util for loading the fragment. **/ private var _fragstreamloader : URLStream; /** Util for loading the key. **/ From fa1e77f747900b7e8809d08fa7281bf0a6b21f42 Mon Sep 17 00:00:00 2001 From: Neil Rackett Date: Tue, 19 Apr 2016 18:36:10 +0100 Subject: [PATCH 5/9] Updated fix for crash caused by playlist IO error --- src/org/mangui/hls/loader/FragmentLoader.as | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/mangui/hls/loader/FragmentLoader.as b/src/org/mangui/hls/loader/FragmentLoader.as index 184df31a..443b7ff2 100644 --- a/src/org/mangui/hls/loader/FragmentLoader.as +++ b/src/org/mangui/hls/loader/FragmentLoader.as @@ -235,7 +235,7 @@ package org.mangui.hls.loader { // check if we received playlist for choosen level. if live playlist, ensure that new playlist has been refreshed // to avoid loading outdated fragments - if ((_levels[level].fragments.length == 0) || (_hls.type == HLSTypes.LIVE && _levelLastLoaded != level)) { + if (_levels.length < level+1 || _levels[level].fragments.length == 0 || (_hls.type == HLSTypes.LIVE && _levelLastLoaded != level)) { // playlist not yet received CONFIG::LOGGING { Log.debug("_checkLoading : playlist not received for level:" + level); From 2e0c2d011b5b6f43e419c2d182e2b3034e6f39bf Mon Sep 17 00:00:00 2001 From: Neil Rackett Date: Mon, 25 Apr 2016 11:13:34 +0100 Subject: [PATCH 6/9] Fixed callback scoping in NetStreamClient --- src/org/mangui/hls/stream/HLSNetStreamClient.as | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/org/mangui/hls/stream/HLSNetStreamClient.as b/src/org/mangui/hls/stream/HLSNetStreamClient.as index 9f3f92f6..22620eab 100644 --- a/src/org/mangui/hls/stream/HLSNetStreamClient.as +++ b/src/org/mangui/hls/stream/HLSNetStreamClient.as @@ -28,16 +28,13 @@ } override flash_proxy function callProperty(methodName : *, ... args) : * { - var r : * = null; - + var r : *; if (_callbacks && _callbacks.hasOwnProperty(methodName)) { - r = _callbacks[methodName].apply(_callbacks, args); + r = _callbacks[methodName].apply(null, args); } - if (_delegate && _delegate.hasOwnProperty(methodName)) { - r = _delegate[methodName].apply(_delegate, args); + r = _delegate[methodName].apply(null, args); } - return r; } @@ -46,11 +43,9 @@ if (_callbacks && _callbacks.hasOwnProperty(name)) { r = _callbacks[name]; } - if (_delegate && _delegate.hasOwnProperty(name)) { r = _delegate[name]; } - return r; } From e51231480514652cb8fb6b73a9e42942be2c5af8 Mon Sep 17 00:00:00 2001 From: Neil Rackett Date: Wed, 11 May 2016 17:50:42 +0100 Subject: [PATCH 7/9] Implemented support for #EXT-X-MEDIA:TYPE=SUBTITLES tracks (LIVE and VOD) --- src/org/mangui/hls/HLS.as | 29 +- src/org/mangui/hls/HLSSettings.as | 33 ++ src/org/mangui/hls/constant/HLSLoaderTypes.as | 8 +- .../controller/SubtitlesTrackController.as | 243 +++++++++++++ src/org/mangui/hls/demux/TSDemuxer.as | 4 +- src/org/mangui/hls/event/HLSEvent.as | 29 +- src/org/mangui/hls/loader/LevelLoader.as | 32 +- .../hls/loader/SubtitlesFragmentLoader.as | 330 ++++++++++++++++++ .../mangui/hls/loader/SubtitlesLevelLoader.as | 205 +++++++++++ src/org/mangui/hls/model/Level.as | 2 + src/org/mangui/hls/model/Subtitle.as | 159 +++++++++ src/org/mangui/hls/model/SubtitlesTrack.as | 35 ++ src/org/mangui/hls/playlist/Manifest.as | 30 +- .../hls/playlist/SubtitlesPlaylistTrack.as | 30 ++ src/org/mangui/hls/stream/HLSNetStream.as | 51 ++- .../mangui/hls/stream/HLSNetStreamClient.as | 38 +- src/org/mangui/hls/stream/StreamBuffer.as | 27 +- src/org/mangui/hls/utils/StringUtil.as | 13 + src/org/mangui/hls/utils/WebVTTParser.as | 111 ++++++ src/org/mangui/hls/utils/hls_internal.as | 4 + .../plugins/traits/HLSClosedCaptionsTrait.as | 2 +- 21 files changed, 1357 insertions(+), 58 deletions(-) create mode 100644 src/org/mangui/hls/controller/SubtitlesTrackController.as create mode 100644 src/org/mangui/hls/loader/SubtitlesFragmentLoader.as create mode 100644 src/org/mangui/hls/loader/SubtitlesLevelLoader.as create mode 100644 src/org/mangui/hls/model/Subtitle.as create mode 100644 src/org/mangui/hls/model/SubtitlesTrack.as create mode 100644 src/org/mangui/hls/playlist/SubtitlesPlaylistTrack.as create mode 100644 src/org/mangui/hls/utils/WebVTTParser.as create mode 100644 src/org/mangui/hls/utils/hls_internal.as diff --git a/src/org/mangui/hls/HLS.as b/src/org/mangui/hls/HLS.as index 53aaed73..062dc1b9 100644 --- a/src/org/mangui/hls/HLS.as +++ b/src/org/mangui/hls/HLS.as @@ -10,14 +10,18 @@ package org.mangui.hls { import flash.net.NetStream; import flash.net.URLLoader; import flash.net.URLStream; + import org.mangui.hls.constant.HLSSeekStates; import org.mangui.hls.controller.AudioTrackController; import org.mangui.hls.controller.LevelController; + import org.mangui.hls.controller.SubtitlesTrackController; import org.mangui.hls.event.HLSEvent; import org.mangui.hls.loader.AltAudioLevelLoader; import org.mangui.hls.loader.LevelLoader; + import org.mangui.hls.loader.SubtitlesLevelLoader; import org.mangui.hls.model.AudioTrack; import org.mangui.hls.model.Level; + import org.mangui.hls.model.SubtitlesTrack; import org.mangui.hls.playlist.AltAudioTrack; import org.mangui.hls.stream.HLSNetStream; import org.mangui.hls.stream.StreamBuffer; @@ -28,9 +32,11 @@ package org.mangui.hls { /** Class that manages the streaming process. **/ public class HLS extends EventDispatcher { private var _levelLoader : LevelLoader; + private var _levelController : LevelController; private var _altAudioLevelLoader : AltAudioLevelLoader; private var _audioTrackController : AudioTrackController; - private var _levelController : LevelController; + private var _subtitlesLevelLoader : SubtitlesLevelLoader; + private var _subtitlesTrackController : SubtitlesTrackController; private var _streamBuffer : StreamBuffer; /** HLS NetStream **/ private var _hlsNetStream : HLSNetStream; @@ -51,6 +57,8 @@ package org.mangui.hls { _audioTrackController = new AudioTrackController(this); _levelController = new LevelController(this); _streamBuffer = new StreamBuffer(this, _audioTrackController, _levelController); + _subtitlesLevelLoader = new SubtitlesLevelLoader(this, _levelLoader); + _subtitlesTrackController = new SubtitlesTrackController(this, _streamBuffer, _levelLoader); _hlsURLStream = URLStream as Class; _hlsURLLoader = URLLoader as Class; // default loader @@ -85,12 +93,15 @@ package org.mangui.hls { _levelLoader.dispose(); _altAudioLevelLoader.dispose(); _audioTrackController.dispose(); + _subtitlesLevelLoader.dispose(); + _subtitlesTrackController.dispose(); _levelController.dispose(); _hlsNetStream.dispose_(); _streamBuffer.dispose(); _levelLoader = null; _altAudioLevelLoader = null; _audioTrackController = null; + _subtitlesLevelLoader = null; _levelController = null; _hlsNetStream = null; _client = null; @@ -209,7 +220,6 @@ package org.mangui.hls { return _hlsNetStream.watched; }; - /** Return the total nb of dropped video frames since last call to hls.load() **/ public function get droppedFrames() : Number { return _hlsNetStream.droppedFrames; @@ -240,6 +250,21 @@ package org.mangui.hls { _client = value; } + /** get subtitles tracks list**/ + public function get subtitlesTracks() : Vector. { + return _subtitlesTrackController.subtitlesTracks; + }; + + /** get index of the selected subtitles track (index in subtitles track lists) **/ + public function get subtitlesTrack() : int { + return _subtitlesTrackController.subtitlesTrack; + }; + + /** select a subtitles track, based on its index in subtitles track lists**/ + public function set subtitlesTrack(val : int) : void { + _subtitlesTrackController.subtitlesTrack = val; + } + /** get audio tracks list**/ public function get audioTracks() : Vector. { return _audioTrackController.audioTracks; diff --git a/src/org/mangui/hls/HLSSettings.as b/src/org/mangui/hls/HLSSettings.as index 7011f4b1..d189f113 100644 --- a/src/org/mangui/hls/HLSSettings.as +++ b/src/org/mangui/hls/HLSSettings.as @@ -340,6 +340,39 @@ package org.mangui.hls { * Default is -1 */ public static var seekFromLevel : Number = -1; + + /** + * subtitlesAutoSelectDefault + * + * Should a subtitles track automatically be selected if it is flagged + * as DEFAULT=YES? + * + * Default is false + */ + public static var subtitlesAutoSelectDefault:Boolean = false; + + /** + * subtitlesAutoSelect + * + * Should a subtitles track automatically be selected if it is flagged + * as AUTOSELECT=YES and the language matches the current system locale? + * If true, these subtitles will always be selected in preference to + * default subtitles. + * + * Default is true + */ + public static var subtitlesAutoSelect:Boolean = true; + + /** + * subtitlesAutoSelectForced + * + * Should a subtitles track automatically be selected is it is flagged + * as FORCED=YES? If true, forced subtitles will always be selected + * in preference to all others. + * + * Default is true + */ + public static var subtitlesAutoSelectForced:Boolean = true; /** * useHardwareVideoDecoder diff --git a/src/org/mangui/hls/constant/HLSLoaderTypes.as b/src/org/mangui/hls/constant/HLSLoaderTypes.as index a3f6543a..d0a0f20e 100644 --- a/src/org/mangui/hls/constant/HLSLoaderTypes.as +++ b/src/org/mangui/hls/constant/HLSLoaderTypes.as @@ -10,9 +10,13 @@ public static const LEVEL_MAIN : int = 1; // playlist / level loader public static const LEVEL_ALTAUDIO : int = 2; + // playlist / level loader + public static const LEVEL_SUBTITLES : int = 4; // main fragment loader - public static const FRAGMENT_MAIN : int = 3; + public static const FRAGMENT_MAIN : int = 5; // alt audio fragment loader - public static const FRAGMENT_ALTAUDIO : int = 4; + public static const FRAGMENT_ALTAUDIO : int = 6; + // subtitles fragment loader + public static const FRAGMENT_SUBTITLES : int = 7; } } \ No newline at end of file diff --git a/src/org/mangui/hls/controller/SubtitlesTrackController.as b/src/org/mangui/hls/controller/SubtitlesTrackController.as new file mode 100644 index 00000000..bb5bc937 --- /dev/null +++ b/src/org/mangui/hls/controller/SubtitlesTrackController.as @@ -0,0 +1,243 @@ +/* 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.controller { + import flash.system.Capabilities; + import flash.utils.setTimeout; + + import org.mangui.hls.HLS; + import org.mangui.hls.HLSSettings; + import org.mangui.hls.event.HLSEvent; + import org.mangui.hls.loader.LevelLoader; + import org.mangui.hls.model.SubtitlesTrack; + import org.mangui.hls.playlist.SubtitlesPlaylistTrack; + import org.mangui.hls.stream.HLSNetStream; + import org.mangui.hls.stream.StreamBuffer; + import org.mangui.hls.utils.hls_internal; + + CONFIG::LOGGING { + import org.mangui.hls.utils.Log; + } + + /** + * Class that handles subtitles tracks, based on alternative audio controller + * @author Neil Rackett + */ + public class SubtitlesTrackController { + /** Reference to the HLS controller. **/ + private var _hls : HLS; + /** Reference to the HLS level loader. **/ + private var _levelLoader : LevelLoader; + /** stream buffer instance **/ + private var _streamBuffer : StreamBuffer; + /** list of subtitles tracks from Manifest, matching with current level **/ + private var _subtitlesTracksFromManifest : Vector.; + /** merged subtitles tracks list **/ + private var _subtitlesTracks : Vector.; + /** current subtitles track id **/ + private var _subtitlesTrackId : int; + /** default subtitles track id **/ + private var _defaultTrackId : int; + /** forced subtitles track id **/ + private var _forcedTrackId : int; + + use namespace hls_internal; + + public function SubtitlesTrackController(hls : HLS, streamBuffer : StreamBuffer, levelLoader : LevelLoader) { + _hls = hls; + _hls.addEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler); + _hls.addEventListener(HLSEvent.LEVEL_LOADED, _levelLoadedHandler); + + _streamBuffer = streamBuffer; + _levelLoader = levelLoader; + + _subtitlesTracks = new Vector.; + _subtitlesTrackId = -1; + _defaultTrackId = -1; + _forcedTrackId = -1; + } + + public function dispose() : void { + _hls.removeEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler); + _hls.removeEventListener(HLSEvent.LEVEL_LOADED, _levelLoadedHandler); + } + + public function set subtitlesTrack(num : int) : void { + if (_subtitlesTrackId != num) { + _subtitlesTrackId = num; + var ev : HLSEvent = new HLSEvent(HLSEvent.SUBTITLES_TRACK_SWITCH); + ev.subtitlesTrack = _subtitlesTrackId; + _hls.dispatchEvent(ev); + CONFIG::LOGGING { + Log.info('Setting subtitles track to ' + num); + } + } + } + + public function get subtitlesTrack() : int { + return _subtitlesTrackId; + } + + public function get subtitlesTracks() : Vector. { + return _subtitlesTracks; + } + + /** + * Reset subtitles tracks + */ + private function _manifestLoadedHandler(event : HLSEvent) : void { + _defaultTrackId = -1; + _forcedTrackId = -1; + _subtitlesTrackId = -1; + _subtitlesTracksFromManifest = new Vector.(); + _subtitlesTracks = new Vector.(); + _updateSubtitlesTrackForLevel(_hls.loadLevel); + }; + + /** Store the manifest data. **/ + private function _levelLoadedHandler(event : HLSEvent) : void { + var level : int = event.loadMetrics.level; + if (level == _hls.loadLevel) { + _updateSubtitlesTrackForLevel(level); + } + }; + + private function _updateSubtitlesTrackForLevel(level : uint) : void { + + var subtitlesTrackList : Vector. = new Vector.(); + var streamId : String = _hls.levels[level].subtitles_stream_id; + var autoSelectId : int = -1; + + // check if subtitles stream id is set, and subtitles tracks available + if (streamId && _levelLoader.subtitlesPlaylistTracks) { + // try to find subtitles streams matching with this ID + for (var idx : int = 0; idx < _levelLoader.subtitlesPlaylistTracks.length; idx++) { + var playlistTrack : SubtitlesPlaylistTrack = _levelLoader.subtitlesPlaylistTracks[idx]; + + if (playlistTrack.group_id == streamId) { + var isDefault : Boolean = playlistTrack.default_track; + var isForced : Boolean = playlistTrack.forced; + var autoSelect : Boolean = playlistTrack.autoselect; + var track:SubtitlesTrack = new SubtitlesTrack(playlistTrack.name, idx, SubtitlesTrack.FROM_PLAYLIST, playlistTrack.lang, isDefault, isForced, autoSelect); + + CONFIG::LOGGING { + Log.debug("subtitles track[" + subtitlesTrackList.length + "]:" + (isDefault ? "default:" : "alternate:") + playlistTrack.name); + } + + subtitlesTrackList.push(track); + + if (isDefault) _defaultTrackId = idx; + if (isForced) _forcedTrackId = idx; + + // Technical Note TN2288: https://developer.apple.com/library/ios/technotes/tn2288/_index.html + if (autoSelect + && playlistTrack.lang.toLowerCase().substr(0,2) == Capabilities.language) { + autoSelectId = idx; + } + } + } + } + + // check if subtitles tracks matching with current level have changed since last time + var subtitlesTrackChanged : Boolean = false; + if (_subtitlesTracksFromManifest.length != subtitlesTrackList.length) { + subtitlesTrackChanged = true; + } else { + for (idx = 0; idx < _subtitlesTracksFromManifest.length; ++idx) { + if (_subtitlesTracksFromManifest[idx].id != subtitlesTrackList[idx].id) { + subtitlesTrackChanged = true; + } + } + } + + // update subtitles list + if (subtitlesTrackChanged) { + _subtitlesTracksFromManifest = subtitlesTrackList; + _subtitlesTracksMerge(); + } + + // PRIORITY #1: Automatically select forced subtitles track + if (HLSSettings.subtitlesAutoSelectForced && _forcedTrackId != -1){ + subtitlesTrack = _forcedTrackId; + return; + } + + // PRIORITY #2: Automatically select auto-select subtitles track that matches current locale + if (HLSSettings.subtitlesAutoSelect && autoSelectId != -1) { + subtitlesTrack = autoSelectId; + return; + } + + // PRIORITY #3: Automatically select default subtitles track + if (HLSSettings.subtitlesAutoSelectDefault && _defaultTrackId != -1){ + subtitlesTrack = _defaultTrackId; + return; + } + + // Otherwise leave subtitles off/unselected + } + + /** + * Strictly speaking this isn't really needed for subtitles, but I've + * left it in place in case we want to merge in CEA-608 captions or + * add external subtitles support in the future + */ + private function _subtitlesTracksMerge() : void { + _subtitlesTracks = _subtitlesTracksFromManifest.slice(); + setTimeout(dispatchMetaData, 0); + } + + /** + * Announce availability of subtitles tracks using TX3G metadata + */ + protected function dispatchMetaData():void { + // Appending an FLVTag here breaks the stream, so we use script to achieve the same outcome + var stream:HLSNetStream = _hls.stream as HLSNetStream; + stream.dispatchClientEvent("onMetaData", tx3gMetaData); + } + + /** + * Minimal TX3G timed text metadata used to announce available + * subtitles tracks via an onMetaData NetStream event + */ + private function get tx3gMetaData():Object { + var trackinfo : Array = []; + for each (var track:SubtitlesTrack in subtitlesTracks) { + trackinfo.push({ + language: track.language, + title: track.title, + sampledescription: [{ + sampletype: 'tx3g' + }] + }); + } + return {trackinfo:trackinfo}; + } + + /** Normally triggered by user selection, it should return the subtitles track to be parsed */ + public function subtitlesTrackSelectionHandler(subtitlesTrackList : Vector.) : SubtitlesTrack { + var subtitlesTrackChanged : Boolean = false; + subtitlesTrackList = subtitlesTrackList.sort(function(a : SubtitlesTrack, b : SubtitlesTrack) : int { + return a.id - b.id; + }); + /* if subtitles track not defined, or subtitles from external source (playlist) return null (not selected) */ + if (_subtitlesTrackId == -1 || _subtitlesTrackId >= _subtitlesTracks.length || _subtitlesTracks[_subtitlesTrackId].source == SubtitlesTrack.FROM_PLAYLIST) { + return null; + } else { + return _subtitlesTracks[_subtitlesTrackId]; + } + } + + public function get defaultSubtitlesTrack():int { + return _defaultTrackId; + } + + public function get hasForcedSubtitles():Boolean { + return _forcedTrackId != -1; + } + + public function get forcedSubtitlesTrack():int { + return _forcedTrackId; + } + } +} diff --git a/src/org/mangui/hls/demux/TSDemuxer.as b/src/org/mangui/hls/demux/TSDemuxer.as index e306f227..4f7a5c48 100644 --- a/src/org/mangui/hls/demux/TSDemuxer.as +++ b/src/org/mangui/hls/demux/TSDemuxer.as @@ -836,8 +836,8 @@ package org.mangui.hls.demux { } break; case _pmtId: - if (_pmtParsed == false || _unknownPIDFound == true) { - CONFIG::LOGGING { + CONFIG::LOGGING { + if (_pmtParsed == false || _unknownPIDFound == true) { if(_pmtParsed == false) { Log.debug("TS: PMT found"); } else { diff --git a/src/org/mangui/hls/event/HLSEvent.as b/src/org/mangui/hls/event/HLSEvent.as index 5333fefd..57f75a29 100644 --- a/src/org/mangui/hls/event/HLSEvent.as +++ b/src/org/mangui/hls/event/HLSEvent.as @@ -2,9 +2,10 @@ * 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.event { - import org.mangui.hls.model.Level; - import flash.events.Event; + + import org.mangui.hls.model.Level; + import org.mangui.hls.model.Subtitle; /** Event fired when an error prevents playback. **/ public class HLSEvent extends Event { @@ -30,7 +31,7 @@ package org.mangui.hls.event { public static const FRAGMENT_LOADING : String = "hlsEventFragmentLoading"; /** Identifier for a fragment loaded event. **/ public static const FRAGMENT_LOADED : String = "hlsEventFragmentLoaded"; - /* Identifier for fragment load aborting for emergency switch down */ + /** Identifier for fragment load aborting for emergency switch down */ public static const FRAGMENT_LOAD_EMERGENCY_ABORTED : String = "hlsEventFragmentLoadEmergencyAborted"; /** Identifier for a fragment playing event. **/ public static const FRAGMENT_PLAYING : String = "hlsEventFragmentPlaying"; @@ -44,6 +45,16 @@ package org.mangui.hls.event { public static const AUDIO_LEVEL_LOADING : String = "hlsEventAudioLevelLoading"; /** Identifier for a audio level loaded event **/ public static const AUDIO_LEVEL_LOADED : String = "hlsEventAudioLevelLoaded"; + /** Identifier for a subtitles tracks list change */ + public static const SUBTITLES_TRACKS_LIST_CHANGE : String = "subtitlesTracksListChange"; + /** Identifier for a subtitles track switch */ + public static const SUBTITLES_TRACK_SWITCH : String = "hlsEventSubtitlesTrackSwitch"; + /** Identifier for a subtitles level loading event */ + public static const SUBTITLES_LEVEL_LOADING : String = "hlsEventSubtitlesLevelLoading"; + /** Identifier for a subtitles level loaded event */ + public static const SUBTITLES_LEVEL_LOADED : String = "hlsEventSubtitlesLevelLoaded"; + /** Identifier for when current subtitles change */ + public static const SUBTITLES_CHANGE : String = "hlsEventSubtitlesChange"; /** Identifier for audio/video TAGS loaded event. **/ public static const TAGS_LOADED : String = "hlsEventTagsLoaded"; /** Identifier when last fragment of playlist has been loaded **/ @@ -101,6 +112,10 @@ package org.mangui.hls.event { public var audioTrack : int; /** a complete ID3 payload from PES, as a hex dump **/ public var ID3Data : String; + /** The current subtitles track */ + public var subtitlesTrack : int; + /** a subtitles model */ + public var subtitle:Subtitle; /** Assign event parameter and dispatch. **/ public function HLSEvent(type : String, parameter : *=null, parameter2 : *=null) { @@ -118,8 +133,12 @@ package org.mangui.hls.event { case FRAGMENT_LOAD_EMERGENCY_ABORTED: case LEVEL_LOADED: case AUDIO_LEVEL_LOADED: - loadMetrics = parameter as HLSLoadMetrics; - break; + case SUBTITLES_LEVEL_LOADED: + loadMetrics = parameter as HLSLoadMetrics; + break; + case SUBTITLES_CHANGE: + subtitle = parameter as Subtitle; + break; case MANIFEST_PARSED: case MANIFEST_LOADED: levels = parameter as Vector.; diff --git a/src/org/mangui/hls/loader/LevelLoader.as b/src/org/mangui/hls/loader/LevelLoader.as index cde286f8..c0084a6a 100644 --- a/src/org/mangui/hls/loader/LevelLoader.as +++ b/src/org/mangui/hls/loader/LevelLoader.as @@ -12,20 +12,21 @@ package org.mangui.hls.loader { import flash.utils.clearTimeout; import flash.utils.getTimer; import flash.utils.setTimeout; - + + import org.mangui.hls.HLS; + import org.mangui.hls.HLSSettings; import org.mangui.hls.constant.HLSLoaderTypes; import org.mangui.hls.constant.HLSPlayStates; import org.mangui.hls.constant.HLSTypes; import org.mangui.hls.event.HLSError; import org.mangui.hls.event.HLSEvent; import org.mangui.hls.event.HLSLoadMetrics; - import org.mangui.hls.HLS; - import org.mangui.hls.HLSSettings; import org.mangui.hls.model.Fragment; import org.mangui.hls.model.Level; import org.mangui.hls.playlist.AltAudioTrack; import org.mangui.hls.playlist.DataUri; import org.mangui.hls.playlist.Manifest; + import org.mangui.hls.playlist.SubtitlesPlaylistTrack; CONFIG::LOGGING { import org.mangui.hls.utils.Log; @@ -53,13 +54,15 @@ package org.mangui.hls.loader { private var _manifestLoading : Manifest; /** is this loader closed **/ private var _closed : Boolean = false; - /* playlist retry timeout */ + /** playlist retry timeout */ private var _retryTimeout : Number; private var _retryCount : int; private var _redundantRetryCount:int; - /* alt audio tracks */ + /** alt audio tracks */ private var _altAudioTracks : Vector.; - /* manifest load metrics */ + /** subtitle tracks */ + private var _subtitlesPlaylistTracks : Vector.; + /** manifest load metrics */ private var _metrics : HLSLoadMetrics; /** Setup the loader. **/ @@ -157,6 +160,10 @@ package org.mangui.hls.loader { public function get altAudioTracks() : Vector. { return _altAudioTracks; } + + public function get subtitlesPlaylistTracks() : Vector. { + return _subtitlesPlaylistTracks; + } /** Load the manifest file. **/ public function load(url : String) : void { @@ -179,6 +186,7 @@ package org.mangui.hls.loader { _retryCount = 0; _redundantRetryCount = 0; _altAudioTracks = null; + _subtitlesPlaylistTracks = null; _loadManifest(); } @@ -318,6 +326,18 @@ package org.mangui.hls.loader { } } } + if (string.indexOf(Manifest.SUBTITLES) > 0) { + CONFIG::LOGGING { + Log.debug("subtitles level found"); + } + // parse subtitles tracks + _subtitlesPlaylistTracks = Manifest.extractSubtitlesTracks(string, _url); + CONFIG::LOGGING { + if (_subtitlesPlaylistTracks.length > 0) { + Log.debug(_subtitlesPlaylistTracks.length + " subtitle tracks found"); + } + } + } } else { errorTxt = "No level found in Manifest"; } diff --git a/src/org/mangui/hls/loader/SubtitlesFragmentLoader.as b/src/org/mangui/hls/loader/SubtitlesFragmentLoader.as new file mode 100644 index 00000000..073f9ee5 --- /dev/null +++ b/src/org/mangui/hls/loader/SubtitlesFragmentLoader.as @@ -0,0 +1,330 @@ +/* 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.loader { + + import flash.events.ErrorEvent; + import flash.events.Event; + import flash.events.IOErrorEvent; + import flash.events.SecurityErrorEvent; + import flash.net.URLLoader; + import flash.net.URLRequest; + import flash.utils.Dictionary; + import flash.utils.clearTimeout; + import flash.utils.setTimeout; + + import org.mangui.hls.HLS; + import org.mangui.hls.HLSSettings; + import org.mangui.hls.constant.HLSLoaderTypes; + import org.mangui.hls.constant.HLSPlayStates; + import org.mangui.hls.constant.HLSTypes; + import org.mangui.hls.event.HLSEvent; + import org.mangui.hls.flv.FLVTag; + import org.mangui.hls.model.Fragment; + import org.mangui.hls.model.Subtitle; + import org.mangui.hls.stream.StreamBuffer; + import org.mangui.hls.utils.WebVTTParser; + import org.mangui.hls.utils.hls_internal; + + CONFIG::LOGGING { + import org.mangui.hls.utils.Log; + } + + use namespace hls_internal; + + /** + * Subtitles (WebVTT) fragment loader + * @author Neil Rackett + */ + public class SubtitlesFragmentLoader { + + protected var _hls:HLS; + + // Loader + protected var _streamBuffer:StreamBuffer; + protected var _loader:URLLoader; + protected var _fragments:Vector. = new Vector.; + protected var _fragment:Fragment; + protected var _retryDelay:int; + protected var _retryRemaining:int; + protected var _retryTimeout:uint; + /** Cache of previously loaded subtitles tags (VOD only) */ + protected var _cache:Dictionary = new Dictionary(true); + /** Track IDs of subtitles tracks that have alreadsy been loaded and appended to the stream (VOD only) */ + protected var _appended:Dictionary = new Dictionary(true); + + public function SubtitlesFragmentLoader(hls:HLS, streamBuffer:StreamBuffer) { + + _hls = hls; + _streamBuffer = streamBuffer; + + _hls.addEventListener(HLSEvent.MANIFEST_LOADING, manifestLoadingHandler); + _hls.addEventListener(HLSEvent.SUBTITLES_TRACK_SWITCH, subtitlesTrackSwitchHandler); + _hls.addEventListener(HLSEvent.SUBTITLES_LEVEL_LOADED, subtitlesLevelLoadedHandler); + _hls.addEventListener(HLSEvent.SEEK_STATE, seekStateHandler); + + _loader = new URLLoader(); + _loader.addEventListener(Event.COMPLETE, loader_completeHandler); + _loader.addEventListener(IOErrorEvent.IO_ERROR, loader_errorHandler); + _loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, loader_errorHandler); + } + + public function dispose():void { + + stop(); + + _hls.removeEventListener(HLSEvent.MANIFEST_LOADING, manifestLoadingHandler); + _hls.removeEventListener(HLSEvent.SUBTITLES_TRACK_SWITCH, subtitlesTrackSwitchHandler); + _hls.removeEventListener(HLSEvent.SUBTITLES_LEVEL_LOADED, subtitlesLevelLoadedHandler); + _hls.removeEventListener(HLSEvent.SEEK_STATE, seekStateHandler); + _hls = null; + + _loader.removeEventListener(Event.COMPLETE, loader_completeHandler); + _loader.removeEventListener(IOErrorEvent.IO_ERROR, loader_errorHandler); + _loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, loader_errorHandler); + _loader = null; + + _fragments = null; + _fragment = null; + _cache = null; + _appended = null; + } + + /** + * Stop any currently loading subtitles + */ + public function stop():void { + CONFIG::LOGGING { + Log.debug(this+" Stopping"); + } + + try { _loader.close(); } + catch (e:Error) {}; + + _fragments = new Vector.(); + } + + /** + * Get ready for a new stream + */ + protected function manifestLoadingHandler(event:HLSEvent):void { + CONFIG::LOGGING { + Log.debug(this+" Manifest loading, stopping loading and resetting cache"); + } + + stop(); + + _cache = new Dictionary(true); + _appended = new Dictionary(true); + } + + /** + * Handle the user switching subtitles track + */ + protected function subtitlesTrackSwitchHandler(event:HLSEvent):void { + CONFIG::LOGGING { + Log.debug("Switching to subtitles track "+event.subtitlesTrack); + } + stop(); + } + + protected function playbackStateHandler(event:HLSEvent):void { + if (event.state == HLSPlayStates.IDLE) { + stop(); + } + } + + /** + * Preload all of the subtitles listed in the loaded subtitles level definitions + */ + protected function subtitlesLevelLoadedHandler(event:HLSEvent):void { + if (_hls.subtitlesTrack != -1) { + CONFIG::LOGGING { + Log.debug(this+" Loading subtitles fragments for track "+_hls.subtitlesTrack); + } + _fragments = _fragments.concat(_hls.subtitlesTracks[_hls.subtitlesTrack].level.fragments); + loadNextFragment(); + } + } + + /** + * Flashls flushes tags on seek, which wipes out VOD subtitles because they're + * all loaded at the start, so we need to reload them (live subtitles are + * per fragment, so they should take care of themselves) + */ + protected function seekStateHandler(event:HLSEvent):void { + if (_hls.type == HLSTypes.VOD && _hls.watched) { + CONFIG::LOGGING { + Log.debug(this+" Re-appending subtitles after seek"); + } + _appended = new Dictionary(true); + subtitlesLevelLoadedHandler(event); + } + } + + /** + * Load the next subtitles fragment + */ + protected function loadNextFragment():void { + if (_fragments.length) { + CONFIG::LOGGING { + Log.debug(this+" Loading next subtitles fragment"); + } + _retryRemaining = HLSSettings.fragmentLoadMaxRetry; + _retryDelay = 1000; + _fragment = _fragments.shift(); + loadFragment(); + } else if (_hls.type == HLSTypes.VOD) { + _appended[_hls.subtitlesTrack] = true; + } + } + + /** + * Load fragment + */ + protected function loadFragment():void { + + clearTimeout(_retryTimeout); + + if (_appended[_hls.subtitlesTrack]) { + CONFIG::LOGGING { + Log.debug(this+" Subtitles fragments for track "+_hls.subtitlesTrack+" already loaded"); + } + return; + } + + if (_fragment) { + + CONFIG::LOGGING { + Log.debug(this+" Loading subtitles fragment: "+_fragment.url); + } + + // Have we already loaded the fragment? + if (_cache[_fragment]) { + var cached:Vector. = _cache[_fragment] as Vector.; + if (cached) { + appendTags(_fragment, cached); + loadNextFragment(); + } + } else { + _loader.load(new URLRequest(_fragment.url)); + } + + } else { + loadNextFragment(); + } + } + + /** + * Parse the loaded WebVTT data + */ + protected function loader_completeHandler(event:Event):void { + + CONFIG::LOGGING { + Log.debug(this+" Loaded "+_fragment.url); + } + + var subtitles:Vector. = WebVTTParser.parse(_loader.data, _fragment.level, _fragment.program_date); + + if (_hls.type == HLSTypes.VOD) { + subtitles = padSubtitles(subtitles); + } + + // Prepare FLVTag to be appended to the stream + var tags:Vector. = toTags(subtitles); + if (tags) { + if (_hls.type == HLSTypes.VOD) { + _cache[_fragment] = tags; + } + appendTags(_fragment, tags); + } + + loadNextFragment(); + } + + /** + * Fill in the gaps between subtitles with blanks + */ + protected function padSubtitles(subtitles:Vector.):Vector. { + + // Fill all the gaps + for (var i:uint=0; i):Vector. { + + CONFIG::LOGGING { + Log.debug("Converting "+subtitles.length+" subtitles into tags"); + } + + var subtitle:Subtitle; + var tags:Vector. = new Vector.(); + + + for each (subtitle in subtitles) { + tags.push(subtitle.$toTag()); + } + + return tags; + } + + /** + * Append subtitle tags to the stream + */ + protected function appendTags(fragment:Fragment, tags:Vector.):void { + if (fragment && tags && tags.length) { + CONFIG::LOGGING { + Log.debug(this+" Appending "+tags.length+" onTextData tags for seqnum "+fragment.seqnum+" to the stream at "+fragment.start_time); + } + _streamBuffer.appendTags( + HLSLoaderTypes.FRAGMENT_SUBTITLES, + fragment.level, + fragment.seqnum, + tags, + tags[0].pts, + tags[tags.length-1].pts, + fragment.continuity, + fragment.start_time + ); + } + } + + /** + * If the subtitles fail to load, give up and load the next subtitles fragment + */ + protected function loader_errorHandler(event:ErrorEvent):void { + CONFIG::LOGGING { + Log.error(this+" Error "+event.errorID+" while loading "+_fragment.url+": "+event.text); + } + if (_retryRemaining--) { + var delay:Number = _retryDelay * 2; + clearTimeout(_retryTimeout); + if (delay <= HLSSettings.fragmentLoadMaxRetryTimeout) { + CONFIG::LOGGING { + Log.error(this+" Retrying in "+_retryDelay+"ms"); + } + _retryTimeout = setTimeout(loadFragment, _retryDelay); + } + } + loadNextFragment(); + } + } + +} diff --git a/src/org/mangui/hls/loader/SubtitlesLevelLoader.as b/src/org/mangui/hls/loader/SubtitlesLevelLoader.as new file mode 100644 index 00000000..3ed051e1 --- /dev/null +++ b/src/org/mangui/hls/loader/SubtitlesLevelLoader.as @@ -0,0 +1,205 @@ +/* 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.loader { + import flash.events.ErrorEvent; + import flash.events.IOErrorEvent; + import flash.events.SecurityErrorEvent; + import flash.utils.clearTimeout; + import flash.utils.getTimer; + import flash.utils.setTimeout; + + import org.mangui.hls.HLS; + import org.mangui.hls.HLSSettings; + import org.mangui.hls.constant.HLSPlayStates; + import org.mangui.hls.event.HLSError; + import org.mangui.hls.event.HLSEvent; + import org.mangui.hls.event.HLSLoadMetrics; + import org.mangui.hls.model.Fragment; + import org.mangui.hls.model.Level; + import org.mangui.hls.model.SubtitlesTrack; + import org.mangui.hls.playlist.Manifest; + import org.mangui.hls.playlist.SubtitlesPlaylistTrack; + + CONFIG::LOGGING { + import org.mangui.hls.utils.Log; + } + + /** + * Subtitles level loader, based on the alternative audio level loader + * @author Neil Rackett + */ + public class SubtitlesLevelLoader { + /** Reference to the hls framework controller. **/ + private var _hls : HLS; + /** Reference to the hls level loader. **/ + private var _levelLoader : LevelLoader; + /** Link to the M3U8 file. **/ + private var _url : String; + /** Timeout ID for reloading live playlists. **/ + private var _timeoutID : uint; + /** last reload manifest time **/ + private var _reloadPlaylistTimer : uint; + /** current subtitles level **/ + private var _currentTrack : int; + /** reference to manifest being loaded **/ + private var _manifestLoading : Manifest; + /** is this loader closed **/ + private var _closed : Boolean = false; + /* playlist retry timeout */ + private var _retryTimeout : Number; + private var _retryCount : int; + + /** Setup the loader. **/ + public function SubtitlesLevelLoader(hls : HLS, levelLoader : LevelLoader) { + _hls = hls; + _hls.addEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler); + _hls.addEventListener(HLSEvent.SUBTITLES_TRACK_SWITCH, _subtitlesTrackSwitchHandler); + + _levelLoader = levelLoader; + }; + + public function dispose() : void { + _close(); + _hls.removeEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler); + _hls.removeEventListener(HLSEvent.SUBTITLES_TRACK_SWITCH, _subtitlesTrackSwitchHandler); + } + + /** Loading failed; return errors. **/ + private function _errorHandler(event : ErrorEvent) : void { + var txt : String; + var code : int; + if (event is SecurityErrorEvent) { + code = HLSError.MANIFEST_LOADING_CROSSDOMAIN_ERROR; + txt = "Cannot load M3U8: crossdomain access denied:" + event.text; + } else if (event is IOErrorEvent && (HLSSettings.manifestLoadMaxRetry == -1 || _retryCount < HLSSettings.manifestLoadMaxRetry)) { + CONFIG::LOGGING { + Log.warn("I/O Error while trying to load Playlist, retry in " + _retryTimeout + " ms"); + } + _timeoutID = setTimeout(_loadSubtitlesLevelPlaylist, _retryTimeout); + /* exponential increase of retry timeout, capped to manifestLoadMaxRetryTimeout */ + _retryTimeout = Math.min(HLSSettings.manifestLoadMaxRetryTimeout, 2 * _retryTimeout); + _retryCount++; + return; + } else { + code = HLSError.MANIFEST_LOADING_IO_ERROR; + txt = "Cannot load M3U8: " + event.text; + } + var hlsError : HLSError = new HLSError(code, _url, txt); + _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError)); + }; + + /** parse a playlist **/ + private function _parseSubtitlesPlaylist(string : String, url : String, level : int, metrics : HLSLoadMetrics) : void { + + if (string != null && string.length != 0) { + CONFIG::LOGGING { + Log.debug("subtitles level " + level + " playlist:\n" + string); + } + + // Extract WebVTT subtitles fragments from the manifest + var frags : Vector. = Manifest.getFragments(string, url, level); + var subtitlesTrack : SubtitlesTrack = _hls.subtitlesTracks[_currentTrack]; + var subtitlesLevel : Level = subtitlesTrack.level; + + if(subtitlesLevel == null) { + subtitlesLevel = subtitlesTrack.level = new Level(); + } + + subtitlesLevel.updateFragments(frags); + subtitlesLevel.targetduration = Manifest.getTargetDuration(string); + + // if stream is live, use a timer to periodically reload playlist + if (!Manifest.hasEndlist(string)) { + //var timeout : int = Math.max(10000, _reloadPlaylistTimer + 1000*(frags.length-1)*subtitlesLevel.targetduration - getTimer()); + //var timeout : int = Math.max(10000, _reloadPlaylistTimer + 1000*(frags.length-1)*subtitlesLevel.averageduration - getTimer()); + var timeout : int = Math.max(10000, _reloadPlaylistTimer+1000*subtitlesLevel.averageduration-getTimer()); + + CONFIG::LOGGING { + Log.debug("Subtitles Level Live Playlist parsing finished: reload in " + timeout + " ms"); + } + _timeoutID = setTimeout(_loadSubtitlesLevelPlaylist, timeout); + } + } + + metrics.id = subtitlesLevel.start_seqnum; + metrics.id2 = subtitlesLevel.end_seqnum; + + _hls.dispatchEvent(new HLSEvent(HLSEvent.SUBTITLES_LEVEL_LOADED, metrics, frags)); + _manifestLoading = null; + }; + + /** load/reload active M3U8 playlist **/ + private function _loadSubtitlesLevelPlaylist() : void { + + if (_closed) { + return; + } + + _reloadPlaylistTimer = getTimer(); + + var subtitlesPlaylistTrack : SubtitlesPlaylistTrack = _levelLoader.subtitlesPlaylistTracks[_hls.subtitlesTracks[_currentTrack].id]; + + _manifestLoading = new Manifest(); + _manifestLoading.loadPlaylist(_hls, subtitlesPlaylistTrack.url, _parseSubtitlesPlaylist, _errorHandler, _currentTrack, _hls.type, HLSSettings.flushLiveURLCache); + _hls.dispatchEvent(new HLSEvent(HLSEvent.SUBTITLES_LEVEL_LOADING, _currentTrack)); + }; + + /** When subtitles track switch occurs, load subtitles level playlist **/ + private function _subtitlesTrackSwitchHandler(event : HLSEvent) : void { + + _currentTrack = event.subtitlesTrack; + clearTimeout(_timeoutID); + + if (_currentTrack > -1 && _currentTrack < _hls.subtitlesTracks.length) { + + var subtitlesTrack:SubtitlesTrack = _hls.subtitlesTracks[_currentTrack]; + + if (subtitlesTrack.source == SubtitlesTrack.FROM_PLAYLIST) { + + var subtitlesPlaylistTrack : SubtitlesPlaylistTrack = _levelLoader.subtitlesPlaylistTracks[subtitlesTrack.id]; + + if (subtitlesPlaylistTrack.url) { + + CONFIG::LOGGING { + Log.debug("switch to subtitles track " + _currentTrack + ", load Playlist"); + } + + _retryTimeout = 1000; + _retryCount = 0; + _closed = false; + + if(_manifestLoading) { + _manifestLoading.close(); + _manifestLoading = null; + } + + _loadSubtitlesLevelPlaylist(); + } + } + } + + }; + + private function _close() : void { + CONFIG::LOGGING { + Log.debug("Cancelling any subtitles level load in progress"); + } + _closed = true; + clearTimeout(_timeoutID); + try { + if (_manifestLoading) { + _manifestLoading.close(); + } + } catch(e : Error) { + } + } + + /** When the framework idles out, stop reloading manifest **/ + private function _stateHandler(event : HLSEvent) : void { + if (event.state == HLSPlayStates.IDLE) { + _close(); + } + }; + } +} diff --git a/src/org/mangui/hls/model/Level.as b/src/org/mangui/hls/model/Level.as index 8723f1ec..cf1cf0f5 100644 --- a/src/org/mangui/hls/model/Level.as +++ b/src/org/mangui/hls/model/Level.as @@ -49,6 +49,8 @@ package org.mangui.hls.model { public var duration : Number; /** Audio Identifier **/ public var audio_stream_id : String; + /** Subtitles Identifier **/ + public var subtitles_stream_id : String; /** Create the quality level. **/ public function Level() : void { diff --git a/src/org/mangui/hls/model/Subtitle.as b/src/org/mangui/hls/model/Subtitle.as new file mode 100644 index 00000000..bc65857e --- /dev/null +++ b/src/org/mangui/hls/model/Subtitle.as @@ -0,0 +1,159 @@ +package org.mangui.hls.model +{ + import flash.net.ObjectEncoding; + import flash.utils.ByteArray; + + import org.mangui.hls.flv.FLVTag; + import org.mangui.hls.utils.StringUtil; + import org.mangui.hls.utils.hls_internal; + + use namespace hls_internal; + + /** + * Subtitle model for Flashls + * @author Neil Rackett + */ + public class Subtitle + { + private var _tag:FLVTag; + + /** + * Convert an object (e.g. data from an onTextData event) into a + * Subtitle class instance + */ + public static function toSubtitle(data:Object):Subtitle + { + return new Subtitle(data.htmlText || data.text, + data.startPTS, data.endPTS, + data.startPosition, data.endPosition, + data.startDate, data.endDate); + } + + private var _trackid:int; + private var _htmlText:String; + private var _text:String; + private var _startPTS:Number; + private var _endPTS:Number; + private var _startDate:Number; + private var _endDate:Number; + private var _startPosition:Number; + private var _endPosition:Number; + + /** + * Create a new Subtitle object + * + * @param trackid The ID of the subtitles track this subtitle related to (TX3G standard naming) + * @param htmlText Subtitle text, including any HTML styling + * @param startPTS Start timestamp for FLVTag in milliseconds (MPEGTS/90 + startPosition*1000) + * @param endPTS End timestamp for FLVTag in milliseconds (MPEGTS/90 + endPosition*1000) + * @param startPosition Start position in seconds + * @param endPosition End position in seconds + * @param startDate Start timestamp (#EXT-X-PROGRAM-DATE-TIME + startPosition*1000) + * @param endDate End timestamp (#EXT-X-PROGRAM-DATE-TIME + endPosition*1000) + */ + public function Subtitle( + trackid:int, + htmlText:String, + startPTS:Number, endPTS:Number, + startPosition:Number=NaN, endPosition:Number=NaN, + startDate:Number=NaN, endDate:Number=NaN + ) + { + _trackid = trackid; + + _htmlText = htmlText || ''; + _text = StringUtil.removeHtmlTags(_htmlText); + + _startPTS = startPTS; + _endPTS = endPTS; + + _startPosition = startPosition || _startPTS/1000; + _endPosition = endPosition || _endPTS/1000; + + _startDate = startDate || _startPosition*1000; + _endDate = endDate || _endPosition*1000 + } + + /** + * The subtitle's text, including HTML tags (if applicable) + */ + public function get htmlText():String { return _htmlText; } + + /** + * The subtitle's text, with HTML markup removed + */ + public function get text():String { return _text; } + + public function get trackid():Number { return _trackid; } + public function get startPTS():Number { return _startPTS; } + public function get endPTS():Number { return _endPTS; } + public function get startPosition():Number { return _startPosition; } + public function get endPosition():Number { return _endPosition; } + public function get startDate():Number { return _startDate; } + public function get endDate():Number { return _endDate; } + public function get duration():Number { return _endPosition-_startPosition; } + + /** + * Convert to a plain object via the standard toJSON method + */ + public function toJSON():Object + { + return { + // TX3G properties + trackid: trackid, + text: text, + + // flashls specific properties + htmlText: htmlText, + startPTS: startPTS, + endPTS: endPTS, + startPosition: startPosition, + endPosition: endPosition, + startDate: startDate, + endDate: endDate, + duration: duration + } + } + + /** + * Does this subtitle have the same content as the specified subtitle? + * @param subtitle The subtitle to compare + * @returns Boolean true if the contents are the same + */ + public function equals(subtitle:Subtitle, textOnly:Boolean=true):Boolean + { + var isMatch:Boolean = subtitle is Subtitle + && htmlText == subtitle.htmlText; + + if (textOnly) return isMatch; + + return isMatch + && startPTS == subtitle.startPTS + && endPTS == subtitle.endPTS; + } + + public function toString():String + { + return '[Subtitles startPTS='+startPTS+' endPTS='+endPTS+' htmlText="'+htmlText+'"]'; + } + + hls_internal function $toTag():FLVTag + { + if (!_tag) + { + _tag = new FLVTag(FLVTag.METADATA, startPTS, startPTS, false); + + var bytes:ByteArray = new ByteArray(); + + bytes.objectEncoding = ObjectEncoding.AMF0; + bytes.writeObject("onTextData"); + bytes.writeObject(toJSON()); + + _tag.push(bytes, 0, bytes.length); + _tag.build(); + } + + return _tag; + } + } +} \ No newline at end of file diff --git a/src/org/mangui/hls/model/SubtitlesTrack.as b/src/org/mangui/hls/model/SubtitlesTrack.as new file mode 100644 index 00000000..2f7b92dd --- /dev/null +++ b/src/org/mangui/hls/model/SubtitlesTrack.as @@ -0,0 +1,35 @@ +/* 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.model { + /** Audio Track identifier **/ + public class SubtitlesTrack { + public static const FROM_PLAYLIST : int = 1; + public var title : String; + public var language : String; + public var id : int; + public var source : int; + public var isDefault : Boolean; + public var isForced : Boolean; + public var autoSelect : Boolean; + public var level : Level; + + /** + * Subtitles track model, based on alternative audio track model + * @author Neil Rackett + */ + public function SubtitlesTrack(title:String, id:int, source:int=0, language:String='', isDefault:Boolean=false, isForced:Boolean=false, autoSelect:Boolean=false) { + this.title = title; + this.language = language; + this.source = source; + this.id = id; + this.isDefault = isDefault; + this.isForced = isForced; + this.autoSelect = autoSelect; + } + + public function toString() : String { + return "SubtitlesTrack ID: " + id + " Title: " + title + " Source: " + source + " Default: " + isDefault + " Forced: " + isForced + " Auto Select: " + autoSelect; + } + } +} \ No newline at end of file diff --git a/src/org/mangui/hls/playlist/Manifest.as b/src/org/mangui/hls/playlist/Manifest.as index cdce7d01..6e4d5eb7 100644 --- a/src/org/mangui/hls/playlist/Manifest.as +++ b/src/org/mangui/hls/playlist/Manifest.as @@ -40,6 +40,8 @@ package org.mangui.hls.playlist { /** Tag that provides info related to alternative audio tracks */ public static const ALTERNATE_AUDIO : String = '#EXT-X-MEDIA:TYPE=AUDIO,'; /** Tag that provides info related to alternative rendition */ + public static const SUBTITLES : String = '#EXT-X-MEDIA:TYPE=SUBTITLES,'; + /** Tag that provides info related to alternative rendition */ private static const MEDIA : String = '#EXT-X-MEDIA:'; /** Tag that provides the sequence number. **/ private static const SEQNUM : String = '#EXT-X-MEDIA-SEQUENCE:'; @@ -349,6 +351,8 @@ package org.mangui.hls.playlist { } } else if (param.indexOf('AUDIO') > -1) { level.audio_stream_id = (param.split('=')[1] as String).replace(replacedoublequote, "").replace(trimwhitespace, ""); + } else if (param.indexOf('SUBTITLES') > -1) { + level.subtitles_stream_id = StringUtil.trim(param.split('=')[1] as String).replace(replacedoublequote, ""); } else if (param.indexOf('CLOSED-CAPTIONS') > -1) { level.closed_captions = (param.split('=')[1] as String).replace(replacedoublequote, "").replace(trimwhitespace, ""); } else if (param.indexOf('NAME') > -1) { @@ -394,11 +398,8 @@ package org.mangui.hls.playlist { var line : String = lines[i++]; if (line.indexOf(MEDIA) == 0) { var params : Object = _parseAlternateRendition(line); - // #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 1",AUTOSELECT=YES,DEFAULT=YES // #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 2",AUTOSELECT=NO,DEFAULT=NO,URI="alternate_audio_aac_sinewave/prog_index.m3u8" - // #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="captions.m3u8" - var uri : String = params['URI']; if (uri) { uri = _extractURL(uri, base); @@ -411,6 +412,29 @@ package org.mangui.hls.playlist { } return altAudioTracks; }; + + /** Extract subtitles tracks from manifest data. **/ + public static function extractSubtitlesTracks(data : String, base : String = '') : Vector. { + var subtitlesTracks : Vector. = new Vector.(); + var lines : Vector. = StringUtil.toLines(data); + var i : int = 0; + while (i < lines.length) { + var line : String = lines[i++]; + if (line.indexOf(MEDIA) == 0) { + var params : Object = _parseAlternateRendition(line); + // #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="captions.m3u8" + var uri : String = params['URI']; + if (uri) { + uri = _extractURL(uri, base); + } + if (params['TYPE'] == 'SUBTITLES') { + var track : SubtitlesPlaylistTrack = new SubtitlesPlaylistTrack(params['GROUP-ID'], params['LANGUAGE'], params['NAME'], params['AUTOSELECT'] == 'YES', params['DEFAULT'] == 'YES', params['FORCED'] == 'YES', uri); + subtitlesTracks.push(track); + } + } + } + return subtitlesTracks; + }; private static const RENDITION_STATE_READKEY : Number = 1; private static const RENDITION_STATE_READVALUESTART : Number = 2; diff --git a/src/org/mangui/hls/playlist/SubtitlesPlaylistTrack.as b/src/org/mangui/hls/playlist/SubtitlesPlaylistTrack.as new file mode 100644 index 00000000..52d19cc5 --- /dev/null +++ b/src/org/mangui/hls/playlist/SubtitlesPlaylistTrack.as @@ -0,0 +1,30 @@ +/* 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.playlist { + public class SubtitlesPlaylistTrack { + public var group_id : String; + public var lang : String; + public var name : String; + public var autoselect : Boolean; + public var default_track : Boolean; + public var forced : Boolean; + public var url : String; + public var id:Object; + + /** Create the quality level. **/ + public function SubtitlesPlaylistTrack(alt_group_id : String, alt_lang : String, alt_name : String, alt_autoselect : Boolean, alt_default : Boolean, alt_forced : Boolean, alt_url : String) { + group_id = alt_group_id; + lang = alt_lang; + name = alt_name; + autoselect = alt_autoselect; + default_track = alt_default; + forced = alt_forced; + url = alt_url; + }; + + public function toString() : String { + return "SubtitlesTrack url: " + url + " group_id: " + group_id + " lang: " + lang + " name: " + name + ' default: ' + default_track + ' forced: ' + forced ; + }; + } +} diff --git a/src/org/mangui/hls/stream/HLSNetStream.as b/src/org/mangui/hls/stream/HLSNetStream.as index 9dcc51f3..06d947b2 100644 --- a/src/org/mangui/hls/stream/HLSNetStream.as +++ b/src/org/mangui/hls/stream/HLSNetStream.as @@ -2,29 +2,29 @@ * 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 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; + + import by.blooddy.crypto.Base64; + + import org.mangui.hls.HLS; + import org.mangui.hls.HLSSettings; + 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.model.Subtitle; CONFIG::LOGGING { import org.mangui.hls.utils.Log; @@ -89,6 +89,8 @@ package org.mangui.hls.stream { _client.registerCallback("onHLSFragmentChange", onHLSFragmentChange); _client.registerCallback("onHLSFragmentSkipped", onHLSFragmentSkipped); _client.registerCallback("onID3Data", onID3Data); + _client.registerCallback("onMetaData", onMetaData); + _client.registerCallback("onTextData", onTextData); super.client = _client; } @@ -124,6 +126,19 @@ package org.mangui.hls.stream { _hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_SKIPPED, duration)); } + + protected function onMetaData(data:Object) : void { + if (_hls.hasEventListener(HLSEvent.SUBTITLES_TRACKS_LIST_CHANGE) && data && data.trackinfo) { + _hls.dispatchEvent(new HLSEvent(HLSEvent.SUBTITLES_TRACKS_LIST_CHANGE)); + } + } + + protected function onTextData(data:Object) : void { + if (_hls.hasEventListener(HLSEvent.SUBTITLES_CHANGE)) { + _hls.dispatchEvent(new HLSEvent(HLSEvent.SUBTITLES_CHANGE, Subtitle.toSubtitle(data))); + } + } + // 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. @@ -479,5 +494,13 @@ package org.mangui.hls.stream { _timer.removeEventListener(TimerEvent.TIMER, _checkBuffer); _bufferThresholdController.dispose(); } + + /** + * Immediately dispatches an event via the client object to simulate + * an FLVTag event from the stream + */ + public function dispatchClientEvent(type:String, ...args):void { + _client[type].apply(_client, args); + } } } diff --git a/src/org/mangui/hls/stream/HLSNetStreamClient.as b/src/org/mangui/hls/stream/HLSNetStreamClient.as index 9f3f92f6..f341d35e 100644 --- a/src/org/mangui/hls/stream/HLSNetStreamClient.as +++ b/src/org/mangui/hls/stream/HLSNetStreamClient.as @@ -5,8 +5,9 @@ import flash.utils.flash_proxy; import flash.utils.Proxy; - /** Proxy that allows dispatching internal events fired by Netstream cues to - * internal listeners as well as the traditional client object + /** + * Proxy that allows dispatching internal events fired by Netstream cues to + * internal listeners as well as the traditional client object */ public class HLSNetStreamClient extends Proxy { private var _delegate : Object; @@ -22,23 +23,32 @@ public function get delegate() : Object { return this._delegate; } + + /** + * We have to create an onTextData method here otherwise the internal + * callback is never invoked. No idea why. + */ + public function onTextData(data:Object):void { + invokeCallback("onTextData", data); + } public function registerCallback(name : String, callback : Function) : void { _callbacks[name] = callback; } + + private function invokeCallback(methodName : String, ... args) : * { + var r : *; + if (_callbacks && _callbacks.hasOwnProperty(methodName)) { + r = _callbacks[methodName].apply(null, args); + } + if (_delegate && _delegate.hasOwnProperty(methodName)) { + r = _delegate[methodName].apply(null, args); + } + return r; + } override flash_proxy function callProperty(methodName : *, ... args) : * { - var r : * = null; - - if (_callbacks && _callbacks.hasOwnProperty(methodName)) { - r = _callbacks[methodName].apply(_callbacks, args); - } - - if (_delegate && _delegate.hasOwnProperty(methodName)) { - r = _delegate[methodName].apply(_delegate, args); - } - - return r; + return invokeCallback.apply(this, [methodName].concat(args)); } override flash_proxy function getProperty(name : *) : * { @@ -46,11 +56,9 @@ if (_callbacks && _callbacks.hasOwnProperty(name)) { r = _callbacks[name]; } - if (_delegate && _delegate.hasOwnProperty(name)) { r = _delegate[name]; } - return r; } diff --git a/src/org/mangui/hls/stream/StreamBuffer.as b/src/org/mangui/hls/stream/StreamBuffer.as index 28cfa9fd..d320e66a 100644 --- a/src/org/mangui/hls/stream/StreamBuffer.as +++ b/src/org/mangui/hls/stream/StreamBuffer.as @@ -5,8 +5,11 @@ package org.mangui.hls.stream { import flash.events.Event; import flash.events.TimerEvent; import flash.utils.Dictionary; - import flash.utils.getTimer; import flash.utils.Timer; + import flash.utils.getTimer; + + import org.mangui.hls.HLS; + import org.mangui.hls.HLSSettings; import org.mangui.hls.constant.HLSLoaderTypes; import org.mangui.hls.constant.HLSPlayStates; import org.mangui.hls.constant.HLSSeekMode; @@ -17,10 +20,9 @@ package org.mangui.hls.stream { import org.mangui.hls.event.HLSEvent; import org.mangui.hls.event.HLSMediatime; import org.mangui.hls.flv.FLVTag; - import org.mangui.hls.HLS; - import org.mangui.hls.HLSSettings; import org.mangui.hls.loader.AltAudioFragmentLoader; import org.mangui.hls.loader.FragmentLoader; + import org.mangui.hls.loader.SubtitlesFragmentLoader; import org.mangui.hls.model.AudioTrack; import org.mangui.hls.model.Fragment; import org.mangui.hls.model.Level; @@ -37,6 +39,7 @@ package org.mangui.hls.stream { private var _hls : HLS; private var _fragmentLoader : FragmentLoader; private var _altaudiofragmentLoader : AltAudioFragmentLoader; + private var _subtitlesFragmentLoader : SubtitlesFragmentLoader; /** Timer used to process FLV tags. **/ private var _timer : Timer; private var _audioTags : Vector., _videoTags : Vector.,_metaTags : Vector., _headerTags : Vector.; @@ -88,6 +91,7 @@ package org.mangui.hls.stream { _hls = hls; _fragmentLoader = new FragmentLoader(hls, audioTrackController, levelController, this); _altaudiofragmentLoader = new AltAudioFragmentLoader(hls, this); + _subtitlesFragmentLoader = new SubtitlesFragmentLoader(hls, this); flushBuffer(); _timer = new Timer(100, 0); _timer.addEventListener(TimerEvent.TIMER, _checkBuffer); @@ -111,6 +115,7 @@ package org.mangui.hls.stream { _altaudiofragmentLoader.dispose(); _fragmentLoader = null; _altaudiofragmentLoader = null; + _subtitlesFragmentLoader.dispose(); _hls = null; _timer = null; } @@ -292,7 +297,7 @@ package org.mangui.hls.stream { useful to compute sliding when discontinuity occurs */ _nextExpectedAbsoluteStartPosMain = nextRelativeStartPos + sliding; - } else { + } else if (fragmentType == HLSLoaderTypes.FRAGMENT_ALTAUDIO) { sliding = _liveSlidingAltAudio; // if a new fragment is being appended if(fragLevel != _fragAltAudioLevel || fragSN != _fragAltAudioSN) { @@ -324,8 +329,8 @@ package org.mangui.hls.stream { useful to compute sliding when discontinuity occurs */ _nextExpectedAbsoluteStartPosAltAudio = nextRelativeStartPos + sliding; - } - + } + for each (var tag : FLVTag in tags) { // CONFIG::LOGGING { // Log.debug2('append type/dts/pts:' + tag.typeString + '/' + tag.dts + '/' + tag.pts); @@ -850,11 +855,17 @@ package org.mangui.hls.stream { aacIdx = avcIdx = disIdx = metIdxMain = metIdxAltAudio = keyIdx = lastIdx = -1; var filteredTags : Vector.= new Vector.(); var idx2Clone : Vector. = new Vector.(); - + + if (isNaN(absoluteStartPosition)) return filteredTags; + // loop through all tags and find index position of header tags located before start position while(lastIdx ==-1) { for (var i : int = 0; i < tags.length; i++) { var data : FLVData = tags[i]; + if (isNaN(data.positionAbsolute)) { + tags.splice(i,1); + continue; + } if (data.positionAbsolute <= absoluteStartPosition) { lastIdx = i; // current tag is before requested start position @@ -866,7 +877,7 @@ package org.mangui.hls.stream { case FLVTag.METADATA: if(data.loaderType == HLSLoaderTypes.FRAGMENT_MAIN) { metIdxMain = i; - } else { + } else if(data.loaderType == HLSLoaderTypes.FRAGMENT_ALTAUDIO) { metIdxAltAudio = i; } break; diff --git a/src/org/mangui/hls/utils/StringUtil.as b/src/org/mangui/hls/utils/StringUtil.as index 1016fc0d..c7a99f94 100644 --- a/src/org/mangui/hls/utils/StringUtil.as +++ b/src/org/mangui/hls/utils/StringUtil.as @@ -78,6 +78,19 @@ package org.mangui.hls.utils return Vector.(lines); } + /** + * Removes all HTML tags from the String and returns it as plain text + * + * @param str HTML string + * @returns String without HTML tags + * @example StringUtil.removeHtmlTags("

Bonjour!

"); // Returns "Bonjour!" + */ + public static function removeHtmlTags(str:String):String + { + var tagExp:RegExp = /(<([^>]+)>)/ig; + return (str || "").replace(tagExp, ''); + } + /** * Converts strings containing Windows (CR-LF), MacOS (CR) and other * non-standard line breaks (LF-CR) into strings using only Linux-style diff --git a/src/org/mangui/hls/utils/WebVTTParser.as b/src/org/mangui/hls/utils/WebVTTParser.as new file mode 100644 index 00000000..45a0bbce --- /dev/null +++ b/src/org/mangui/hls/utils/WebVTTParser.as @@ -0,0 +1,111 @@ +/* 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.utils { + + import org.mangui.hls.model.Subtitle; + + /** + * WebVTT subtitles parser + * + * It supports standard WebVTT format text with or without align:* + * elements, which are currently ignored. + * + * @author Neil Rackett + */ + public class WebVTTParser { + + static private const CUE:RegExp = /^(?:(.*)(?:\n))?([\d:,.]+) --> ([\d:,.]+)((.|\n)*)/; + static private const TIMESTAMP:RegExp = /^(?:(\d{2,}):)?(\d{2}):(\d{2})[,.](\d{3})$/; + static private const MPEGTS:RegExp = /MPEGTS[:=](\d+)/; + + /** + * Parse a string into a series of Subtitles objects and return + * them in a Vector + */ + static public function parse(data:String, level:int=-1, fragmentTime:Number=0):Vector. { + data = StringUtil.toLF(data); + + CONFIG::LOGGING { + Log.debug("[WebVTTParser] Received:\n"+data); + } + + var mpegTS:Number = 0; + var results:Vector. = new Vector.; + var lines:Array = data.replace(/\balign:.*+/ig,'').split(/(?:(?:\n){2,})/); + + for each (var line:String in lines) { + + var matches:Array; + + switch (true) + { + case MPEGTS.test(line): { + matches = MPEGTS.exec(line); + mpegTS = Number(matches[1]); + + if (mpegTS > 4294967295) { + mpegTS -= 8589934592; + } + + CONFIG::LOGGING { + Log.debug2(mpegTS); + } + + continue; + } + + case CUE.test(line): { + matches = CUE.exec(line); + + var startPosition:Number = parseTime(matches[2]); + var startPTS:Number = Math.round(mpegTS/90 + startPosition*1000); + var startTime:Number = fragmentTime + startPosition*1000; + + var endPosition:Number = parseTime(matches[3]); + var endPTS:Number = Math.round(mpegTS/90 + endPosition*1000); + var endTime:Number = fragmentTime + endPosition*1000; + + var text:String = StringUtil.trim((matches[4] || '').replace(/(\|)/g, '\n')); + var subtitle:Subtitle = new Subtitle(level, text, startPTS, endPTS, startPosition, endPosition, startTime, endTime); + + results.push(subtitle); + + CONFIG::LOGGING { + Log.debug2(subtitle); + } + + continue; + } + + default: { + CONFIG::LOGGING { + Log.debug("[WebVTTParser] Unknown data found: "+line); + } + continue; + } + } + } + + return results; + } + + /** + * Converts a time string in the format 00:00:00.000 into seconds + */ + static public function parseTime(time:String):Number { + + if (!TIMESTAMP.test(time)) return NaN; + + var a:Array = TIMESTAMP.exec(time); + var seconds:Number = a[4]/1000; + + seconds += parseInt(a[3]); + + if (a[2]) seconds += a[2] * 60; + if (a[1]) seconds += a[1] * 60 * 60; + + return seconds; + } + } +} diff --git a/src/org/mangui/hls/utils/hls_internal.as b/src/org/mangui/hls/utils/hls_internal.as new file mode 100644 index 00000000..a18491ea --- /dev/null +++ b/src/org/mangui/hls/utils/hls_internal.as @@ -0,0 +1,4 @@ +package org.mangui.hls.utils +{ + public namespace hls_internal; +} \ No newline at end of file diff --git a/src/org/mangui/osmf/plugins/traits/HLSClosedCaptionsTrait.as b/src/org/mangui/osmf/plugins/traits/HLSClosedCaptionsTrait.as index 0515cbf4..8d77baab 100644 --- a/src/org/mangui/osmf/plugins/traits/HLSClosedCaptionsTrait.as +++ b/src/org/mangui/osmf/plugins/traits/HLSClosedCaptionsTrait.as @@ -15,7 +15,7 @@ private var _hls : HLS; private var _hasClosedCapations : String; - public function HLSClosedCaptionsTrait(hls : HLS, closed_captions : String = HLSClosedCaptionsState.UNKNOWN) { + public function HLSClosedCaptionsTrait(hls : HLS, closed_captions : String = "unknown") { CONFIG::LOGGING { Log.debug("HLSClosedCaptionsTrait()"); } From 8257d2926e8a64df8481fbbe5326517189dcfd10 Mon Sep 17 00:00:00 2001 From: Neil Rackett Date: Wed, 11 May 2016 18:10:20 +0100 Subject: [PATCH 8/9] Prevented events being dispatched for subtitles that don't match selected track --- src/org/mangui/hls/model/Subtitle.as | 3 +- src/org/mangui/hls/stream/HLSNetStream.as | 1012 ++++++++++----------- 2 files changed, 508 insertions(+), 507 deletions(-) diff --git a/src/org/mangui/hls/model/Subtitle.as b/src/org/mangui/hls/model/Subtitle.as index bc65857e..18e0d316 100644 --- a/src/org/mangui/hls/model/Subtitle.as +++ b/src/org/mangui/hls/model/Subtitle.as @@ -23,7 +23,8 @@ package org.mangui.hls.model */ public static function toSubtitle(data:Object):Subtitle { - return new Subtitle(data.htmlText || data.text, + return new Subtitle(data.trackid, + data.htmlText || data.text, data.startPTS, data.endPTS, data.startPosition, data.endPosition, data.startDate, data.endDate); diff --git a/src/org/mangui/hls/stream/HLSNetStream.as b/src/org/mangui/hls/stream/HLSNetStream.as index 06d947b2..10b5bbe7 100644 --- a/src/org/mangui/hls/stream/HLSNetStream.as +++ b/src/org/mangui/hls/stream/HLSNetStream.as @@ -1,506 +1,506 @@ -/* 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 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.NetStreamPlayOptions; - import flash.utils.ByteArray; - import flash.utils.Timer; - - import by.blooddy.crypto.Base64; - - import org.mangui.hls.HLS; - import org.mangui.hls.HLSSettings; - 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.model.Subtitle; - - 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); - _client.registerCallback("onMetaData", onMetaData); - _client.registerCallback("onTextData", onTextData); - 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)); - } - - - protected function onMetaData(data:Object) : void { - if (_hls.hasEventListener(HLSEvent.SUBTITLES_TRACKS_LIST_CHANGE) && data && data.trackinfo) { - _hls.dispatchEvent(new HLSEvent(HLSEvent.SUBTITLES_TRACKS_LIST_CHANGE)); - } - } - - protected function onTextData(data:Object) : void { - if (_hls.hasEventListener(HLSEvent.SUBTITLES_CHANGE)) { - _hls.dispatchEvent(new HLSEvent(HLSEvent.SUBTITLES_CHANGE, Subtitle.toSubtitle(data))); - } - } - - // 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.encode(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) { - /* 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.) : 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(); - } - - /** - * Immediately dispatches an event via the client object to simulate - * an FLVTag event from the stream - */ - public function dispatchClientEvent(type:String, ...args):void { - _client[type].apply(_client, args); - } - } -} +/* 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 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.NetStreamPlayOptions; + import flash.utils.ByteArray; + import flash.utils.Timer; + + import by.blooddy.crypto.Base64; + + import org.mangui.hls.HLS; + import org.mangui.hls.HLSSettings; + 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.model.Subtitle; + + 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); + _client.registerCallback("onMetaData", onMetaData); + _client.registerCallback("onTextData", onTextData); + 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)); + } + + + protected function onMetaData(data:Object) : void { + if (_hls.hasEventListener(HLSEvent.SUBTITLES_TRACKS_LIST_CHANGE) && data && data.trackinfo) { + _hls.dispatchEvent(new HLSEvent(HLSEvent.SUBTITLES_TRACKS_LIST_CHANGE)); + } + } + + protected function onTextData(data:Object) : void { + if (_hls.hasEventListener(HLSEvent.SUBTITLES_CHANGE) && data.trackid == _hls.subtitlesTrack) { + _hls.dispatchEvent(new HLSEvent(HLSEvent.SUBTITLES_CHANGE, Subtitle.toSubtitle(data))); + } + } + + // 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.encode(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) { + /* 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.) : 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(); + } + + /** + * Immediately dispatches an event via the client object to simulate + * an FLVTag event from the stream + */ + public function dispatchClientEvent(type:String, ...args):void { + _client[type].apply(_client, args); + } + } +} From a2ae0bac0afb9030f9d188eac06e86ea94170103 Mon Sep 17 00:00:00 2001 From: Neil Rackett Date: Thu, 12 May 2016 08:43:18 +0100 Subject: [PATCH 9/9] Corrected splice index --- src/org/mangui/hls/stream/StreamBuffer.as | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/mangui/hls/stream/StreamBuffer.as b/src/org/mangui/hls/stream/StreamBuffer.as index d320e66a..433ed02b 100644 --- a/src/org/mangui/hls/stream/StreamBuffer.as +++ b/src/org/mangui/hls/stream/StreamBuffer.as @@ -863,7 +863,7 @@ package org.mangui.hls.stream { for (var i : int = 0; i < tags.length; i++) { var data : FLVData = tags[i]; if (isNaN(data.positionAbsolute)) { - tags.splice(i,1); + tags.splice(i--, 1); continue; } if (data.positionAbsolute <= absoluteStartPosition) {