/** * @author Jefferson González * @copyright 2010 Jefferson González * * @license * This file is part of Jaris FLV Player. * * Jaris FLV Player is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License or GNU LESSER GENERAL * PUBLIC LICENSE as published by the Free Software Foundation, either version * 3 of the License, or (at your option) any later version. * * Jaris FLV Player is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License and * GNU LESSER GENERAL PUBLIC LICENSE along with Jaris FLV Player. If not, * see . */ package jaris.player; import flash.display.Loader; import flash.display.MovieClip; import flash.display.Sprite; import flash.display.Stage; import flash.display.BitmapData; import flash.display.JPEGEncoderOptions; import flash.utils.ByteArray; import flash.display.StageDisplayState; import flash.events.AsyncErrorEvent; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.FullScreenEvent; import flash.events.IOErrorEvent; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.events.NetStatusEvent; import flash.events.ProgressEvent; import flash.events.TimerEvent; import flash.events.StatusEvent; import flash.geom.Rectangle; import flash.Lib; import flash.media.ID3Info; import flash.media.Sound; import flash.media.SoundChannel; import flash.media.SoundTransform; import flash.media.Video; import flash.media.Camera; import flash.net.NetConnection; import flash.net.NetStream; import flash.net.URLRequest; import flash.system.Capabilities; import flash.system.Security; import flash.system.SecurityPanel; import flash.ui.Keyboard; import flash.ui.Mouse; import flash.utils.Timer; import jaris.events.PlayerEvents; import jaris.utils.Utils; import jaris.player.AspectRatio; import jaris.player.UserSettings; import flash.errors.IOError; import flash.geom.Matrix; /** * Jaris main video player */ class Player extends EventDispatcher { //{Member variables private var _stage:Stage; private var _movieClip:MovieClip; private var _connection:NetConnection; private var _stream:NetStream; private var _fullscreen:Bool; private var _soundMuted:Bool; private var _volume:Float; private var _bufferTime:Float; private var _mouseVisible:Bool; private var _mediaLoaded:Bool; private var _hideMouseTimer:Timer; private var _checkAudioTimer:Timer; private var _mediaSource:String; private var _type:String; private var _streamType:String; private var _server:String; //For future use on rtmp private var _sound:Sound; private var _soundChannel:SoundChannel; private var _id3Info:ID3Info; private var _video:Video; private var _videoWidth:Float; private var _videoHeight:Float; private var _scaleWidth:Int; private var _scaleHeight:Int; public var _naturalWidth:Float; public var _naturalHeight:Float; public var fullMetaData:Dynamic; private var _videoMask:Sprite; private var _videoQualityHigh:Bool; private var _mediaDuration:Float; private var _lastTime:Float; private var _lastProgress:Float; public var _isPlaying:Bool; private var _aspectRatio:Float; private var _currentAspectRatio:String; private var _originalAspectRatio:Float; private var _mediaEndReached:Bool; private var _seekPoints:Array; private var _downloadCompleted:Bool; private var _startTime:Float; private var _firstLoad:Bool; private var _stopped:Bool; private var _useHardWareScaling:Bool; private var _youtubeLoader:Loader; private var _userSettings:UserSettings; private var _loadedYoutube:Bool; private var _ytReady:Bool; private var _ytCue:Bool; public var noAPITrigger:Bool; public var _showLoader:Bool; private var _preLoading:Bool; private var _requestedPlay:Bool; private var _requestedLoad:Bool; private var _allowPreloadedPlay:Bool; private var _hasPoster:Bool; public var lastSeekTime:Float; private var _cam:Camera; public var camLength:Int; //} //{Constructor public function new() { super(); //{Main Variables Init _stage = Lib.current.stage; _movieClip = Lib.current; _mouseVisible = true; _soundMuted = false; _volume = 1.0; _bufferTime = 10; _fullscreen = false; _mediaLoaded = false; _requestedLoad = false; _hideMouseTimer = new Timer(1500); _checkAudioTimer = new Timer(100); _seekPoints = new Array(); _downloadCompleted = false; _startTime = 0; _firstLoad = true; _stopped = false; _videoQualityHigh = false; _loadedYoutube = false; _ytReady = false; _ytCue = false; _isPlaying = false; _streamType = StreamType.FILE; _type = InputType.VIDEO; _server = ""; _currentAspectRatio = "original"; _aspectRatio = 0; _lastTime = 0; _lastProgress = 0; _userSettings = new UserSettings(); noAPITrigger = false; _hasPoster = false; _showLoader = true; lastSeekTime = 0; _allowPreloadedPlay = false; _naturalWidth = 0; _naturalHeight = 0; camLength = Camera.names.length; //} //{Initialize sound object _sound = new Sound(); _sound.addEventListener(Event.COMPLETE, onSoundComplete); _sound.addEventListener(Event.ID3, onSoundID3); _sound.addEventListener(IOErrorEvent.IO_ERROR, onSoundIOError); _sound.addEventListener(ProgressEvent.PROGRESS, onSoundProgress); //} //{Initialize video and connection objects _connection = new NetConnection(); _connection.client = this; _connection.connect(null); _stream = new NetStream(_connection); _video = new Video(_stage.stageWidth, _stage.stageHeight); _movieClip.addChild(_video); //} //Video mask so that custom menu items work _videoMask = new Sprite(); _movieClip.addChild(_videoMask); //Set initial rendering to high quality toggleQuality(); //{Initialize system event listeners _movieClip.addEventListener(Event.ENTER_FRAME, onEnterFrame); _stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); _stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove); _stage.addEventListener(FullScreenEvent.FULL_SCREEN, onFullScreen); _stage.addEventListener(Event.RESIZE, onResize); _hideMouseTimer.addEventListener(TimerEvent.TIMER, hideMouseTimer); _checkAudioTimer.addEventListener(TimerEvent.TIMER, checkAudioTimer); _connection.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); _connection.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); _scaleWidth = Std.int(_stage.stageWidth); _scaleHeight = Std.int(_stage.stageHeight); //} } //} public function init():Void { var parameters:Dynamic = flash.Lib.current.loaderInfo.parameters; if (parameters.poster == "" && _streamType == StreamType.YOUTUBE && _mediaSource != null) { _ytCue = true; noAPITrigger = true; load(_mediaSource, _type, _streamType, _server); noAPITrigger = false; } else if(_streamType == StreamType.USER){ getUserMedia(); } } private function getUserMedia():Void { var status; var aspect; _cam = Camera.getCamera(); _mediaSource = ''; if(camLength > 0){ _cam.addEventListener(StatusEvent.STATUS, userStatusHandler); _video.visible = false; aspect = _cam.height / _cam.width; _cam.setMode(640, Std.int(640 * aspect), 25, true); _video.attachCamera(_cam); if(!_cam.muted){ status = new StatusEvent('status', false, false, 'Camera.Unmuted'); userStatusHandler(status); } } else { callEvents(PlayerEvents.USERNOTSUPPORTED); } } public function attachUsermedia():Void { if(_streamType == StreamType.USER){ addEventListener(PlayerEvents.PLAY_PAUSE, onUserPlayPause); if(_isPlaying){ onUserPlayPause(); } } } public function detachUsermedia():Void { if(_streamType == StreamType.USER){ removeEventListener(PlayerEvents.PLAY_PAUSE, onUserPlayPause); _video.attachCamera(null); } } private function onUserPlayPause(event = null){ var data; if(_isPlaying){ data = { width: _cam.width, height: _cam.height, duration: Math.POSITIVE_INFINITY }; _video.attachCamera(_cam); _video.visible = true; if(_firstLoad){ onMetaData(data); } } else { _video.attachCamera(null); } } private function userStatusHandler(event){ if(event.code == 'Camera.Unmuted'){ _videoHeight = event.target.height; _videoWidth = event.target.width; _originalAspectRatio = AspectRatio.getAspectRatio(_videoWidth, _videoHeight); _mediaLoaded = true; if (_aspectRatio <= 0) { _aspectRatio = _originalAspectRatio; } _video.attachCamera(null); callEvents(PlayerEvents.USERSUCCESS); } else { callEvents(PlayerEvents.USERDENIED); } } public function createScreenShot() { var ret = ''; if(_mediaLoaded && !_firstLoad){ var vwidth = Std.int(_videoWidth); var vheight = Std.int(_videoHeight); var matrix:Matrix = new Matrix(); //return 'sw: '+ swidth+' sh: '+ sheight+' vw: '+ vwidth +' vh: '+ vheight; matrix.scale(vwidth / _scaleWidth, vheight / _scaleHeight); var qImageData:BitmapData = new BitmapData(vwidth, vheight, false, 0x00FF00); var byteArray:ByteArray = new ByteArray(); qImageData.draw(_video, matrix, null, null, null, true); qImageData.encode(new Rectangle(0, 0, vwidth, vheight), new flash.display.JPEGEncoderOptions(), byteArray); ret = Utils.enocdeBytesData(byteArray); } return ret; } //{Timers /** * Timer that hides the mouse pointer when it is idle and dispatch the PlayerEvents.MOUSE_HIDE * @param event */ private function hideMouseTimer(event:TimerEvent):Void { if (_fullscreen) { if (_mouseVisible) { _mouseVisible = false; } else { Mouse.hide(); callEvents(PlayerEvents.MOUSE_HIDE); _hideMouseTimer.stop(); } } } /** * To check if the sound finished playing * @param event */ private function checkAudioTimer(event:TimerEvent):Void { if (_soundChannel.position + 100 >= _sound.length) { _isPlaying = false; _mediaEndReached = true; callEvents(PlayerEvents.PLAYBACK_FINISHED); _checkAudioTimer.stop(); } } //} //{Events /** * Callback after bandwidth calculation for rtmp streams */ private function onBWDone():Void { //Need to study this more } /** * Triggers error event on rtmp connections * @param event */ private function onAsyncError(event:AsyncErrorEvent):Void { //TODO: Should trigger event for controls to display error message trace(event.error); } /** * Checks if connection failed or succeed * @param event */ private function onNetStatus(event:NetStatusEvent):Void { switch (event.info.code) { case "NetConnection.Connect.Success": if (_streamType == StreamType.RTMP) { _stream = new NetStream(_connection); _stream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); _stream.bufferTime = 10; _stream.play(Utils.rtmpSourceParser(_mediaSource), true); _stream.client = this; if (_type == InputType.VIDEO) {_video.attachNetStream(_stream); } } callEvents(PlayerEvents.CONNECTION_SUCCESS); case "NetStream.Play.StreamNotFound": trace("Stream not found: " + _mediaSource); //Replace with a dispatch for error event callEvents(PlayerEvents.CONNECTION_FAILED); case "NetStream.Play.Stop": if (_streamType != StreamType.RTMP) { if (_isPlaying) { _stream.togglePause(); } _isPlaying = false; _mediaEndReached = true; callEvents(PlayerEvents.PLAYBACK_FINISHED); } case "NetStream.Play.Start": _mediaEndReached = false; if (_isPlaying && (_stream.bytesLoaded != _stream.bytesTotal || _streamType == StreamType.RTMP)) { callEvents(PlayerEvents.BUFFERING); } case "NetStream.Seek.Notify": _mediaEndReached = false; if (_streamType == StreamType.RTMP) { _isPlaying = true; callEvents(PlayerEvents.PLAY_PAUSE); callEvents(PlayerEvents.BUFFERING); } case "NetStream.Buffer.Empty": if (_stream.bytesLoaded != _stream.bytesTotal) { callEvents(PlayerEvents.BUFFERING); } case "NetStream.Buffer.Full": callEvents(PlayerEvents.NOT_BUFFERING); case "NetStream.Buffer.Flush": if (_stream.bytesLoaded == _stream.bytesTotal) { _downloadCompleted = true; } } } /** * Proccess keyboard shortcuts * @param event */ private function onKeyDown(event:KeyboardEvent):Void { var F_KEY:UInt = 70; var M_KEY:UInt = 77; switch(event.keyCode) { case F_KEY: toggleFullscreen(); case M_KEY: toggleMute(); case Keyboard.SPACE: togglePlay(); } } /** * IF player is full screen shows the mouse when gets hide * @param event */ private function onMouseMove(event:MouseEvent):Void { if (_fullscreen && !_mouseVisible) { if (!_hideMouseTimer.running) { _hideMouseTimer.start(); } _mouseVisible = true; Mouse.show(); callEvents(PlayerEvents.MOUSE_SHOW); } } /** * Resize video player * @param event */ private function onResize(event:Event):Void { resizeAndCenterPlayer(); } /** * Dispath a full screen event to listeners as redraw player an takes care of some other aspects * @param event */ private function onFullScreen(event:FullScreenEvent):Void { _fullscreen = event.fullScreen; if (!event.fullScreen) { Mouse.show(); callEvents(PlayerEvents.MOUSE_SHOW); _mouseVisible = true; } else { _mouseVisible = true; _hideMouseTimer.start(); } resizeAndCenterPlayer(); callEvents(PlayerEvents.FULLSCREEN); } /** * Sits for any cue points available * @param data * @note Planned future implementation */ private function onCuePoint(data:Dynamic):Void { } /** * After a video is loaded this callback gets the video information at start and stores it on variables * @param data */ private function onMetaData(data:Dynamic):Void { var i, fields; if (_firstLoad) { _isPlaying = _preLoading ? false : true; _firstLoad = false; if (data.width) { _videoWidth = data.width; _videoHeight = data.height; } else { _videoWidth = _video.width; _videoHeight = _video.height; } _naturalWidth = _video.videoWidth; _naturalHeight = _video.videoHeight; if (_naturalWidth == 0) { _naturalWidth = _videoWidth; } if (_naturalHeight == 0) { _naturalHeight = _videoHeight; } //Store seekpoints times if (data.hasOwnProperty("seekpoints")) { for (position in Reflect.fields(data.seekpoints)) { _seekPoints.push(Reflect.field(data.seekpoints, position).time); } } else if (data.hasOwnProperty("keyframes")) { for (position in Reflect.fields(data.keyframes.times)) { _seekPoints.push(Reflect.field(data.keyframes.times, position)); } } _mediaLoaded = true; _mediaDuration = data.duration; _originalAspectRatio = AspectRatio.getAspectRatio(_videoWidth, _videoHeight); if (_aspectRatio <= 0) { _aspectRatio = _originalAspectRatio; } fields = Reflect.fields(data); fullMetaData = { }; for (i in fields) { Reflect.setField(fullMetaData, i, Reflect.field(data, i)); } callEvents(PlayerEvents.MEDIA_INITIALIZED); resizeAndCenterPlayer(); //Retrieve the volume that user selected last time setVolume(_userSettings.getVolume()); } } /** * Dummy function invoked for pseudostream servers * @param data */ private function onLastSecond(data:Dynamic):Void { trace("last second pseudostream"); } /** * Broadcast Timeupdate and Duration */ private function onEnterFrame(event:Event):Void { if (getDuration() > 0 && _lastTime != getCurrentTime()) { _lastTime = getCurrentTime(); callEvents(PlayerEvents.TIME); } if (getBytesLoaded() > 0 && _lastProgress < getBytesLoaded()) { _lastProgress = getBytesLoaded(); callEvents(PlayerEvents.PROGRESS); } } /** * Triggers when playbacks end on rtmp streaming server */ private function onPlayStatus(info:Dynamic):Void { _isPlaying = false; _mediaEndReached = true; callEvents(PlayerEvents.PLAYBACK_FINISHED); } /** * When sound finished downloading * @param event */ private function onSoundComplete(event:Event) { _mediaDuration = _sound.length / 1000; _downloadCompleted = true; callEvents(PlayerEvents.MEDIA_INITIALIZED); } /** * Mimic stream onMetaData * @param event */ private function onSoundID3(event:Event) { if (_firstLoad) { _soundChannel = _sound.play(); _checkAudioTimer.start(); _isPlaying = true; _firstLoad = false; _mediaLoaded = true; _mediaDuration = ((_sound.bytesTotal / _sound.bytesLoaded) * _sound.length) / 1000; _aspectRatio = AspectRatio.getAspectRatio(_videoWidth, _videoHeight); _originalAspectRatio = _aspectRatio; _id3Info = _sound.id3; callEvents(PlayerEvents.CONNECTION_SUCCESS); callEvents(PlayerEvents.MEDIA_INITIALIZED); resizeAndCenterPlayer(); //Retrieve the volume that user selected last time setVolume(_userSettings.getVolume()); } } /** * Dispatch connection failed event on error * @param event */ private function onSoundIOError(event:IOErrorEvent) { callEvents(PlayerEvents.CONNECTION_FAILED); } /** * Monitor sound download progress * @param event */ private function onSoundProgress(event:ProgressEvent) { var oldDuration = _mediaDuration; if (_sound.isBuffering) { callEvents(PlayerEvents.BUFFERING); } else { callEvents(PlayerEvents.NOT_BUFFERING); } _mediaDuration = ((_sound.bytesTotal / _sound.bytesLoaded) * _sound.length) / 1000; if (_mediaDuration != oldDuration) { callEvents(PlayerEvents.MEDIA_INITIALIZED); } } /** * Initializes the youtube loader object * @param event */ private function onYouTubeLoaderInit(event:Event):Void { _youtubeLoader.content.addEventListener("onReady", onYoutubeReady); _youtubeLoader.content.addEventListener("onError", onYoutubeError); _youtubeLoader.content.addEventListener("onStateChange", onYoutubeStateChange); _youtubeLoader.content.addEventListener("onPlaybackQualityChange", onYoutubePlaybackQualityChange); } /** * This event is fired when the player is loaded and initialized, meaning it is ready to receive API calls. */ private function onYoutubeReady(event:Event):Void { _movieClip.addChild(_youtubeLoader.content); _movieClip.setChildIndex(_youtubeLoader.content, 0); _ytReady = true; Reflect.field(_youtubeLoader.content, "setSize")(_stage.stageWidth, _stage.stageHeight); if (_ytCue && !_isPlaying && !_requestedPlay) { Reflect.field(_youtubeLoader.content, "cueVideoByUrl")(Utils.youtubeSourceParse(_mediaSource), 0, Utils.youtubeQualitySourceParse(_mediaSource)); } else { Reflect.field(_youtubeLoader.content, "loadVideoByUrl")(Utils.youtubeSourceParse(_mediaSource), 0, Utils.youtubeQualitySourceParse(_mediaSource)); callEvents(PlayerEvents.BUFFERING); } } /** * This event is fired whenever the player's state changes. Possible values are unstarted (-1), ended (0), * playing (1), paused (2), buffering (3), video cued (5). When the SWF is first loaded it will broadcast * an unstarted (-1) event. When the video is cued and ready to play it will broadcast a video cued event (5). * @param event */ private function onYoutubeStateChange(event:Event):Void { var status:UInt = Std.parseInt(Reflect.field(event, "data")); var oldPlaying:Bool = _isPlaying; var quality; if (!_mediaLoaded && _ytCue && !_isPlaying && _requestedPlay) { play(); } _mediaLoaded = true; switch(status) { case -1: callEvents(PlayerEvents.BUFFERING); case 0: _isPlaying = false; _mediaEndReached = true; callEvents(PlayerEvents.PLAYBACK_FINISHED); case 1: _isPlaying = true; if (_firstLoad) { _videoWidth = _stage.stageWidth; _videoHeight = _stage.stageHeight; quality = Reflect.field(_youtubeLoader.content, "getPlaybackQuality")(); switch(quality) { case 'small': _naturalHeight = 240; _naturalWidth = 320; case 'medium': _naturalHeight = 360; _naturalWidth = 640; case 'large': _naturalHeight = 480; _naturalWidth = 853; case 'hd720': _naturalHeight = 720; _naturalWidth = 1280; case 'hd1080': _naturalHeight = 1080; _naturalWidth = 1920; case 'highres': _naturalHeight = 1080; _naturalWidth = 1920; } if (_naturalWidth == 0) { _naturalWidth = _videoWidth; } if (_naturalHeight == 0) { _naturalHeight = _videoHeight; } _firstLoad = false; _mediaDuration = Reflect.field(_youtubeLoader.content, "getDuration")(); _aspectRatio = AspectRatio.getAspectRatio(_videoWidth, _videoHeight); _originalAspectRatio = _aspectRatio; callEvents(PlayerEvents.CONNECTION_SUCCESS); callEvents(PlayerEvents.MEDIA_INITIALIZED); resizeAndCenterPlayer(); //Retrieve the volume that user selected last time setVolume(_userSettings.getVolume()); } callEvents(PlayerEvents.NOT_BUFFERING); if (oldPlaying != _isPlaying) { callEvents(PlayerEvents.PLAY_PAUSE); } case 2: _isPlaying = false; callEvents(PlayerEvents.NOT_BUFFERING); if (oldPlaying != _isPlaying) { callEvents(PlayerEvents.PLAY_PAUSE); } case 3: callEvents(PlayerEvents.BUFFERING); case 5: callEvents(PlayerEvents.NOT_BUFFERING); } } /** * This event is fired whenever the video playback quality changes. For example, if you call the * setPlaybackQuality(suggestedQuality) function, this event will fire if the playback quality actually * changes. Your code should respond to the event and should not assume that the quality will automatically * change when the setPlaybackQuality(suggestedQuality) function is called. Similarly, your code should not * assume that playback quality will only change as a result of an explicit call to setPlaybackQuality or any * other function that allows you to set a suggested playback quality. * * The value that the event broadcasts is the new playback quality. Possible values are "small", "medium", * "large" and "hd720". * @param event */ private function onYoutubePlaybackQualityChange(event:Event):Void { //trace(Reflect.field(event, "data")); } /** * This event is fired when an error in the player occurs. The possible error codes are 100, 101, * and 150. The 100 error code is broadcast when the video requested is not found. This occurs when * a video has been removed (for any reason), or it has been marked as private. The 101 error code is * broadcast when the video requested does not allow playback in the embedded players. The error code * 150 is the same as 101, it's just 101 in disguise! * @param event */ private function onYoutubeError(event:Event):Void { trace(Reflect.field(event, "data")); } //} //{Private Methods /** * Function used each time is needed to dispatch an event * @param type */ private function callEvents(type:String):Void { var playerEvent:PlayerEvents = new PlayerEvents(type, true); playerEvent.aspectRatio = getAspectRatio(); playerEvent.duration = getDuration(); playerEvent.fullscreen = isFullscreen(); playerEvent.mute = getMute(); playerEvent.volume = getVolume(); playerEvent.width = _video.width; playerEvent.height = _video.height; playerEvent.stream = getNetStream(); playerEvent.sound = getSound(); playerEvent.time = getCurrentTime(); playerEvent.id3Info = getId3Info(); playerEvent.seekTime = lastSeekTime; dispatchEvent(playerEvent); } /** * Reposition and resizes the video player to fit on screen */ private function resizeAndCenterPlayer():Void { if (_streamType != StreamType.YOUTUBE) { _video.height = _stage.stageHeight; _video.width = _video.height * _aspectRatio; _video.x = (_stage.stageWidth / 2) - (_video.width / 2); _video.y = 0; if (_video.width > _stage.stageWidth && _aspectRatio == _originalAspectRatio) { var aspectRatio:Float = _videoHeight / _videoWidth; _video.width = _stage.stageWidth; _video.height = aspectRatio * _video.width; _video.x = 0; _video.y = (_stage.stageHeight / 2) - (_video.height / 2); } _videoMask.graphics.clear(); _videoMask.graphics.lineStyle(); _videoMask.graphics.beginFill(0x000000, 0); _videoMask.graphics.drawRect(_video.x, _video.y, _video.width, _video.height); _videoMask.graphics.endFill(); } else { Reflect.field(_youtubeLoader.content, "setSize")(_stage.stageWidth, _stage.stageHeight); _videoMask.graphics.clear(); _videoMask.graphics.lineStyle(); _videoMask.graphics.beginFill(0x000000, 0); _videoMask.graphics.drawRect(0, 0, _stage.stageWidth, _stage.stageHeight); _videoMask.graphics.endFill(); } callEvents(PlayerEvents.RESIZE); } /** * Check the best seek point available if the seekpoints array is available * @param time time in seconds * @return best seek point in seconds or given one if no seekpoints array is available */ private function getBestSeekPoint(time:Float):Float { if (_seekPoints.length > 0) { var timeOne:String = "0"; var timeTwo:String = "0"; for (prop in Reflect.fields(_seekPoints)) { if (Reflect.field(_seekPoints, prop) < time) { timeOne = prop; } else { timeTwo = prop; break; } } if (time - _seekPoints[Std.parseInt(timeOne)] < _seekPoints[Std.parseInt(timeTwo)] - time) { return _seekPoints[Std.parseInt(timeOne)]; } else { return _seekPoints[Std.parseInt(timeTwo)]; } } return time; } /** * Checks if the given seek time is already buffered * @param time time in seconds * @return true if can seek false if not in buffer */ private function canSeek(time:Float):Bool { if (_type == InputType.VIDEO) { time = getBestSeekPoint(time); } var cacheTotal = Math.floor((getDuration() - _startTime) * (getBytesLoaded() / getBytesTotal())) - 1; if (time >= _startTime && time < _startTime + cacheTotal) { return true; } return false; } //} //{Public methods /** * Preload a video without playing */ public function preload():Void { if (!_requestedLoad && !_mediaLoaded && _mediaSource != "" && !_requestedPlay) { var isVideo = (_type == InputType.VIDEO && (_streamType == StreamType.FILE || _streamType == StreamType.PSEUDOSTREAM)); var isAudio = (_type == InputType.AUDIO && _streamType == StreamType.FILE); if (isVideo || isAudio) { stopAndClose(); load(_mediaSource, _type, _streamType, _server, true); _stopped = false; _mediaLoaded = false; _firstLoad = true; _startTime = 0; _downloadCompleted = false; _preLoading = true; _allowPreloadedPlay = true; } } } //{Public methods /** * Loads a video and starts playing it * @param video video url to load */ public function load(source:String, type:String = "video", streamType:String = "file", server:String = "", preload:Bool = false):Void { if (!_requestedLoad && !_mediaLoaded && (_streamType != StreamType.YOUTUBE || !_loadedYoutube)) { stopAndClose(); _type = type; _streamType = streamType; _mediaSource = source; _stopped = false; _mediaLoaded = false; _firstLoad = true; _startTime = 0; _downloadCompleted = false; _seekPoints = new Array(); _server = server; _requestedLoad = true; if (!preload) { callEvents(PlayerEvents.BUFFERING); } if (_streamType == StreamType.YOUTUBE) { Security.allowDomain("*"); Security.allowDomain("www.youtube.com"); Security.allowDomain("youtube.com"); Security.allowDomain("i4.ytimg.com"); Security.allowDomain("s.ytimg.com"); Security.allowDomain("i.ytimg.com"); _youtubeLoader = new Loader(); _youtubeLoader.contentLoaderInfo.addEventListener(Event.INIT, onYouTubeLoaderInit); _youtubeLoader.load(new URLRequest("http://www.youtube.com/apiplayer?version=3")); _loadedYoutube = true; } else if (_type == InputType.VIDEO && (_streamType == StreamType.FILE || _streamType == StreamType.PSEUDOSTREAM)) { Security.allowDomain("*"); _connection.connect(null); _stream = new NetStream(_connection); _stream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); _stream.bufferTime = _bufferTime; _stream.checkPolicyFile = true; _stream.play(source); _stream.client = this; _video.attachNetStream(_stream); if (preload) { pause(); _isPlaying = false; } if (!preload || !_hasPoster) { _video.attachNetStream(_stream); } } else if (_streamType == StreamType.RTMP) { _connection.connect(_server); } else if (_type == InputType.AUDIO && _streamType == StreamType.FILE) { _sound.load(new URLRequest(source)); try { _soundChannel = _sound.play(); if (preload) { _soundChannel.stop(); _isPlaying = false; } else { _isPlaying = true; } _firstLoad = false; _mediaLoaded = true; } catch (error:IOError) { } } } } /** * Closes the connection and makes player available for another video */ public function stopAndClose():Void { if (_mediaLoaded || _requestedLoad) { _requestedLoad = false; _mediaLoaded = false; _isPlaying = false; _stopped = true; _startTime = 0; _allowPreloadedPlay = false; if (_streamType == StreamType.YOUTUBE) { Reflect.field(_youtubeLoader.content, "destroy")(); } else if (_type == InputType.VIDEO) { _stream.close(); } else { _soundChannel.stop(); _sound.close(); } } callEvents(PlayerEvents.STOP_CLOSE); } /** * Seeks video player to a given time in seconds * @param seekTime time in seconds to seek * @return current play time after seeking */ public function seek(seekTime:Float):Float { if (_startTime <= 1 && _downloadCompleted) { if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { _stream.seek(seekTime); } else if (_type == InputType.AUDIO) { _soundChannel.stop(); _soundChannel = _sound.play(seekTime * 1000); if (!_isPlaying) { _soundChannel.stop(); } setVolume(_userSettings.getVolume()); } } else if (_seekPoints.length > 0 && _streamType == StreamType.PSEUDOSTREAM) { seekTime = getBestSeekPoint(seekTime); if (canSeek(seekTime)) { _stream.seek(seekTime - _startTime); } else if (seekTime != _startTime) { _startTime = seekTime; var url:String; if (_mediaSource.indexOf("?") != -1) { url = _mediaSource + "&start=" + seekTime; } else { url = _mediaSource + "?start=" + seekTime; } _stream.play(url); } } else if (_streamType == StreamType.YOUTUBE) { if (!canSeek(seekTime)) { if (_ytReady) { _startTime = seekTime; Reflect.field(_youtubeLoader.content, "seekTo")(seekTime); } } else if (_ytReady) { Reflect.field(_youtubeLoader.content, "seekTo")(seekTime); } } else if (_streamType == StreamType.RTMP) { // seekTime = getBestSeekPoint(seekTime); //Not Needed? _stream.seek(seekTime); } else if (canSeek(seekTime)) { if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { _stream.seek(seekTime); } else if (_type == InputType.AUDIO) { _soundChannel.stop(); _soundChannel = _sound.play(seekTime * 1000); if (!_isPlaying) { _soundChannel.stop(); } setVolume(_userSettings.getVolume()); } } lastSeekTime = seekTime; callEvents(PlayerEvents.SEEK); return seekTime; } /** * To check wheter the media is playing * @return true if is playing false otherwise */ public function isPlaying():Bool { return _isPlaying; } /** * Swithces between play and pause */ public function togglePlay():Bool { if (_isPlaying) { this.pause(); } else { this.play(); } return _isPlaying; } /** * pause */ public function pause():Bool { _requestedPlay = false; if (!_mediaEndReached) { if (_streamType == StreamType.YOUTUBE) { if (_ytReady) { Reflect.field(_youtubeLoader.content, "pauseVideo")(); } } else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { _stream.pause(); } else if (_type == InputType.AUDIO) { _soundChannel.stop(); } } if (_isPlaying) { _isPlaying = false; callEvents(PlayerEvents.PLAY_PAUSE); if (!_mediaLoaded) { callEvents(PlayerEvents.NOT_BUFFERING); } } return _isPlaying; } /** * */ public function play():Bool { if(_streamType != StreamType.USER){ _video.attachNetStream(_stream); } _preLoading = false; _requestedPlay = true; if (_mediaLoaded || _allowPreloadedPlay) { if (_mediaEndReached) { _mediaEndReached = false; if (_streamType == StreamType.YOUTUBE) { Reflect.field(_youtubeLoader.content, "seekTo")(0); Reflect.field(_youtubeLoader.content, "playVideo")(); } else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { _stream.seek(0); _stream.resume(); } else if (_type == InputType.AUDIO) { _checkAudioTimer.start(); _soundChannel = _sound.play(); setVolume(_userSettings.getVolume()); } } else { if (_streamType == StreamType.YOUTUBE) { if (_ytReady) { Reflect.field(_youtubeLoader.content, "playVideo")(); } } else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { _stream.resume(); } else if (_type == InputType.AUDIO) { if (!_isPlaying) { //If end of audio reached start from beggining if (_soundChannel.position + 100 >= _sound.length) { _soundChannel = _sound.play(); } else { _soundChannel = _sound.play(_soundChannel.position); } } setVolume(_userSettings.getVolume()); } } if (!_isPlaying) { _isPlaying = true; callEvents(PlayerEvents.PLAY_PAUSE); } return _isPlaying; } else if (_mediaSource != "") { load(_mediaSource, _type, _streamType, _server); callEvents(PlayerEvents.BUFFERING); _isPlaying = true; return true; } return true; } /** * Switches on or off fullscreen * @return true if fullscreen otherwise false */ public function toggleFullscreen():Bool { if (_fullscreen) { _stage.displayState = StageDisplayState.NORMAL; _stage.focus = _stage; return false; } else { if (_useHardWareScaling) { //Match full screen aspec ratio to desktop var aspectRatio = Capabilities.screenResolutionY / Capabilities.screenResolutionX; _stage.fullScreenSourceRect = new Rectangle(0, 0, _videoWidth, _videoWidth * aspectRatio); } else { //Use desktop resolution _stage.fullScreenSourceRect = new Rectangle(0, 0, Capabilities.screenResolutionX, Capabilities.screenResolutionY); } _stage.displayState = StageDisplayState.FULL_SCREEN; _stage.focus = _stage; return true; } } /** * Toggles betewen high and low quality image rendering * @return true if quality high false otherwise */ public function toggleQuality():Bool { if (_videoQualityHigh) { _video.smoothing = false; _video.deblocking = 1; } else { _video.smoothing = true; _video.deblocking = 5; } _videoQualityHigh = _videoQualityHigh ? false : true; return _videoQualityHigh; } /** * Mutes or unmutes the sound * @return true if muted false if unmuted */ public function toggleMute() { this.mute(!_soundMuted); } public function mute(goMuted:Bool) { var soundTransform:SoundTransform = new SoundTransform(); //unmute sound if (!goMuted) { _soundMuted = false; soundTransform.volume = _volume; } //mute sound else { _soundMuted = true; _volume = _stream.soundTransform.volume; soundTransform.volume = 0; _stream.soundTransform = soundTransform; _userSettings.setVolume(0); } if (_streamType == StreamType.YOUTUBE) { if (this._ytReady) { Reflect.field(_youtubeLoader.content, "setVolume")(soundTransform.volume * 100); } } else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { _stream.soundTransform = soundTransform; } else if (_type == InputType.AUDIO) { _soundChannel.soundTransform = soundTransform; setVolume(_userSettings.getVolume()); } callEvents(PlayerEvents.MUTE); } /** * Check if player is running on fullscreen mode * @return true if fullscreen false if not */ public function isFullscreen():Bool { return _stage.displayState == StageDisplayState.FULL_SCREEN; } //{Setters /** * Set input type * @param type Allowable values are audio, video */ public function setType(type:String):Void { _type = type; } /** * Set streaming type * @param streamType Allowable values are file, http, rmtp */ public function setStreamType(streamType:String):Void { _streamType = streamType; } /** * Sets the server url for rtmp streams * @param server */ public function setServer(server:String):Void { _server = server; } /** * To set the video source in case we dont want to start downloading at first so when use tooglePlay the * media is loaded automatically * @param source */ public function setSource(source):Void { _mediaSource = source; } /** * Changes the current volume * @param volume */ public function setVolume(volume:Float):Void { var soundTransform:SoundTransform = new SoundTransform(); _volume = volume; soundTransform.volume = volume; if (!_firstLoad) { if (_streamType == StreamType.YOUTUBE) { Reflect.field(_youtubeLoader.content, "setVolume")(soundTransform.volume * 100); } else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { _stream.soundTransform = soundTransform; } else if (_type == InputType.AUDIO) { _soundChannel.soundTransform = soundTransform; } } //Store volume into user settings _userSettings.setVolume(_volume); callEvents(PlayerEvents.VOLUME_CHANGE); } /** * Changes the buffer time for local and pseudo streaming * @param time in seconds */ public function setBufferTime(time:Float):Void { if (time > 0) { _bufferTime = time; } } /** * Show/Hide the Loader * @param true / false */ public function setLoader(val:Bool):Void { _showLoader = val; } /** * Changes the aspec ratio of current playing media and resizes video player * @param aspectRatio new aspect ratio value */ public function setAspectRatio(aspectRatio:Float):Void { _aspectRatio = aspectRatio; switch(_aspectRatio) { case 0.0: _currentAspectRatio = "original"; case AspectRatio._1_1: _currentAspectRatio = "1:1"; case AspectRatio._3_2: _currentAspectRatio = "3:2"; case AspectRatio._4_3: _currentAspectRatio = "4:3"; case AspectRatio._5_4: _currentAspectRatio = "5:4"; case AspectRatio._14_9: _currentAspectRatio = "14:9"; case AspectRatio._14_10: _currentAspectRatio = "14:10"; case AspectRatio._16_9: _currentAspectRatio = "16:9"; case AspectRatio._16_10: _currentAspectRatio = "16:10"; } resizeAndCenterPlayer(); //Store aspect ratio into user settings _userSettings.setAspectRatio(_aspectRatio); } /** * Enable or disable hardware scaling * @param value true to enable false to disable */ public function setHardwareScaling(value:Bool):Void { _useHardWareScaling = value; } //} //{Getters /** * Gets the volume amount 0.0 to 1.0 * @return */ public function getVolume():Float { return _volume; } /** * The current aspect ratio of the loaded Player * @return */ public function getAspectRatio():Float { return _aspectRatio; } /** * The current aspect ratio of the loaded Player in string format * @return */ public function getAspectRatioString():String { return _currentAspectRatio; } /** * Original aspect ratio of the video * @return original aspect ratio */ public function getOriginalAspectRatio():Float { return _originalAspectRatio; } /** * Total duration time of the loaded media * @return time in seconds */ public function getDuration():Float { return _mediaDuration; } /** * The time in seconds where the player started downloading * @return time in seconds */ public function getStartTime():Float { return _startTime; } /** * The stream associated with the player * @return netstream object */ public function getNetStream():NetStream { return _stream; } /** * Video object associated to the player * @return video object for further manipulation */ public function getVideo():Video { return _video; } /** * Sound object associated to the player * @return sound object for further manipulation */ public function getSound():Sound { return _sound; } /** * The id3 info of sound object * @return */ public function getId3Info():ID3Info { return _id3Info; } /** * The current sound state * @return true if mute otherwise false */ public function getMute():Bool { return _soundMuted; } /** * The amount of total bytes * @return amount of bytes */ public function getBytesTotal():Float { var bytesTotal:Float = 0; if (_streamType == StreamType.YOUTUBE) { if (_youtubeLoader != null && _mediaLoaded) bytesTotal = Reflect.field(_youtubeLoader.content, "getVideoBytesTotal")(); } else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { bytesTotal = _stream.bytesTotal; } else if (_type == InputType.AUDIO) { bytesTotal = _sound.bytesTotal; } return bytesTotal; } /** * The amount of bytes loaded * @return amount of bytes */ public function getBytesLoaded():Float { var bytesLoaded:Float = 0; if (_streamType == StreamType.YOUTUBE) { if (_youtubeLoader != null && _mediaLoaded) bytesLoaded = Reflect.field(_youtubeLoader.content, "getVideoBytesLoaded")(); } else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { bytesLoaded = _stream.bytesLoaded; } else if (_type == InputType.AUDIO) { bytesLoaded = _sound.bytesLoaded; } return bytesLoaded; } /** * Current playing file type * @return audio or video */ public function getType():String { return _type; } /** * The stream method for the current playing media * @return */ public function getStreamType():String { return _streamType; } /** * The server url for current rtmp stream * @return */ public function getServer():String { return _server; } /** * To check current quality mode * @return true if high quality false if low */ public function getQuality():Bool { return _videoQualityHigh; } /** * The current playing time * @return current playing time in seconds */ public function getCurrentTime():Float { var time:Float = 0; if (_streamType == StreamType.YOUTUBE) { if (_youtubeLoader != null && _ytReady) { time = Reflect.field(_youtubeLoader.content, "getCurrentTime")(); } else { time = 0; } } else if (_streamType == StreamType.PSEUDOSTREAM) { time = getStartTime() + _stream.time; } else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP) { time = _stream.time; } else if (_type == InputType.AUDIO) { if (_soundChannel != null) { time = _soundChannel.position / 1000; } else { time = 0; } } return time; } //} /** * Return the load type * @return */ public function getLoadType():Bool { return _preLoading; } public function hasPoster(val:Bool) { _hasPoster = val; } }