533 lines
20 KiB
ActionScript
533 lines
20 KiB
ActionScript
/* 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.flowplayer {
|
|
|
|
import flash.display.DisplayObject;
|
|
import flash.media.Video;
|
|
import flash.net.NetConnection;
|
|
import flash.net.NetStream;
|
|
import flash.system.Security;
|
|
import flash.utils.Dictionary;
|
|
import org.flowplayer.controller.StreamProvider;
|
|
import org.flowplayer.controller.TimeProvider;
|
|
import org.flowplayer.controller.VolumeController;
|
|
import org.flowplayer.model.Clip;
|
|
import org.flowplayer.model.ClipError;
|
|
import org.flowplayer.model.ClipEvent;
|
|
import org.flowplayer.model.ClipEventType;
|
|
import org.flowplayer.model.ClipType;
|
|
import org.flowplayer.model.Playlist;
|
|
import org.flowplayer.model.Plugin;
|
|
import org.flowplayer.model.PluginModel;
|
|
import org.flowplayer.view.Flowplayer;
|
|
import org.flowplayer.view.StageVideoWrapper;
|
|
import org.mangui.hls.constant.HLSPlayStates;
|
|
import org.mangui.hls.event.HLSEvent;
|
|
import org.mangui.hls.HLS;
|
|
import org.mangui.hls.utils.JSURLLoader;
|
|
import org.mangui.hls.utils.JSURLStream;
|
|
import org.mangui.hls.utils.Params2Settings;
|
|
|
|
CONFIG::LOGGING {
|
|
import org.mangui.hls.utils.Log;
|
|
}
|
|
public class HLSStreamProvider implements StreamProvider,Plugin {
|
|
private var _volumecontroller : VolumeController;
|
|
private var _playlist : Playlist;
|
|
private var _timeProvider : TimeProvider;
|
|
private var _model : PluginModel;
|
|
private var _player : Flowplayer;
|
|
private var _clip : Clip;
|
|
private var _video : Video;
|
|
/** reference to the framework. **/
|
|
private var _hls : HLS;
|
|
// event values
|
|
private var _duration : Number = 0;
|
|
private var _durationCapped : Number = 0;
|
|
private var _clipStart : Number = 0;
|
|
private var _videoWidth : int = -1;
|
|
private var _videoHeight : int = -1;
|
|
private var _isManifestLoaded : Boolean = false;
|
|
private var _pauseAfterStart : Boolean;
|
|
private var _seekable : Boolean = false;
|
|
private var _streamAttached : Boolean = false;
|
|
|
|
public function getDefaultConfig() : Object {
|
|
return null;
|
|
}
|
|
|
|
public function onConfig(model : PluginModel) : void {
|
|
CONFIG::LOGGING {
|
|
Log.info("onConfig()");
|
|
}
|
|
_model = model;
|
|
}
|
|
|
|
public function onLoad(player : Flowplayer) : void {
|
|
CONFIG::LOGGING {
|
|
Log.info("onLoad()");
|
|
}
|
|
Security.allowDomain("*");
|
|
_player = player;
|
|
_hls = new HLS();
|
|
_hls.stage = player.screen.getDisplayObject().stage;
|
|
_hls.addEventListener(HLSEvent.PLAYBACK_COMPLETE, _completeHandler);
|
|
_hls.addEventListener(HLSEvent.ERROR, _errorHandler);
|
|
_hls.addEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler);
|
|
_hls.addEventListener(HLSEvent.MEDIA_TIME, _mediaTimeHandler);
|
|
_hls.addEventListener(HLSEvent.PLAYBACK_STATE, _playbackStateHandler);
|
|
_hls.addEventListener(HLSEvent.ID3_UPDATED, _ID3Handler);
|
|
|
|
var cfg : Object = _model.config;
|
|
for (var object : String in cfg) {
|
|
var subidx : int = object.indexOf("hls_");
|
|
if (subidx != -1) {
|
|
if(object.indexOf("jsloader")!=-1) {
|
|
if(cfg[object] == true) {
|
|
_hls.URLstream = JSURLStream as Class;
|
|
_hls.URLloader = JSURLLoader as Class;
|
|
}
|
|
} else {
|
|
Params2Settings.set(object.substr(4), cfg[object]);
|
|
}
|
|
}
|
|
}
|
|
|
|
_model.dispatchOnLoad();
|
|
}
|
|
|
|
private function _completeHandler(event : HLSEvent) : void {
|
|
// dispatch a before event because the finish has default behavior that can be prevented by listeners
|
|
_clip.dispatchBeforeEvent(new ClipEvent(ClipEventType.FINISH));
|
|
_clip.startDispatched = false;
|
|
};
|
|
|
|
private function _errorHandler(event : HLSEvent) : void {
|
|
_clip.dispatchError(ClipError.STREAM_LOAD_FAILED,event.error.toString());
|
|
};
|
|
|
|
private function _ID3Handler(event : HLSEvent) : void {
|
|
_clip.dispatch(ClipEventType.NETSTREAM_EVENT, "onID3", event.ID3Data);
|
|
};
|
|
|
|
private function _manifestLoadedHandler(event : HLSEvent) : void {
|
|
_duration = event.levels[_hls.startLevel].duration - _clipStart;
|
|
_isManifestLoaded = true;
|
|
// only update duration if not capped
|
|
if (!_durationCapped) {
|
|
_clip.duration = _duration;
|
|
} else {
|
|
// ensure capped duration is lt real one
|
|
_durationCapped = Math.min(_durationCapped, _duration);
|
|
}
|
|
_clip.stopLiveOnPause = false;
|
|
/*
|
|
var nbLevel = event.levels.length;
|
|
if (nbLevel > 1) {
|
|
var bitrates : Array = new Array();
|
|
for (var i : int = 0; i < nbLevel; i++) {
|
|
var info : Object = new Object();
|
|
var level : Level = event.levels[i];
|
|
info.bitrate = level.bitrate;
|
|
info.url = level.url;
|
|
info.width = level.width;
|
|
info.height = level.height;
|
|
info.isDefault = (i == _hls.startLevel);
|
|
bitrates.push(info);
|
|
}
|
|
_clip.setCustomProperty("bitrates", bitrates);
|
|
}
|
|
*/
|
|
_clip.dispatch(ClipEventType.METADATA);
|
|
_seekable = true;
|
|
// real seek position : add clip.start offset. if not defined, use -1 to fix seeking issue on live playlist
|
|
_hls.stream.play(null, (_clip.start == 0) ? -1 : _clipStart);
|
|
_clip.dispatch(ClipEventType.SEEK, 0);
|
|
if (_pauseAfterStart) {
|
|
pause(new ClipEvent(ClipEventType.PAUSE));
|
|
}
|
|
};
|
|
|
|
private function _mediaTimeHandler(event : HLSEvent) : void {
|
|
_duration = event.mediatime.duration - _clipStart;
|
|
// only update duration if not capped
|
|
if (!_durationCapped) {
|
|
if(_clip.duration != _duration) {
|
|
_clip.duration = _duration;
|
|
_clip.dispatch(ClipEventType.METADATA_CHANGED);
|
|
}
|
|
} else {
|
|
// ensure capped duration is lt real one
|
|
_durationCapped = Math.min(_durationCapped, _duration);
|
|
if (_durationCapped - time <= 0.1) {
|
|
// reach end of stream, stop playback and simulate complete event
|
|
_hls.stream.close();
|
|
_clip.dispatchBeforeEvent(new ClipEvent(ClipEventType.FINISH));
|
|
_clip.startDispatched = false;
|
|
}
|
|
}
|
|
var videoWidth : int = _video.videoWidth;
|
|
var videoHeight : int = _video.videoHeight;
|
|
if (videoWidth && videoHeight) {
|
|
var changed : Boolean = _videoWidth != videoWidth || _videoHeight != videoHeight;
|
|
if (changed) {
|
|
CONFIG::LOGGING {
|
|
Log.info("video size changed to " + videoWidth + "/" + videoHeight);
|
|
}
|
|
_videoWidth = videoWidth;
|
|
_videoHeight = videoHeight;
|
|
_clip.originalWidth = videoWidth;
|
|
_clip.originalHeight = videoHeight;
|
|
if (!_clip.startDispatched) {
|
|
_clip.dispatch(ClipEventType.START);
|
|
_clip.startDispatched = true;
|
|
}
|
|
_clip.dispatch(ClipEventType.METADATA_CHANGED);
|
|
}
|
|
}
|
|
};
|
|
|
|
private function _playbackStateHandler(event : HLSEvent) : void {
|
|
// CONFIG::LOGGING {
|
|
// Log.txt("state:"+ event.state);
|
|
// }
|
|
switch(event.state) {
|
|
case HLSPlayStates.IDLE:
|
|
case HLSPlayStates.PLAYING:
|
|
case HLSPlayStates.PAUSED:
|
|
_clip.dispatch(ClipEventType.BUFFER_FULL);
|
|
break;
|
|
case HLSPlayStates.PLAYING_BUFFERING:
|
|
case HLSPlayStates.PAUSED_BUFFERING:
|
|
_clip.dispatch(ClipEventType.BUFFER_EMPTY);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Starts loading the specified clip. Once video data is available the provider
|
|
* must set it to the clip using <code>clip.setContent()</code>. Typically the video
|
|
* object passed to the clip is an instance of <a href="http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/media/Video.html">flash.media.Video</a>.
|
|
*
|
|
* @param event the event that this provider should dispatch once loading has successfully started,
|
|
* once dispatched the player will call <code>getVideo()</code>
|
|
* @param clip the clip to load
|
|
* @param pauseAfterStart if <code>true</code> the playback is paused on first frame and
|
|
* buffering is continued
|
|
* @see Clip#setContent()
|
|
* @see #getVideo()
|
|
*/
|
|
public function load(event : ClipEvent, clip : Clip, pauseAfterStart : Boolean = true) : void {
|
|
_clip = clip;
|
|
CONFIG::LOGGING {
|
|
Log.info("load()" + clip.completeUrl);
|
|
}
|
|
_hls.load(clip.completeUrl);
|
|
_pauseAfterStart = pauseAfterStart;
|
|
_durationCapped = clip.duration;
|
|
_clipStart = clip.start;
|
|
clip.type = ClipType.VIDEO;
|
|
clip.dispatch(ClipEventType.BEGIN);
|
|
clip.setNetStream(_hls.stream);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Gets the <a href="http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/media/Video.html">Video</a> object.
|
|
* A stream will be attached to the returned video object using <code>attachStream()</code>.
|
|
* @param clip the clip for which the Video object is queried for
|
|
* @see #attachStream()
|
|
*/
|
|
public function getVideo(clip : Clip) : DisplayObject {
|
|
CONFIG::LOGGING {
|
|
Log.debug("getVideo()");
|
|
}
|
|
if (_video == null) {
|
|
if (clip.useStageVideo) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("useStageVideo");
|
|
}
|
|
_video = new StageVideoWrapper(clip);
|
|
} else {
|
|
_video = new Video();
|
|
_video.smoothing = clip.smoothing;
|
|
}
|
|
}
|
|
return _video;
|
|
}
|
|
|
|
/**
|
|
* Attaches a stream to the specified display object.
|
|
* @param video the video object that was originally retrieved using <code>getVideo()</code>.
|
|
* @see #getVideo()
|
|
*/
|
|
public function attachStream(video : DisplayObject) : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug("attachStream()");
|
|
}
|
|
if(_streamAttached == false) {
|
|
Video(video).attachNetStream(_hls.stream);
|
|
_streamAttached = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Pauses playback.
|
|
* @param event the event that this provider should dispatch once loading has been successfully paused
|
|
*/
|
|
public function pause(event : ClipEvent) : void {
|
|
CONFIG::LOGGING {
|
|
Log.info("pause()");
|
|
}
|
|
_hls.stream.pause();
|
|
if (event) {
|
|
_clip.dispatch(ClipEventType.PAUSE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Resumes playback.
|
|
* @param event the event that this provider should dispatch once loading has been successfully resumed
|
|
*/
|
|
public function resume(event : ClipEvent) : void {
|
|
CONFIG::LOGGING {
|
|
Log.info("resume()");
|
|
}
|
|
_hls.stream.resume();
|
|
if (event) {
|
|
_clip.dispatch(ClipEventType.RESUME);
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Stops and rewinds to the beginning of current clip.
|
|
* @param event the event that this provider should dispatch once loading has been successfully stopped
|
|
*/
|
|
public function stop(event : ClipEvent, closeStream : Boolean = false) : void {
|
|
CONFIG::LOGGING {
|
|
Log.info("stop()");
|
|
}
|
|
_hls.stream.close();
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Seeks to the specified point in the timeline.
|
|
* @param event the event that this provider should dispatch once the seek is in target
|
|
* @param seconds the target point in the timeline
|
|
*/
|
|
public function seek(event : ClipEvent, seconds : Number) : void {
|
|
CONFIG::LOGGING {
|
|
Log.info("seek(" + seconds + ")");
|
|
}
|
|
if (Math.abs(time - seconds) > 0.2) {
|
|
// real seek position : add clip.start offset
|
|
_hls.stream.seek(seconds + _clipStart);
|
|
} else {
|
|
CONFIG::LOGGING {
|
|
Log.warn("seek(" + seconds + ") to current position, discard");
|
|
}
|
|
}
|
|
if (event) {
|
|
_clip.dispatch(ClipEventType.SEEK, seconds);
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* File size in bytes.
|
|
*/
|
|
public function get fileSize() : Number {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Current playhead time in seconds.
|
|
*/
|
|
public function get time() : Number {
|
|
var _time : Number = Math.max(0, _hls.position - _clipStart);
|
|
return _time;
|
|
}
|
|
|
|
/**
|
|
* The point in timeline where the buffered data region begins, in seconds.
|
|
*/
|
|
public function get bufferStart() : Number {
|
|
var _bufferStart : Number;
|
|
if (!_durationCapped) {
|
|
_bufferStart = Math.min(_hls.position - _hls.stream.backBufferLength - _clipStart, _duration);
|
|
} else {
|
|
_bufferStart = Math.min(_hls.position - _hls.stream.backBufferLength - _clipStart, _durationCapped);
|
|
}
|
|
return Math.max(_bufferStart, 0);
|
|
}
|
|
|
|
/**
|
|
* The point in timeline where the buffered data region ends, in seconds.
|
|
*/
|
|
public function get bufferEnd() : Number {
|
|
var _bufferEnd : Number;
|
|
if (!_durationCapped) {
|
|
_bufferEnd = Math.min(_hls.stream.bufferLength + _hls.position - _clipStart, _duration);
|
|
} else {
|
|
_bufferEnd = Math.min(_hls.stream.bufferLength + _hls.position - _clipStart, _durationCapped);
|
|
}
|
|
return Math.max(_bufferEnd, 0);
|
|
}
|
|
|
|
/**
|
|
* Does this provider support random seeking to unbuffered areas in the timeline?
|
|
*/
|
|
public function get allowRandomSeek() : Boolean {
|
|
// CONFIG::LOGGING {
|
|
// Log.info("allowRandomSeek()");
|
|
// }
|
|
return _seekable;
|
|
}
|
|
|
|
/**
|
|
* Volume controller used to control the video volume.
|
|
*/
|
|
public function set volumeController(controller : VolumeController) : void {
|
|
_volumecontroller = controller;
|
|
_volumecontroller.netStream = _hls.stream;
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Is this provider in the process of stopping the stream?
|
|
* When stopped the provider should not dispatch any events resulting from events that
|
|
* might get triggered by the underlying streaming implementation.
|
|
*/
|
|
public function get stopping() : Boolean {
|
|
CONFIG::LOGGING {
|
|
Log.info("stopping()");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* The playlist instance.
|
|
*/
|
|
public function set playlist(playlist : Playlist) : void {
|
|
// CONFIG::LOGGING {
|
|
// Log.debug("set playlist()");
|
|
// }
|
|
_playlist = playlist;
|
|
return;
|
|
}
|
|
|
|
public function get playlist() : Playlist {
|
|
CONFIG::LOGGING {
|
|
Log.debug("get playlist()");
|
|
}
|
|
return _playlist;
|
|
}
|
|
|
|
/**
|
|
* Adds a callback public function to the NetConnection instance. This public function will fire ClipEvents whenever
|
|
* the callback is invoked in the connection.
|
|
* @param name
|
|
* @param listener
|
|
* @return
|
|
* @see ClipEventType#CONNECTION_EVENT
|
|
*/
|
|
public function addConnectionCallback(name : String, listener : Function) : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug("addConnectionCallback()");
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Adds a callback public function to the NetStream object. This public function will fire a ClipEvent of type StreamEvent whenever
|
|
* the callback has been invoked on the stream. The invokations typically come from a server-side app running
|
|
* on RTMP server.
|
|
* @param name
|
|
* @param listener
|
|
* @return
|
|
* @see ClipEventType.NETSTREAM_EVENT
|
|
*/
|
|
public function addStreamCallback(name : String, listener : Function) : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug("addStreamCallback()");
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Get the current stream callbacks.
|
|
* @return a dictionary of callbacks, keyed using callback names and values being the callback functions
|
|
*/
|
|
public function get streamCallbacks() : Dictionary {
|
|
CONFIG::LOGGING {
|
|
Log.debug("get streamCallbacks()");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets the underlying NetStream object.
|
|
* @return the netStream currently in use, or null if this provider has not started streaming yet
|
|
*/
|
|
public function get netStream() : NetStream {
|
|
CONFIG::LOGGING {
|
|
Log.debug("get netStream()");
|
|
}
|
|
return _hls.stream;
|
|
}
|
|
|
|
/**
|
|
* Gets the underlying netConnection object.
|
|
* @return the netConnection currently in use, or null if this provider has not started streaming yet
|
|
*/
|
|
public function get netConnection() : NetConnection {
|
|
CONFIG::LOGGING {
|
|
Log.debug("get netConnection()");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets a time provider to be used by this StreamProvider. Normally the playhead time is queried from
|
|
* the NetStream.time property.
|
|
*
|
|
* @param timeProvider
|
|
*/
|
|
public function set timeProvider(timeProvider : TimeProvider) : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug("set timeProvider()");
|
|
}
|
|
_timeProvider = timeProvider;
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Gets the type of StreamProvider either http, rtmp, psuedo.
|
|
*/
|
|
public function get type() : String {
|
|
return "httpstreaming";
|
|
}
|
|
|
|
/**
|
|
* Switch the stream in realtime with / without dynamic stream switching support
|
|
*
|
|
* @param event ClipEvent the clip event
|
|
* @param clip Clip the clip to switch to
|
|
* @param netStreamPlayOptions Object the NetStreamPlayOptions object to enable dynamic stream switching
|
|
*/
|
|
public function switchStream(event : ClipEvent, clip : Clip, netStreamPlayOptions : Object = null) : void {
|
|
CONFIG::LOGGING {
|
|
Log.info("switchStream()");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|