package {
  import com.axis.audioclient.AxisTransmit;
  import com.axis.ClientEvent;
  import com.axis.ErrorManager;
  import com.axis.http.url;
  import com.axis.httpclient.HTTPClient;
  import com.axis.IClient;
  import com.axis.Logger;
  import com.axis.mjpegclient.MJPEGClient;
  import com.axis.rtmpclient.RTMPClient;
  import com.axis.rtspclient.IRTSPHandle;
  import com.axis.rtspclient.RTSPClient;
  import com.axis.rtspclient.RTSPoverHTTPHandle;
  import com.axis.rtspclient.RTSPoverTCPHandle;

  import flash.display.LoaderInfo;
  import flash.display.Sprite;
  import flash.display.Stage;
  import flash.display.StageAlign;
  import flash.display.StageDisplayState;
  import flash.display.StageScaleMode;
  import flash.display.DisplayObject;
  import flash.display.InteractiveObject;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.external.ExternalInterface;
  import flash.media.Microphone;
  import flash.media.SoundMixer;
  import flash.media.SoundTransform;
  import flash.media.Video;
  import flash.net.NetStream;
  import flash.system.Security;
  import mx.utils.StringUtil;

  [SWF(frameRate="60")]
  [SWF(backgroundColor="#efefef")]

  public class Player extends Sprite {
    [Embed(source = "../VERSION", mimeType = "application/octet-stream")] private var Version:Class;

    public static var locomoteID:String = null;

    private static const EVENT_STREAM_STARTED:String  = "streamStarted";
    private static const EVENT_STREAM_PAUSED:String  = "streamPaused";
    private static const EVENT_STREAM_STOPPED:String  = "streamStopped";
    private static const EVENT_STREAM_ENDED:String  = "streamEnded";
    private static const EVENT_FULLSCREEN_ENTERED:String  = "fullscreenEntered";
    private static const EVENT_FULLSCREEN_EXITED:String  = "fullscreenExited";
    private static const EVENT_FRAME_READY:String  = "frameReady";

    public static var config:Object = {
      'buffer': 3,
      'connectionTimeout': 10,
      'scaleUp': false,
      'allowFullscreen': true,
      'debugLogger': false,
      'frameByFrame': false
    };

    private var audioTransmit:AxisTransmit = new AxisTransmit();
    private var meta:Object = {};
    private var client:IClient;
    private var urlParsed:Object;
    private var savedSpeakerVolume:Number;
    private var fullscreenAllowed:Boolean = true;
    private var currentState:String = "stopped";
    private var streamHasAudio:Boolean = false;
    private var streamHasVideo:Boolean = false;
    private var newPlaylistItem:Boolean = false;
    private var startOptions:Object = null;

    public function Player() {
      var self:Player = this;

      Security.allowDomain("*");
      Security.allowInsecureDomain("*");

      trace('Loaded Locomote, version ' + StringUtil.trim(new Version().toString()));

      if (ExternalInterface.available) {
          setupAPICallbacks();
      } else {
        trace("External interface is not available for this container.");
      }

      /* Set default speaker volume */
      this.speakerVolume(50);

      /* Stage setup */
      this.stage.align = StageAlign.TOP_LEFT;
      this.stage.scaleMode = StageScaleMode.NO_SCALE;
      addEventListener(Event.ADDED_TO_STAGE, onStageAdded);

      /* Fullscreen support setup */
      this.stage.doubleClickEnabled = true;
      this.stage.addEventListener(MouseEvent.DOUBLE_CLICK, fullscreen);
      this.stage.addEventListener(Event.FULLSCREEN, function(event:Event):void {
        (StageDisplayState.NORMAL === stage.displayState) ? callAPI(EVENT_FULLSCREEN_EXITED) : callAPI(EVENT_FULLSCREEN_ENTERED);
      });
      this.stage.addEventListener(Event.RESIZE, function(event:Event):void {
        videoResize();
      });

      this.setConfig(Player.config);
    }

    /**
     * Registers the appropriate API functions with the container, so that
     * they can be called, and triggers the apiReady event
     * which tells the container that the Player is ready to receive API calls.
     */
    public function setupAPICallbacks():void {
      ExternalInterface.marshallExceptions = true;

      /* Media player API */
      ExternalInterface.addCallback("play", play);
      ExternalInterface.addCallback("pause", pause);
      ExternalInterface.addCallback("resume", resume);
      ExternalInterface.addCallback("stop", stop);
      ExternalInterface.addCallback("seek", seek);
      ExternalInterface.addCallback("playFrames", playFrames);
      ExternalInterface.addCallback("streamStatus", streamStatus);
      ExternalInterface.addCallback("playerStatus", playerStatus);
      ExternalInterface.addCallback("speakerVolume", speakerVolume);
      ExternalInterface.addCallback("muteSpeaker", muteSpeaker);
      ExternalInterface.addCallback("unmuteSpeaker", unmuteSpeaker);
      ExternalInterface.addCallback("microphoneVolume", microphoneVolume);
      ExternalInterface.addCallback("muteMicrophone", muteMicrophone);
      ExternalInterface.addCallback("unmuteMicrophone", unmuteMicrophone);
      ExternalInterface.addCallback("setConfig", setConfig);

      /* Audio Transmission API */
      ExternalInterface.addCallback("startAudioTransmit", startAudioTransmit);
      ExternalInterface.addCallback("stopAudioTransmit", stopAudioTransmit);
    }

    public function fullscreen(event:MouseEvent):void {
      if (config.allowFullscreen) {
        this.stage.displayState = (StageDisplayState.NORMAL === stage.displayState) ?
          StageDisplayState.FULL_SCREEN : StageDisplayState.NORMAL;
      }
    }

    public function videoResize():void {
      if (!this.client) {
        return;
      }

      var stagewidth:uint = (StageDisplayState.NORMAL === stage.displayState) ?
        stage.stageWidth : stage.fullScreenWidth;
      var stageheight:uint = (StageDisplayState.NORMAL === stage.displayState) ?
        stage.stageHeight : stage.fullScreenHeight;

      var video:DisplayObject = this.client.getDisplayObject();

      var scale:Number = ((stagewidth / meta.width) > (stageheight / meta.height)) ?
        (stageheight / meta.height) : (stagewidth / meta.width);

      video.width = meta.width;
      video.height = meta.height;
      if ((scale < 1.0) || (scale > 1.0 && true === config.scaleUp)) {
        Logger.log('scaling video, scale:' + scale.toFixed(2) + ' (aspect ratio: ' +  (video.width / video.height).toFixed(2) + ')');
        video.width = meta.width * scale;
        video.height = meta.height * scale;
      }

      video.x = (stagewidth - video.width) / 2;
      video.y = (stageheight - video.height) / 2;
    }

    public function setConfig(iconfig:Object):void {
      if (iconfig.buffer !== undefined) {
        if (this.client) {
          if (false === this.client.setBuffer(config.buffer)) {
            ErrorManager.dispatchError(830);
          } else {
            config.buffer = iconfig.buffer;
          }
        } else {
            config.buffer = iconfig.buffer;
        }
      }

      if (iconfig.frameByFrame !== undefined) {
        if (this.client) {
          if (false === this.client.setFrameByFrame(iconfig.frameByFrame)) {
            ErrorManager.dispatchError(832);
          } else {
            config.frameByFrame = iconfig.frameByFrame;
          }
        } else {
          config.frameByFrame = iconfig.frameByFrame;
        }
      }

      if (iconfig.scaleUp !== undefined) {
        var scaleUpChanged:Boolean = (config.scaleUp !== iconfig.scaleUp);
        config.scaleUp = iconfig.scaleUp;
        if (scaleUpChanged && this.client)
          this.videoResize();
      }

      if (iconfig.allowFullscreen !== undefined) {
        config.allowFullscreen = iconfig.allowFullscreen;

        if (!config.allowFullscreen)
          this.stage.displayState = StageDisplayState.NORMAL;
      }

      if (iconfig.debugLogger !== undefined) {
        config.debugLogger = iconfig.debugLogger;
      }

      if (iconfig.connectionTimeout !== undefined) {
        config.connectionTimeout = iconfig.connectionTimeout;
      }
    }

    public function play(param:* = null, options:Object = null):void {
      this.streamHasAudio = false;
      this.streamHasVideo = false;

      if (param is String) {
        urlParsed = url.parse(String(param));
      } else {
        urlParsed = url.parse(param.url);
        urlParsed.connect = param.url;
        urlParsed.streamName = param.streamName;
      }

      this.newPlaylistItem = true;
      this.startOptions = options;

      if (client) {
        /* Stop the client, and 'onStopped' will start the new stream. */
        client.stop();
        return;
      }

      start();
    }

    private function start():void {
      switch (urlParsed.protocol) {
      case 'rtsph':
        /* RTSP over HTTP */
        client = new RTSPClient(urlParsed, new RTSPoverHTTPHandle(urlParsed, false));
        break;

      case 'rtsphs':
        /* RTSP over HTTPS */
        client = new RTSPClient(urlParsed, new RTSPoverHTTPHandle(urlParsed, true));
        break;

      case 'rtsp':
        /* RTSP over TCP */
        client = new RTSPClient(urlParsed, new RTSPoverTCPHandle(urlParsed));
        break;

      case 'http':
      case 'https':
        /* Progressive download over HTTP */
        client = new HTTPClient(urlParsed);
        break;

      case 'httpm':
        /* Progressive mjpg download over HTTP (x-mixed-replace) */
        client = new MJPEGClient(urlParsed);
        break;

      case 'rtmp':
      case 'rtmps':
      case 'rtmpt':
        /* RTMP */
        client = new RTMPClient(urlParsed);
        break;

      default:
        ErrorManager.dispatchError(811, [urlParsed.protocol])
        return;
      }

      addChild(this.client.getDisplayObject());

      client.addEventListener(ClientEvent.STOPPED, onStopped);
      client.addEventListener(ClientEvent.START_PLAY, onStartPlay);
      client.addEventListener(ClientEvent.PAUSED, onPaused);
      client.addEventListener(ClientEvent.ENDED, onEnded);
      client.addEventListener(ClientEvent.META, onMeta);
      client.addEventListener(ClientEvent.FRAME, onFrame);
      client.start(this.startOptions);
      this.newPlaylistItem = false;
    }

    public function seek(position:String):void {
      if (!client || !client.seek(Number(position))) {
        ErrorManager.dispatchError(828);
      }
    }

    public function playFrames(timestamp:Number):void {
      client && client.playFrames(timestamp);
    }

    public function pause():void {
      try {
        client.pause()
      } catch (err:Error) {
        ErrorManager.dispatchError(808);
      }
    }

    public function resume():void {
      if (!client || !client.resume()) {
        ErrorManager.dispatchError(809);
      }
    }

    public function stop():void {
      if (!client || !client.stop()) {
        ErrorManager.dispatchError(810);
        return;
      }

      this.currentState = "stopped";
      this.streamHasAudio = false;
      this.streamHasVideo = false;
    }

    public function onMeta(event:ClientEvent):void {
      this.meta = event.data;
      this.videoResize();
    }

    public function streamStatus():Object {
      if (this.currentState === 'playing') {
        /* This causes a crash in some situations */
        //this.streamHasAudio = (this.streamHasAudio || this.client.hasAudio());
        this.streamHasVideo = (this.streamHasVideo || this.client.hasVideo());
      }
      var status:Object = {
        'fps': (this.client) ? this.client.currentFPS() : null,
        'resolution': (this.client) ? { width: meta.width, height: meta.height } : null,
        'playbackSpeed': (this.client) ? 1.0 : null,
        'protocol': (this.urlParsed) ? this.urlParsed.protocol : null,
        'audio': (this.client) ? this.streamHasAudio : null,
        'video': (this.client) ? this.streamHasVideo : null,
        'state': this.currentState,
        'streamURL': (this.urlParsed) ? this.urlParsed.full : null,
        'duration': meta.duration ? meta.duration : null,
        'currentTime': (this.client) ? this.client.getCurrentTime() : -1,
        'bufferedTime': (this.client) ? this.client.bufferedTime() : -1
      };

      return status;
    }

    public function playerStatus():Object {
      var mic:Microphone = Microphone.getMicrophone();

      var status:Object = {
        'version': StringUtil.trim(new Version().toString()),
        'microphoneVolume': audioTransmit.microphoneVolume,
        'speakerVolume': this.savedSpeakerVolume,
        'microphoneMuted': (mic.gain === 0),
        'speakerMuted': (flash.media.SoundMixer.soundTransform.volume === 0),
        'fullscreen': (StageDisplayState.FULL_SCREEN === stage.displayState),
        'buffer': (client === null) ? 0 : Player.config.buffer
      };

      return status;
    }

    public function speakerVolume(volume:Number):void {
      this.savedSpeakerVolume = volume;
      var transform:SoundTransform = new SoundTransform(volume / 100.0);
      flash.media.SoundMixer.soundTransform = transform;
    }

    public function muteSpeaker():void {
      var transform:SoundTransform = new SoundTransform(0);
      flash.media.SoundMixer.soundTransform = transform;
    }

    public function unmuteSpeaker():void {
      if (flash.media.SoundMixer.soundTransform.volume !== 0)
        return;

      var transform:SoundTransform = new SoundTransform(this.savedSpeakerVolume / 100.0);
      flash.media.SoundMixer.soundTransform = transform;
    }

    public function microphoneVolume(volume:Number):void {
      audioTransmit.microphoneVolume = volume;
    }

    public function muteMicrophone():void {
      audioTransmit.muteMicrophone();
    }

    public function unmuteMicrophone():void {
      audioTransmit.unmuteMicrophone();
    }

    public function startAudioTransmit(url:String = null, type:String = 'axis'):void {
      if (type === 'axis') {
        audioTransmit.start(url);
      } else {
        ErrorManager.dispatchError(812);
      }
    }

    public function stopAudioTransmit():void {
      audioTransmit.stop();
    }

    public function allowFullscreen(state:Boolean):void {
      this.fullscreenAllowed = state;

      if (!state)
        this.stage.displayState = StageDisplayState.NORMAL;
    }

    private function onStageAdded(e:Event):void {
      Player.locomoteID = LoaderInfo(this.root.loaderInfo).parameters.locomoteID.toString();
      ExternalInterface.call("LocomoteMap['" + Player.locomoteID + "'].__swfReady");
    }

    private function onStartPlay(event:ClientEvent):void {
      this.currentState = "playing";
      this.callAPI(EVENT_STREAM_STARTED);
    }

    private function onEnded(event:ClientEvent):void {
      this.currentState = "ended";
      this.callAPI(EVENT_STREAM_ENDED, event.data);
    }

    private function onPaused(event:ClientEvent):void {
      this.currentState = "paused";
      this.callAPI(EVENT_STREAM_PAUSED, event.data);
    }

    private function onStopped(event:ClientEvent):void {
      this.removeChild(this.client.getDisplayObject());
      this.client.removeEventListener(ClientEvent.STOPPED, onStopped);
      this.client.removeEventListener(ClientEvent.START_PLAY, onStartPlay);
      this.client.removeEventListener(ClientEvent.PAUSED, onPaused);
      this.client.removeEventListener(ClientEvent.ENDED, onEnded);
      this.client.removeEventListener(ClientEvent.META, onMeta);
      this.client.removeEventListener(ClientEvent.FRAME, onFrame);
      this.client = null;
      this.callAPI(EVENT_STREAM_STOPPED, event.data);

      /* If a new `play` has been queued, fire it */
      if (this.newPlaylistItem) {
        start();
      }
    }

    private function onFrame(event:ClientEvent):void {
      this.callAPI(EVENT_FRAME_READY, { timestamp: event.data });
    }

    private function callAPI(eventName:String, data:Object = null):void {
      var functionName:String = "LocomoteMap['" + Player.locomoteID + "'].__playerEvent";
      if (data) {
        ExternalInterface.call(functionName, eventName, data);
      } else {
        ExternalInterface.call(functionName, eventName);
      }
    }
  }
}

package {
  import com.axis.audioclient.AxisTransmit;
  import com.axis.ClientEvent;
  import com.axis.ErrorManager;
  import com.axis.http.url;
  import com.axis.httpclient.HTTPClient;
  import com.axis.IClient;
  import com.axis.Logger;
  import com.axis.mjpegclient.MJPEGClient;
  import com.axis.rtmpclient.RTMPClient;
  import com.axis.rtspclient.IRTSPHandle;
  import com.axis.rtspclient.RTSPClient;
  import com.axis.rtspclient.RTSPoverHTTPHandle;
  import com.axis.rtspclient.RTSPoverTCPHandle;

  import flash.display.LoaderInfo;
  import flash.display.Sprite;
  import flash.display.Stage;
  import flash.display.StageAlign;
  import flash.display.StageDisplayState;
  import flash.display.StageScaleMode;
  import flash.display.DisplayObject;
  import flash.display.InteractiveObject;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.external.ExternalInterface;
  import flash.media.Microphone;
  import flash.media.SoundMixer;
  import flash.media.SoundTransform;
  import flash.media.Video;
  import flash.net.NetStream;
  import flash.system.Security;
  import mx.utils.StringUtil;

  [SWF(frameRate="60")]
  [SWF(backgroundColor="#efefef")]

  public class Player extends Sprite {
    [Embed(source = "../VERSION", mimeType = "application/octet-stream")] private var Version:Class;

    public static var locomoteID:String = null;

    private static const EVENT_STREAM_STARTED:String  = "streamStarted";
    private static const EVENT_STREAM_PAUSED:String  = "streamPaused";
    private static const EVENT_STREAM_STOPPED:String  = "streamStopped";
    private static const EVENT_STREAM_ENDED:String  = "streamEnded";
    private static const EVENT_FULLSCREEN_ENTERED:String  = "fullscreenEntered";
    private static const EVENT_FULLSCREEN_EXITED:String  = "fullscreenExited";
    private static const EVENT_FRAME_READY:String  = "frameReady";

    public static var config:Object = {
      'buffer': 3,
      'connectionTimeout': 10,
      'scaleUp': false,
      'allowFullscreen': true,
      'debugLogger': false,
      'frameByFrame': false
    };

    private var audioTransmit:AxisTransmit = new AxisTransmit();
    private var meta:Object = {};
    private var client:IClient;
    private var urlParsed:Object;
    private var savedSpeakerVolume:Number;
    private var fullscreenAllowed:Boolean = true;
    private var currentState:String = "stopped";
    private var streamHasAudio:Boolean = false;
    private var streamHasVideo:Boolean = false;
    private var newPlaylistItem:Boolean = false;
    private var startOptions:Object = null;

    public function Player() {
      var self:Player = this;

      Security.allowDomain("*");
      Security.allowInsecureDomain("*");

      trace('Loaded Locomote, version ' + StringUtil.trim(new Version().toString()));

      if (ExternalInterface.available) {
          setupAPICallbacks();
      } else {
        trace("External interface is not available for this container.");
      }

      /* Set default speaker volume */
      this.speakerVolume(50);

      /* Stage setup */
      this.stage.align = StageAlign.TOP_LEFT;
      this.stage.scaleMode = StageScaleMode.NO_SCALE;
      addEventListener(Event.ADDED_TO_STAGE, onStageAdded);

      /* Fullscreen support setup */
      this.stage.doubleClickEnabled = true;
      this.stage.addEventListener(MouseEvent.DOUBLE_CLICK, fullscreen);
      this.stage.addEventListener(Event.FULLSCREEN, function(event:Event):void {
        (StageDisplayState.NORMAL === stage.displayState) ? callAPI(EVENT_FULLSCREEN_EXITED) : callAPI(EVENT_FULLSCREEN_ENTERED);
      });
      this.stage.addEventListener(Event.RESIZE, function(event:Event):void {
        videoResize();
      });

      this.setConfig(Player.config);
    }

    /**
     * Registers the appropriate API functions with the container, so that
     * they can be called, and triggers the apiReady event
     * which tells the container that the Player is ready to receive API calls.
     */
    public function setupAPICallbacks():void {
      ExternalInterface.marshallExceptions = true;

      /* Media player API */
      ExternalInterface.addCallback("play", play);
      ExternalInterface.addCallback("pause", pause);
      ExternalInterface.addCallback("resume", resume);
      ExternalInterface.addCallback("stop", stop);
      ExternalInterface.addCallback("seek", seek);
      ExternalInterface.addCallback("playFrames", playFrames);
      ExternalInterface.addCallback("streamStatus", streamStatus);
      ExternalInterface.addCallback("playerStatus", playerStatus);
      ExternalInterface.addCallback("speakerVolume", speakerVolume);
      ExternalInterface.addCallback("muteSpeaker", muteSpeaker);
      ExternalInterface.addCallback("unmuteSpeaker", unmuteSpeaker);
      ExternalInterface.addCallback("microphoneVolume", microphoneVolume);
      ExternalInterface.addCallback("muteMicrophone", muteMicrophone);
      ExternalInterface.addCallback("unmuteMicrophone", unmuteMicrophone);
      ExternalInterface.addCallback("setConfig", setConfig);

      /* Audio Transmission API */
      ExternalInterface.addCallback("startAudioTransmit", startAudioTransmit);
      ExternalInterface.addCallback("stopAudioTransmit", stopAudioTransmit);
    }

    public function fullscreen(event:MouseEvent):void {
      if (config.allowFullscreen) {
        this.stage.displayState = (StageDisplayState.NORMAL === stage.displayState) ?
          StageDisplayState.FULL_SCREEN : StageDisplayState.NORMAL;
      }
    }

    public function videoResize():void {
      if (!this.client) {
        return;
      }

      var stagewidth:uint = (StageDisplayState.NORMAL === stage.displayState) ?
        stage.stageWidth : stage.fullScreenWidth;
      var stageheight:uint = (StageDisplayState.NORMAL === stage.displayState) ?
        stage.stageHeight : stage.fullScreenHeight;

      var video:DisplayObject = this.client.getDisplayObject();

      var scale:Number = ((stagewidth / meta.width) > (stageheight / meta.height)) ?
        (stageheight / meta.height) : (stagewidth / meta.width);

      video.width = meta.width;
      video.height = meta.height;
      if ((scale < 1.0) || (scale > 1.0 && true === config.scaleUp)) {
        Logger.log('scaling video, scale:' + scale.toFixed(2) + ' (aspect ratio: ' +  (video.width / video.height).toFixed(2) + ')');
        video.width = meta.width * scale;
        video.height = meta.height * scale;
      }

      video.x = (stagewidth - video.width) / 2;
      video.y = (stageheight - video.height) / 2;
    }

    public function setConfig(iconfig:Object):void {
      if (iconfig.buffer !== undefined) {
        if (this.client) {
          if (false === this.client.setBuffer(config.buffer)) {
            ErrorManager.dispatchError(830);
          } else {
            config.buffer = iconfig.buffer;
          }
        } else {
            config.buffer = iconfig.buffer;
        }
      }

      if (iconfig.frameByFrame !== undefined) {
        if (this.client) {
          if (false === this.client.setFrameByFrame(iconfig.frameByFrame)) {
            ErrorManager.dispatchError(832);
          } else {
            config.frameByFrame = iconfig.frameByFrame;
          }
        } else {
          config.frameByFrame = iconfig.frameByFrame;
        }
      }

      if (iconfig.scaleUp !== undefined) {
        var scaleUpChanged:Boolean = (config.scaleUp !== iconfig.scaleUp);
        config.scaleUp = iconfig.scaleUp;
        if (scaleUpChanged && this.client)
          this.videoResize();
      }

      if (iconfig.allowFullscreen !== undefined) {
        config.allowFullscreen = iconfig.allowFullscreen;

        if (!config.allowFullscreen)
          this.stage.displayState = StageDisplayState.NORMAL;
      }

      if (iconfig.debugLogger !== undefined) {
        config.debugLogger = iconfig.debugLogger;
      }

      if (iconfig.connectionTimeout !== undefined) {
        config.connectionTimeout = iconfig.connectionTimeout;
      }
    }

    public function play(param:* = null, options:Object = null):void {
      this.streamHasAudio = false;
      this.streamHasVideo = false;

      if (param is String) {
        urlParsed = url.parse(String(param));
      } else {
        urlParsed = url.parse(param.url);
        urlParsed.connect = param.url;
        urlParsed.streamName = param.streamName;
      }

      this.newPlaylistItem = true;
      this.startOptions = options;

      if (client) {
        /* Stop the client, and 'onStopped' will start the new stream. */
        client.stop();
        return;
      }

      start();
    }

    private function start():void {
      switch (urlParsed.protocol) {
      case 'rtsph':
        /* RTSP over HTTP */
        client = new RTSPClient(urlParsed, new RTSPoverHTTPHandle(urlParsed, false));
        break;

      case 'rtsphs':
        /* RTSP over HTTPS */
        client = new RTSPClient(urlParsed, new RTSPoverHTTPHandle(urlParsed, true));
        break;

      case 'rtsp':
        /* RTSP over TCP */
        client = new RTSPClient(urlParsed, new RTSPoverTCPHandle(urlParsed));
        break;

      case 'http':
      case 'https':
        /* Progressive download over HTTP */
        client = new HTTPClient(urlParsed);
        break;

      case 'httpm':
        /* Progressive mjpg download over HTTP (x-mixed-replace) */
        client = new MJPEGClient(urlParsed);
        break;

      case 'rtmp':
      case 'rtmps':
      case 'rtmpt':
        /* RTMP */
        client = new RTMPClient(urlParsed);
        break;

      default:
        ErrorManager.dispatchError(811, [urlParsed.protocol])
        return;
      }

      addChild(this.client.getDisplayObject());

      client.addEventListener(ClientEvent.STOPPED, onStopped);
      client.addEventListener(ClientEvent.START_PLAY, onStartPlay);
      client.addEventListener(ClientEvent.PAUSED, onPaused);
      client.addEventListener(ClientEvent.ENDED, onEnded);
      client.addEventListener(ClientEvent.META, onMeta);
      client.addEventListener(ClientEvent.FRAME, onFrame);
      client.start(this.startOptions);
      this.newPlaylistItem = false;
    }

    public function seek(position:String):void {
      if (!client || !client.seek(Number(position))) {
        ErrorManager.dispatchError(828);
      }
    }

    public function playFrames(timestamp:Number):void {
      client && client.playFrames(timestamp);
    }

    public function pause():void {
      try {
        client.pause()
      } catch (err:Error) {
        ErrorManager.dispatchError(808);
      }
    }

    public function resume():void {
      if (!client || !client.resume()) {
        ErrorManager.dispatchError(809);
      }
    }

    public function stop():void {
      if (!client || !client.stop()) {
        ErrorManager.dispatchError(810);
        return;
      }

      this.currentState = "stopped";
      this.streamHasAudio = false;
      this.streamHasVideo = false;
    }

    public function onMeta(event:ClientEvent):void {
      this.meta = event.data;
      this.videoResize();
    }

    public function streamStatus():Object {
      //if (this.currentState === 'playing') {
        /* This causes a crash in some situations */
        //this.streamHasAudio = (this.streamHasAudio || this.client.hasAudio());
        //this.streamHasVideo = (this.streamHasVideo || this.client.hasVideo());
      //}
      var status:Object = {
        'fps': (this.client) ? this.client.currentFPS() : null,
        'resolution': (this.client) ? { width: meta.width, height: meta.height } : null,
        'playbackSpeed': (this.client) ? 1.0 : null,
        'protocol': (this.urlParsed) ? this.urlParsed.protocol : null,
        //'audio': (this.client) ? this.streamHasAudio : null,
        //'video': (this.client) ? this.streamHasVideo : null,
        'state': this.currentState,
        'streamURL': (this.urlParsed) ? this.urlParsed.full : null,
        'duration': meta.duration ? meta.duration : null,
        'currentTime': (this.client) ? this.client.getCurrentTime() : -1,
        'bufferedTime': (this.client) ? this.client.bufferedTime() : -1
      };

      return status;
    }

    public function playerStatus():Object {
      var mic:Microphone = Microphone.getMicrophone();

      var status:Object = {
        'version': StringUtil.trim(new Version().toString()),
        'microphoneVolume': audioTransmit.microphoneVolume,
        'speakerVolume': this.savedSpeakerVolume,
        'microphoneMuted': (mic.gain === 0),
        'speakerMuted': (flash.media.SoundMixer.soundTransform.volume === 0),
        'fullscreen': (StageDisplayState.FULL_SCREEN === stage.displayState),
        'buffer': (client === null) ? 0 : Player.config.buffer
      };

      return status;
    }

    public function speakerVolume(volume:Number):void {
      this.savedSpeakerVolume = volume;
      var transform:SoundTransform = new SoundTransform(volume / 100.0);
      flash.media.SoundMixer.soundTransform = transform;
    }

    public function muteSpeaker():void {
      var transform:SoundTransform = new SoundTransform(0);
      flash.media.SoundMixer.soundTransform = transform;
    }

    public function unmuteSpeaker():void {
      if (flash.media.SoundMixer.soundTransform.volume !== 0)
        return;

      var transform:SoundTransform = new SoundTransform(this.savedSpeakerVolume / 100.0);
      flash.media.SoundMixer.soundTransform = transform;
    }

    public function microphoneVolume(volume:Number):void {
      audioTransmit.microphoneVolume = volume;
    }

    public function muteMicrophone():void {
      audioTransmit.muteMicrophone();
    }

    public function unmuteMicrophone():void {
      audioTransmit.unmuteMicrophone();
    }

    public function startAudioTransmit(url:String = null, type:String = 'axis'):void {
      if (type === 'axis') {
        audioTransmit.start(url);
      } else {
        ErrorManager.dispatchError(812);
      }
    }

    public function stopAudioTransmit():void {
      audioTransmit.stop();
    }

    public function allowFullscreen(state:Boolean):void {
      this.fullscreenAllowed = state;

      if (!state)
        this.stage.displayState = StageDisplayState.NORMAL;
    }

    private function onStageAdded(e:Event):void {
      Player.locomoteID = LoaderInfo(this.root.loaderInfo).parameters.locomoteID.toString();
      ExternalInterface.call("LocomoteMap['" + Player.locomoteID + "'].__swfReady");
    }

    private function onStartPlay(event:ClientEvent):void {
      this.currentState = "playing";
      this.callAPI(EVENT_STREAM_STARTED);
    }

    private function onEnded(event:ClientEvent):void {
      this.currentState = "ended";
      this.callAPI(EVENT_STREAM_ENDED, event.data);
    }

    private function onPaused(event:ClientEvent):void {
      this.currentState = "paused";
      this.callAPI(EVENT_STREAM_PAUSED, event.data);
    }

    private function onStopped(event:ClientEvent):void {
      this.removeChild(this.client.getDisplayObject());
      this.client.removeEventListener(ClientEvent.STOPPED, onStopped);
      this.client.removeEventListener(ClientEvent.START_PLAY, onStartPlay);
      this.client.removeEventListener(ClientEvent.PAUSED, onPaused);
      this.client.removeEventListener(ClientEvent.ENDED, onEnded);
      this.client.removeEventListener(ClientEvent.META, onMeta);
      this.client.removeEventListener(ClientEvent.FRAME, onFrame);
      this.client = null;
      this.callAPI(EVENT_STREAM_STOPPED, event.data);

      /* If a new `play` has been queued, fire it */
      if (this.newPlaylistItem) {
        start();
      }
    }

    private function onFrame(event:ClientEvent):void {
      this.callAPI(EVENT_FRAME_READY, { timestamp: event.data });
    }

    private function callAPI(eventName:String, data:Object = null):void {
      var functionName:String = "LocomoteMap['" + Player.locomoteID + "'].__playerEvent";
      if (data) {
        ExternalInterface.call(functionName, eventName, data);
      } else {
        ExternalInterface.call(functionName, eventName);
      }
    }
  }
}

Fix error in stream status

Fix error in stream status


package com.kaltura.hls
{
	import com.kaltura.hls.m2ts.M2TSNetLoader;
	import com.kaltura.hls.manifest.HLSManifestParser;
	import com.kaltura.hls.manifest.HLSManifestPlaylist;
	import com.kaltura.hls.manifest.HLSManifestStream;

	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 org.osmf.elements.VideoElement;
	import org.osmf.elements.proxyClasses.LoadFromDocumentLoadTrait;
	import org.osmf.events.MediaError;
	import org.osmf.events.MediaErrorEvent;
	import org.osmf.media.DefaultMediaFactory;
	import org.osmf.media.MediaElement;
	import org.osmf.media.MediaResourceBase;
	import org.osmf.media.MediaTypeUtil;
	import org.osmf.media.URLResource;
	import org.osmf.metadata.Metadata;
	import org.osmf.metadata.MetadataNamespaces;
	import org.osmf.net.DynamicStreamingItem;
	import org.osmf.net.StreamType;
	import org.osmf.net.StreamingItem;
	import org.osmf.net.StreamingItemType;
	import org.osmf.traits.LoadState;
	import org.osmf.traits.LoadTrait;
	import org.osmf.traits.LoaderBase;
	import org.osmf.utils.URL;

	public class HLSLoader extends LoaderBase
	{
		protected var loadTrait:LoadTrait;
		protected var manifestLoader:URLLoader;
		protected var parser:HLSManifestParser;
		protected var factory:DefaultMediaFactory = new DefaultMediaFactory();
		private var supportedMimeTypes:Vector.<String> = new Vector.<String>();

		public function HLSLoader()
		{
			super();

			supportedMimeTypes.push( "application/x-mpegURL" );
			supportedMimeTypes.push( "application/vnd.apple.mpegURL" );
			supportedMimeTypes.push( "vnd.apple.mpegURL" );
			supportedMimeTypes.push( "video/MP2T" );
		}

		override protected function executeLoad(loadTrait:LoadTrait):void
		{
			this.loadTrait = loadTrait;

			updateLoadTrait(loadTrait, LoadState.LOADING);

			var url:String = URLResource(loadTrait.resource).url;
			manifestLoader = new URLLoader(new URLRequest(url));
			manifestLoader.addEventListener(Event.COMPLETE, onComplete);
			manifestLoader.addEventListener(IOErrorEvent.IO_ERROR, onError);
			manifestLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
		}

		private function onComplete(event:Event):void
		{
			manifestLoader.removeEventListener(Event.COMPLETE, onComplete);
			manifestLoader.removeEventListener(IOErrorEvent.IO_ERROR, onError);
			manifestLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);

			try {
				var resourceData:String = String((event.target as URLLoader).data);

				var url:String = URLResource(loadTrait.resource).url;

				// Start parsing the manifest.
				parser = new HLSManifestParser();
				parser.addEventListener(Event.COMPLETE, onManifestComplete);
				parser.parse(resourceData, url);
			}
			catch (parseError:Error)
			{
				updateLoadTrait(loadTrait, LoadState.LOAD_ERROR);
				loadTrait.dispatchEvent(new MediaErrorEvent(MediaErrorEvent.MEDIA_ERROR, false, false, new MediaError(parseError.errorID, parseError.message)));
			}
		}

		protected function onManifestComplete(event:Event):void
		{
			trace("Manifest is loaded.");

			var isDVR:Boolean = false;

			// Construct the streaming resource and set it as our resource.
			var stream:HLSStreamingResource = new HLSStreamingResource(URLResource(loadTrait.resource).url, "", StreamType.DVR);
			stream.manifest = parser;

			var item:DynamicStreamingItem;
			var items:Vector.<DynamicStreamingItem> = new Vector.<DynamicStreamingItem>();
			for(var i:int=0; i<parser.streams.length; i++)
			{
				var curStream:HLSManifestStream = parser.streams[i];

				//If there is more than one streaming quality available, check to see if filtering is required
				if (parser.streams.length > 1)
				{
					//Checks to see if there are min/max bitrate restrictions then throws out the outliers
					if(HLSManifestParser.MAX_BITRATE != -1 && curStream.bandwidth > HLSManifestParser.MAX_BITRATE ||
						HLSManifestParser.MIN_BITRATE != -1 && curStream.bandwidth < HLSManifestParser.MIN_BITRATE)
					{
						continue;
					}
				}

				item = new DynamicStreamingItem(curStream.uri, curStream.bandwidth, curStream.width, curStream.height);
				curStream.dynamicStream = item;
				items.push(item);
				if ( !(curStream.manifest ? curStream.manifest.streamEnds : false) ) isDVR = true;

				// Create dynamic streaming items for the backup streams
				if (!curStream.backupStream)
					continue;

				var mainStream:HLSManifestStream = curStream;
				curStream = curStream.backupStream;
				while (curStream != mainStream)
				{
					curStream.dynamicStream = new DynamicStreamingItem(curStream.uri, curStream.bandwidth, curStream.width, curStream.height);
					curStream = curStream.backupStream;
				}
			}

			// Deal with single rate M3Us by stuffing a single stream in.
			if(items.length == 0)
			{
				item = new DynamicStreamingItem(URLResource(loadTrait.resource).url, 0, 0, 0);
				items.push(item);

				// Also set the DVR state
				if ( !stream.manifest.streamEnds ) isDVR = true;
			}

			var alternateAudioItems:Vector.<StreamingItem> = new <StreamingItem>[];
			for ( var j:int = 0; j < parser.playLists.length; j++ )
			{
				var playList:HLSManifestPlaylist = parser.playLists[ j ];
				if ( !playList.manifest ) continue;
				var audioItem:StreamingItem = new StreamingItem( StreamingItemType.AUDIO, playList.name, 0, playList );
				alternateAudioItems.push( audioItem );
			}

			stream.streamItems = items;
			stream.alternativeAudioStreamItems = alternateAudioItems;

			var preferredIndex:int;

			//If there is only one stream quality (or less) sets default to first stream
			if (items.length <= 1)
			{
				preferredIndex = 0;
			}
			else if (HLSManifestParser.PREF_BITRATE != -1)
			{
				//If there is a preferred bitrate set by kaltura, tests all streams to find highest bitrate below the preferred
				var bitrateDifference:int = HLSManifestParser.PREF_BITRATE - items[0].bitrate;
				preferredIndex = 0;

				for(var k:int=1; k<items.length; k++)
				{
					if (HLSManifestParser.PREF_BITRATE - items[k].bitrate > 0 && HLSManifestParser.PREF_BITRATE - items[k].bitrate < bitrateDifference)
					{
						preferredIndex = k;
					}
				}
			}
			else
			{
				//Sets the preferred index to the middle (or higher of the 2 middle) bitrate streams
				preferredIndex = items.length / 2;
			}
			stream.initialIndex = preferredIndex;

			stream.addMetadataValue( HLSMetadataNamespaces.PLAYABLE_RESOURCE_METADATA, true );

			var loadedElem:MediaElement = new VideoElement( stream, new M2TSNetLoader() );
			LoadFromDocumentLoadTrait( loadTrait ).mediaElement = loadedElem;

			if ( parser.subtitlePlayLists.length > 0 )
			{
				var subtitleTrait:SubtitleTrait = new SubtitleTrait();
				subtitleTrait.playLists = parser.subtitlePlayLists;
				subtitleTrait.owningMediaElement = loadedElem;
				stream.subtitleTrait = subtitleTrait;
			}

			if ( isDVR )
			{
				var dvrMetadata:Metadata = new Metadata();
				stream.addMetadataValue(MetadataNamespaces.DVR_METADATA, dvrMetadata);
			}

			updateLoadTrait( loadTrait, LoadState.READY );
		}

		private function onError(event:ErrorEvent):void
		{
			manifestLoader.removeEventListener(Event.COMPLETE, onComplete);
			manifestLoader.removeEventListener(IOErrorEvent.IO_ERROR, onError);
			manifestLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);

			updateLoadTrait(loadTrait, LoadState.LOAD_ERROR);
			loadTrait.dispatchEvent(new MediaErrorEvent(MediaErrorEvent.MEDIA_ERROR, false, false, new MediaError(0, event.text)));
		}

		override public function canHandleResource(resource:MediaResourceBase):Boolean
		{
			var supported:int = MediaTypeUtil.checkMetadataMatchWithResource(resource, new Vector.<String>(), supportedMimeTypes);

			if ( supported == MediaTypeUtil.METADATA_MATCH_FOUND )
				return true;

			if (!(resource is URLResource))
				return false;

			var url:URL = new URL((resource as URLResource).url);
			if (url.extension != "m3u8" && url.extension != "m3u")
				return false;

			return true;
		}

	}
}

package com.kaltura.hls
{
	import com.kaltura.hls.m2ts.M2TSNetLoader;
	import com.kaltura.hls.manifest.HLSManifestParser;
	import com.kaltura.hls.manifest.HLSManifestPlaylist;
	import com.kaltura.hls.manifest.HLSManifestStream;

	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 org.osmf.elements.VideoElement;
	import org.osmf.elements.proxyClasses.LoadFromDocumentLoadTrait;
	import org.osmf.events.MediaError;
	import org.osmf.events.MediaErrorEvent;
	import org.osmf.media.DefaultMediaFactory;
	import org.osmf.media.MediaElement;
	import org.osmf.media.MediaResourceBase;
	import org.osmf.media.MediaTypeUtil;
	import org.osmf.media.URLResource;
	import org.osmf.metadata.Metadata;
	import org.osmf.metadata.MetadataNamespaces;
	import org.osmf.net.DynamicStreamingItem;
	import org.osmf.net.StreamType;
	import org.osmf.net.StreamingItem;
	import org.osmf.net.StreamingItemType;
	import org.osmf.traits.LoadState;
	import org.osmf.traits.LoadTrait;
	import org.osmf.traits.LoaderBase;
	import org.osmf.utils.URL;

	public class HLSLoader extends LoaderBase
	{
		protected var loadTrait:LoadTrait;
		protected var manifestLoader:URLLoader;
		protected var parser:HLSManifestParser;
		protected var factory:DefaultMediaFactory = new DefaultMediaFactory();
		private var supportedMimeTypes:Vector.<String> = new Vector.<String>();

		public function HLSLoader()
		{
			super();

			supportedMimeTypes.push( "application/x-mpegURL" );
			supportedMimeTypes.push( "application/vnd.apple.mpegURL" );
			supportedMimeTypes.push( "vnd.apple.mpegURL" );
			supportedMimeTypes.push( "video/MP2T" );
		}

		override protected function executeLoad(loadTrait:LoadTrait):void
		{
			this.loadTrait = loadTrait;

			updateLoadTrait(loadTrait, LoadState.LOADING);

			var url:String = URLResource(loadTrait.resource).url;
			manifestLoader = new URLLoader(new URLRequest(url));
			manifestLoader.addEventListener(Event.COMPLETE, onComplete);
			manifestLoader.addEventListener(IOErrorEvent.IO_ERROR, onError);
			manifestLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
		}

		private function onComplete(event:Event):void
		{
			manifestLoader.removeEventListener(Event.COMPLETE, onComplete);
			manifestLoader.removeEventListener(IOErrorEvent.IO_ERROR, onError);
			manifestLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);

			try {
				var resourceData:String = String((event.target as URLLoader).data);

				var url:String = URLResource(loadTrait.resource).url;

				// Start parsing the manifest.
				parser = new HLSManifestParser();
				parser.addEventListener(Event.COMPLETE, onManifestComplete);
				parser.parse(resourceData, url);
			}
			catch (parseError:Error)
			{
				updateLoadTrait(loadTrait, LoadState.LOAD_ERROR);
				loadTrait.dispatchEvent(new MediaErrorEvent(MediaErrorEvent.MEDIA_ERROR, false, false, new MediaError(parseError.errorID, parseError.message)));
			}
		}

		protected function onManifestComplete(event:Event):void
		{
			trace("Manifest is loaded.");

			var isDVR:Boolean = false;

			// Construct the streaming resource and set it as our resource.
			var stream:HLSStreamingResource = new HLSStreamingResource(URLResource(loadTrait.resource).url, "", StreamType.DVR);
			stream.manifest = parser;

			var item:DynamicStreamingItem;
			var items:Vector.<DynamicStreamingItem> = new Vector.<DynamicStreamingItem>();
			for(var i:int=0; i<parser.streams.length; i++)
			{
				var curStream:HLSManifestStream = parser.streams[i];

				//If there is more than one streaming quality available, check to see if filtering is required
				if (parser.streams.length > 1)
				{
					//Checks to see if there are min/max bitrate restrictions then throws out the outliers
					if(HLSManifestParser.MAX_BITRATE != -1 && curStream.bandwidth > HLSManifestParser.MAX_BITRATE ||
						HLSManifestParser.MIN_BITRATE != -1 && curStream.bandwidth < HLSManifestParser.MIN_BITRATE)
					{
						continue;
					}
				}

				item = new DynamicStreamingItem(curStream.uri, curStream.bandwidth, curStream.width, curStream.height);
				curStream.dynamicStream = item;
				items.push(item);
				if ( !(curStream.manifest ? curStream.manifest.streamEnds : false) ) isDVR = true;

				// Create dynamic streaming items for the backup streams
				if (!curStream.backupStream)
					continue;

				var mainStream:HLSManifestStream = curStream;
				curStream = curStream.backupStream;
				while (curStream != mainStream)
				{
					curStream.dynamicStream = new DynamicStreamingItem(curStream.uri, curStream.bandwidth, curStream.width, curStream.height);
					curStream = curStream.backupStream;
				}
			}

			// Deal with single rate M3Us by stuffing a single stream in.
			if(items.length == 0)
			{
				item = new DynamicStreamingItem(URLResource(loadTrait.resource).url, 0, 0, 0);
				items.push(item);

				// Also set the DVR state
				if ( !stream.manifest.streamEnds ) isDVR = true;
			}

			var alternateAudioItems:Vector.<StreamingItem> = new <StreamingItem>[];
			for ( var j:int = 0; j < parser.playLists.length; j++ )
			{
				var playList:HLSManifestPlaylist = parser.playLists[ j ];
				if ( !playList.manifest ) continue;
				var audioItem:StreamingItem = new StreamingItem( StreamingItemType.AUDIO, playList.name, 0, playList );
				alternateAudioItems.push( audioItem );
			}

			stream.streamItems = items;
			stream.alternativeAudioStreamItems = alternateAudioItems;

			var preferredIndex:int;

			//If there is only one stream quality (or less) sets default to first stream
			if (items.length <= 1)
			{
				preferredIndex = 0;
			}
			else if (HLSManifestParser.PREF_BITRATE != -1)
			{
				//If there is a preferred bitrate set by kaltura, tests all streams to find highest bitrate below the preferred
				preferredIndex = 0;
				var preferredDistance:Number = Number.MAX_VALUE;

				for(var k:int=0; k<items.length; k++)
				{
					var curItem:DynamicStreamingItem = items[k];
					var curDist:Number = items[k].bitrate - HLSManifestParser.PREF_BITRATE;

					/// Reject too low or not improved items.
					if(curDist < 0 || curDist >= preferredDistance)
						continue;

					preferredIndex = k;
					preferredDistance = curDist;
				}
			}
			else
			{
				//Sets the preferred index to the middle (or higher of the 2 middle) bitrate streams
				preferredIndex = items.length / 2;
			}
			stream.initialIndex = preferredIndex;

			stream.addMetadataValue( HLSMetadataNamespaces.PLAYABLE_RESOURCE_METADATA, true );

			var loadedElem:MediaElement = new VideoElement( stream, new M2TSNetLoader() );
			LoadFromDocumentLoadTrait( loadTrait ).mediaElement = loadedElem;

			if ( parser.subtitlePlayLists.length > 0 )
			{
				var subtitleTrait:SubtitleTrait = new SubtitleTrait();
				subtitleTrait.playLists = parser.subtitlePlayLists;
				subtitleTrait.owningMediaElement = loadedElem;
				stream.subtitleTrait = subtitleTrait;
			}

			if ( isDVR )
			{
				var dvrMetadata:Metadata = new Metadata();
				stream.addMetadataValue(MetadataNamespaces.DVR_METADATA, dvrMetadata);
			}

			updateLoadTrait( loadTrait, LoadState.READY );
		}

		private function onError(event:ErrorEvent):void
		{
			manifestLoader.removeEventListener(Event.COMPLETE, onComplete);
			manifestLoader.removeEventListener(IOErrorEvent.IO_ERROR, onError);
			manifestLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);

			updateLoadTrait(loadTrait, LoadState.LOAD_ERROR);
			loadTrait.dispatchEvent(new MediaErrorEvent(MediaErrorEvent.MEDIA_ERROR, false, false, new MediaError(0, event.text)));
		}

		override public function canHandleResource(resource:MediaResourceBase):Boolean
		{
			var supported:int = MediaTypeUtil.checkMetadataMatchWithResource(resource, new Vector.<String>(), supportedMimeTypes);

			if ( supported == MediaTypeUtil.METADATA_MATCH_FOUND )
				return true;

			if (!(resource is URLResource))
				return false;

			var url:URL = new URL((resource as URLResource).url);
			if (url.extension != "m3u8" && url.extension != "m3u")
				return false;

			return true;
		}

	}
}

Update preferred bitrate selection logic to not assume ordered bitrates.


// vim: tabstop=4 shiftwidth=4

// Copyright (c) 2010 , 杨博 (Yang Bo) All rights reserved.
//
//         pop.atry@gmail.com
//
// Use, modification and distribution are subject to the "New BSD License"
// as listed at <url: http://www.opensource.org/licenses/bsd-license.php >.

package com.netease.protobuf {
	import flash.utils.ByteArray
	public function stringToByteArray(s:String):ByteArray {
		const ba:ByteArray = new ByteArray
		ba.writeMultiByte(s, "iso-8859-1")
		return ba
	}
}

// vim: tabstop=4 shiftwidth=4

// Copyright (c) 2010 , 杨博 (Yang Bo) All rights reserved.
//
//         pop.atry@gmail.com
//
// Use, modification and distribution are subject to the "New BSD License"
// as listed at <url: http://www.opensource.org/licenses/bsd-license.php >.

package com.netease.protobuf {
	import flash.utils.ByteArray
	public function stringToByteArray(s:String):ByteArray {
		const ba:ByteArray = new ByteArray
		for (var i:uint = 0; i < s.length; ++i) {
			ba.writeByte(s.charCodeAt(i))
		}
		return ba
	}
}

修复字符串中存在 \0 时转换到 ByteArray 会被截断的 bug

修复字符串中存在 \0 时转换到 ByteArray 会被截断的 bug


////////////////////////////////////////////////////////////////////////////////
//
//  Licensed to the Apache Software Foundation (ASF) under one or more
//  contributor license agreements.  See the NOTICE file distributed with
//  this work for additional information regarding copyright ownership.
//  The ASF licenses this file to You under the Apache License, Version 2.0
//  (the "License"); you may not use this file except in compliance with
//  the License.  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

package spark.skins.mobile
{

import flash.display.DisplayObject;
import flash.display.GradientType;

import mx.core.DPIClassification;
import mx.core.mx_internal;
import mx.events.FlexEvent;
import mx.utils.ColorUtil;

import spark.components.supportClasses.StyleableTextField;
import spark.skins.mobile.supportClasses.ButtonSkinBase;
import spark.skins.mobile120.assets.Button_down;
import spark.skins.mobile120.assets.Button_up;
import spark.skins.mobile160.assets.Button_down;
import spark.skins.mobile160.assets.Button_up;
import spark.skins.mobile240.assets.Button_down;
import spark.skins.mobile240.assets.Button_up;
import spark.skins.mobile320.assets.Button_down;
import spark.skins.mobile320.assets.Button_up;
import spark.skins.mobile480.assets.Button_down;
import spark.skins.mobile480.assets.Button_up;
import spark.skins.mobile640.assets.Button_down;
import spark.skins.mobile640.assets.Button_up;

use namespace mx_internal;

/**
 *  ActionScript-based skin for Button controls in mobile applications. The skin supports
 *  iconClass and labelPlacement. It uses FXG classes to
 *  implement the vector drawing.
 *
 *  @langversion 3.0
 *  @playerversion Flash 10
 *  @playerversion AIR 2.5
 *  @productversion Flex 4.5
 */
public class ButtonSkin extends ButtonSkinBase
{
    //--------------------------------------------------------------------------
    //
    //  Class constants
    //
    //--------------------------------------------------------------------------

    /**
     * An array of color distribution ratios.
     * This is used in the chrome color fill.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    mx_internal static const CHROME_COLOR_RATIOS:Array = [0, 127.5];

    /**
     * An array of alpha values for the corresponding colors in the colors array.
     * This is used in the chrome color fill.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    mx_internal static const CHROME_COLOR_ALPHAS:Array = [1, 1];

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------
    /**
     *  Constructor.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    public function ButtonSkin()
    {
        super();

        switch (applicationDPI)
        {
			case DPIClassification.DPI_640:
			{
				upBorderSkin = spark.skins.mobile640.assets.Button_up;
				downBorderSkin = spark.skins.mobile640.assets.Button_down;

				layoutGap = 20;
				layoutCornerEllipseSize = 40;
				layoutPaddingLeft = 40;
				layoutPaddingRight = 40;
				layoutPaddingTop = 40;
				layoutPaddingBottom = 40;
				layoutBorderSize = 2;
				measuredDefaultWidth = 128;
				measuredDefaultHeight = 172;

				break;
			}
			case DPIClassification.DPI_480:
			{
				// Note provisional may need changes
				upBorderSkin = spark.skins.mobile480.assets.Button_up;
				downBorderSkin = spark.skins.mobile480.assets.Button_down;

				layoutGap = 14;
				layoutCornerEllipseSize = 30;
				layoutPaddingLeft = 30;
				layoutPaddingRight = 30;
				layoutPaddingTop = 30;
				layoutPaddingBottom = 30;
				layoutBorderSize = 2;
				measuredDefaultWidth = 96;
				measuredDefaultHeight = 130;

				break;
			}
            case DPIClassification.DPI_320:
            {
                upBorderSkin = spark.skins.mobile320.assets.Button_up;
                downBorderSkin = spark.skins.mobile320.assets.Button_down;

                layoutGap = 10;
                layoutCornerEllipseSize = 20;
                layoutPaddingLeft = 20;
                layoutPaddingRight = 20;
                layoutPaddingTop = 20;
                layoutPaddingBottom = 20;
                layoutBorderSize = 2;
                measuredDefaultWidth = 64;
                measuredDefaultHeight = 86;

                break;
            }
			case DPIClassification.DPI_240:
			{
				upBorderSkin = spark.skins.mobile240.assets.Button_up;
				downBorderSkin = spark.skins.mobile240.assets.Button_down;

				layoutGap = 7;
				layoutCornerEllipseSize = 15;
				layoutPaddingLeft = 15;
				layoutPaddingRight = 15;
				layoutPaddingTop = 15;
				layoutPaddingBottom = 15;
				layoutBorderSize = 1;
				measuredDefaultWidth = 48;
				measuredDefaultHeight = 65;

				break;
			}
			case DPIClassification.DPI_120:
			{
				upBorderSkin = spark.skins.mobile120.assets.Button_up;
				downBorderSkin = spark.skins.mobile120.assets.Button_down;

				layoutGap = 4;
				layoutCornerEllipseSize = 8;
				layoutPaddingLeft = 8;
				layoutPaddingRight = 8;
				layoutPaddingTop = 8;
				layoutPaddingBottom = 8;
				layoutBorderSize = 1;
				measuredDefaultWidth = 24;
				measuredDefaultHeight = 33;

				break;
			}
            default:
            {
                // default DPI_160
                upBorderSkin = spark.skins.mobile160.assets.Button_up;
                downBorderSkin = spark.skins.mobile160.assets.Button_down;

                layoutGap = 5;
                layoutCornerEllipseSize = 10;
                layoutPaddingLeft = 10;
                layoutPaddingRight = 10;
                layoutPaddingTop = 10;
                layoutPaddingBottom = 10;
                layoutBorderSize = 1;
                measuredDefaultWidth = 32;
                measuredDefaultHeight = 43;

                break;
            }
        }
    }

    //--------------------------------------------------------------------------
    //
    //  Layout variables
    //
    //--------------------------------------------------------------------------

    /**
     *  Defines the corner radius.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    protected var layoutCornerEllipseSize:uint;

    //--------------------------------------------------------------------------
    //
    //  Variables
    //
    //--------------------------------------------------------------------------
    private var _border:DisplayObject;

    private var changeFXGSkin:Boolean = false;

    private var borderClass:Class;

    mx_internal var fillColorStyleName:String = "chromeColor";

    /**
     *  Defines the shadow for the Button control's label.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    public var labelDisplayShadow:StyleableTextField;

    /**
     *  Read-only button border graphic. Use getBorderClassForCurrentState()
     *  to specify a graphic per-state.
     *
     *  @see #getBorderClassForCurrentState()
     */
    protected function get border():DisplayObject
    {
        return _border;
    }

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    /**
     *  Class to use for the border in the up state.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     *
     *  @default Button_up
     */
    protected var upBorderSkin:Class;

    /**
     *  Class to use for the border in the down state.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     *
     *  @default Button_down
     */
    protected var downBorderSkin:Class;

    //--------------------------------------------------------------------------
    //
    //  Overridden methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function createChildren():void
    {
        super.createChildren();

        if (!labelDisplayShadow && labelDisplay)
        {
            labelDisplayShadow = StyleableTextField(createInFontContext(StyleableTextField));
            labelDisplayShadow.styleName = this;
            labelDisplayShadow.colorName = "textShadowColor";
            labelDisplayShadow.useTightTextBounds = false;

            // add shadow before display
            addChildAt(labelDisplayShadow, getChildIndex(labelDisplay));
        }

        setStyle("textAlign", "center");
    }

    /**
     *  @private
     */
    override protected function commitCurrentState():void
    {
        super.commitCurrentState();

        borderClass = getBorderClassForCurrentState();

        if (!(_border is borderClass))
            changeFXGSkin = true;

        // update borderClass and background
        invalidateDisplayList();
    }

    /**
     *  @private
     */
    override protected function layoutContents(unscaledWidth:Number, unscaledHeight:Number):void
    {
        super.layoutContents(unscaledWidth, unscaledHeight);

        // size the FXG background
        if (changeFXGSkin)
        {
            changeFXGSkin = false;

            if (_border)
            {
                removeChild(_border);
                _border = null;
            }

            if (borderClass)
            {
                _border = new borderClass();
                addChildAt(_border, 0);
            }
        }

        layoutBorder(unscaledWidth, unscaledHeight);

        // update label shadow
        labelDisplayShadow.alpha = getStyle("textShadowAlpha");
        labelDisplayShadow.commitStyles();

        // don't use tightText positioning on shadow
        setElementPosition(labelDisplayShadow, labelDisplay.x, labelDisplay.y + 1);
        setElementSize(labelDisplayShadow, labelDisplay.width, labelDisplay.height);

        // if labelDisplay is truncated, then push it down here as well.
        // otherwise, it would have gotten pushed in the labelDisplay_valueCommitHandler()
        if (labelDisplay.isTruncated)
            labelDisplayShadow.text = labelDisplay.text;
    }

    /**
     *  Position the background of the skin. Override this function to re-position the background.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    mx_internal function layoutBorder(unscaledWidth:Number, unscaledHeight:Number):void
    {
        setElementSize(border, unscaledWidth, unscaledHeight);
        setElementPosition(border, 0, 0);
    }

    override protected function drawBackground(unscaledWidth:Number, unscaledHeight:Number):void
    {
        super.drawBackground(unscaledWidth, unscaledHeight);

        var chromeColor:uint = getStyle(fillColorStyleName);

        // In the down state, the fill shadow is defined in the FXG asset
        if (currentState == "down")
        {
            graphics.beginFill(chromeColor);
        }
        else
        {
            var colors:Array = [];
            colorMatrix.createGradientBox(unscaledWidth, unscaledHeight, Math.PI / 2, 0, 0);
            colors[0] = ColorUtil.adjustBrightness2(chromeColor, 70);
            colors[1] = chromeColor;

            graphics.beginGradientFill(GradientType.LINEAR, colors, CHROME_COLOR_ALPHAS, CHROME_COLOR_RATIOS, colorMatrix);
        }

        // inset chrome color by BORDER_SIZE
        // bottom line is a shadow
        graphics.drawRoundRect(layoutBorderSize, layoutBorderSize,
            unscaledWidth - (layoutBorderSize * 2),
            unscaledHeight - (layoutBorderSize * 2),
            layoutCornerEllipseSize, layoutCornerEllipseSize);
        graphics.endFill();
    }

    /**
     *  Returns the borderClass to use based on the currentState.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    protected function getBorderClassForCurrentState():Class
    {
        if (currentState == "down")
            return downBorderSkin;
        else
            return upBorderSkin;
    }

    //--------------------------------------------------------------------------
    //
    //  Event Handlers
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function labelDisplay_valueCommitHandler(event:FlexEvent):void
    {
        super.labelDisplay_valueCommitHandler(event);
        labelDisplayShadow.text = labelDisplay.text;
    }

}
}

////////////////////////////////////////////////////////////////////////////////
//
//  Licensed to the Apache Software Foundation (ASF) under one or more
//  contributor license agreements.  See the NOTICE file distributed with
//  this work for additional information regarding copyright ownership.
//  The ASF licenses this file to You under the Apache License, Version 2.0
//  (the "License"); you may not use this file except in compliance with
//  the License.  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

package spark.skins.mobile
{

import flash.display.DisplayObject;
import flash.display.GradientType;

import mx.core.DPIClassification;
import mx.core.mx_internal;
import mx.events.FlexEvent;
import mx.utils.ColorUtil;

import spark.components.supportClasses.StyleableTextField;
import spark.skins.mobile.supportClasses.ButtonSkinBase;
import spark.skins.mobile120.assets.Button_down;
import spark.skins.mobile120.assets.Button_up;
import spark.skins.mobile160.assets.Button_down;
import spark.skins.mobile160.assets.Button_up;
import spark.skins.mobile240.assets.Button_down;
import spark.skins.mobile240.assets.Button_up;
import spark.skins.mobile320.assets.Button_down;
import spark.skins.mobile320.assets.Button_up;
import spark.skins.mobile480.assets.Button_down;
import spark.skins.mobile480.assets.Button_up;
import spark.skins.mobile640.assets.Button_down;
import spark.skins.mobile640.assets.Button_up;

use namespace mx_internal;

/**
 *  ActionScript-based skin for Button controls in mobile applications. The skin supports
 *  iconClass and labelPlacement. It uses FXG classes to
 *  implement the vector drawing.
 *
 *  @langversion 3.0
 *  @playerversion Flash 10
 *  @playerversion AIR 2.5
 *  @productversion Flex 4.5
 */
public class ButtonSkin extends ButtonSkinBase
{
    //--------------------------------------------------------------------------
    //
    //  Class constants
    //
    //--------------------------------------------------------------------------

    /**
     * An array of color distribution ratios.
     * This is used in the chrome color fill.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    mx_internal static const CHROME_COLOR_RATIOS:Array = [0, 127.5];

    /**
     * An array of alpha values for the corresponding colors in the colors array.
     * This is used in the chrome color fill.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    mx_internal static const CHROME_COLOR_ALPHAS:Array = [1, 1];

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------
    /**
     *  Constructor.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    public function ButtonSkin()
    {
        super();

        switch (applicationDPI)
        {
			case DPIClassification.DPI_640:
			{
				upBorderSkin = spark.skins.mobile640.assets.Button_up;
				downBorderSkin = spark.skins.mobile640.assets.Button_down;

				layoutGap = 20;
				layoutCornerEllipseSize = 28;
				layoutPaddingLeft = 40;
				layoutPaddingRight = 40;
				layoutPaddingTop = 40;
				layoutPaddingBottom = 40;
				layoutBorderSize = 2;
				measuredDefaultWidth = 128;
				measuredDefaultHeight = 172;

				break;
			}
			case DPIClassification.DPI_480:
			{
				// Note provisional may need changes
				upBorderSkin = spark.skins.mobile480.assets.Button_up;
				downBorderSkin = spark.skins.mobile480.assets.Button_down;

				layoutGap = 14;
				layoutCornerEllipseSize = 20;
				layoutPaddingLeft = 30;
				layoutPaddingRight = 30;
				layoutPaddingTop = 30;
				layoutPaddingBottom = 30;
				layoutBorderSize = 2;
				measuredDefaultWidth = 96;
				measuredDefaultHeight = 130;

				break;
			}
            case DPIClassification.DPI_320:
            {
                upBorderSkin = spark.skins.mobile320.assets.Button_up;
                downBorderSkin = spark.skins.mobile320.assets.Button_down;

                layoutGap = 10;
                layoutCornerEllipseSize = 10;
                layoutPaddingLeft = 20;
                layoutPaddingRight = 20;
                layoutPaddingTop = 20;
                layoutPaddingBottom = 20;
                layoutBorderSize = 2;
                measuredDefaultWidth = 64;
                measuredDefaultHeight = 86;

                break;
            }
			case DPIClassification.DPI_240:
			{
				upBorderSkin = spark.skins.mobile240.assets.Button_up;
				downBorderSkin = spark.skins.mobile240.assets.Button_down;

				layoutGap = 7;
				layoutCornerEllipseSize = 8;
				layoutPaddingLeft = 15;
				layoutPaddingRight = 15;
				layoutPaddingTop = 15;
				layoutPaddingBottom = 15;
				layoutBorderSize = 1;
				measuredDefaultWidth = 48;
				measuredDefaultHeight = 65;

				break;
			}
			case DPIClassification.DPI_120:
			{
				upBorderSkin = spark.skins.mobile120.assets.Button_up;
				downBorderSkin = spark.skins.mobile120.assets.Button_down;

				layoutGap = 4;
				layoutCornerEllipseSize = 8;
				layoutPaddingLeft = 8;
				layoutPaddingRight = 8;
				layoutPaddingTop = 8;
				layoutPaddingBottom = 8;
				layoutBorderSize = 1;
				measuredDefaultWidth = 24;
				measuredDefaultHeight = 33;

				break;
			}
            default:
            {
                // default DPI_160
                upBorderSkin = spark.skins.mobile160.assets.Button_up;
                downBorderSkin = spark.skins.mobile160.assets.Button_down;

                layoutGap = 5;
                layoutCornerEllipseSize = 10;
                layoutPaddingLeft = 10;
                layoutPaddingRight = 10;
                layoutPaddingTop = 10;
                layoutPaddingBottom = 10;
                layoutBorderSize = 1;
                measuredDefaultWidth = 32;
                measuredDefaultHeight = 43;

                break;
            }
        }
    }

    //--------------------------------------------------------------------------
    //
    //  Layout variables
    //
    //--------------------------------------------------------------------------

    /**
     *  Defines the corner radius.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    protected var layoutCornerEllipseSize:uint;

    //--------------------------------------------------------------------------
    //
    //  Variables
    //
    //--------------------------------------------------------------------------
    private var _border:DisplayObject;

    private var changeFXGSkin:Boolean = false;

    private var borderClass:Class;

    mx_internal var fillColorStyleName:String = "chromeColor";

    /**
     *  Defines the shadow for the Button control's label.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    public var labelDisplayShadow:StyleableTextField;

    /**
     *  Read-only button border graphic. Use getBorderClassForCurrentState()
     *  to specify a graphic per-state.
     *
     *  @see #getBorderClassForCurrentState()
     */
    protected function get border():DisplayObject
    {
        return _border;
    }

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    /**
     *  Class to use for the border in the up state.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     *
     *  @default Button_up
     */
    protected var upBorderSkin:Class;

    /**
     *  Class to use for the border in the down state.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     *
     *  @default Button_down
     */
    protected var downBorderSkin:Class;

    //--------------------------------------------------------------------------
    //
    //  Overridden methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function createChildren():void
    {
        super.createChildren();

        if (!labelDisplayShadow && labelDisplay)
        {
            labelDisplayShadow = StyleableTextField(createInFontContext(StyleableTextField));
            labelDisplayShadow.styleName = this;
            labelDisplayShadow.colorName = "textShadowColor";
            labelDisplayShadow.useTightTextBounds = false;

            // add shadow before display
            addChildAt(labelDisplayShadow, getChildIndex(labelDisplay));
        }

        setStyle("textAlign", "center");
    }

    /**
     *  @private
     */
    override protected function commitCurrentState():void
    {
        super.commitCurrentState();

        borderClass = getBorderClassForCurrentState();

        if (!(_border is borderClass))
            changeFXGSkin = true;

        // update borderClass and background
        invalidateDisplayList();
    }

    /**
     *  @private
     */
    override protected function layoutContents(unscaledWidth:Number, unscaledHeight:Number):void
    {
        super.layoutContents(unscaledWidth, unscaledHeight);

        // size the FXG background
        if (changeFXGSkin)
        {
            changeFXGSkin = false;

            if (_border)
            {
                removeChild(_border);
                _border = null;
            }

            if (borderClass)
            {
                _border = new borderClass();
                addChildAt(_border, 0);
            }
        }

        layoutBorder(unscaledWidth, unscaledHeight);

        // update label shadow
        labelDisplayShadow.alpha = getStyle("textShadowAlpha");
        labelDisplayShadow.commitStyles();

        // don't use tightText positioning on shadow
        setElementPosition(labelDisplayShadow, labelDisplay.x, labelDisplay.y + 1);
        setElementSize(labelDisplayShadow, labelDisplay.width, labelDisplay.height);

        // if labelDisplay is truncated, then push it down here as well.
        // otherwise, it would have gotten pushed in the labelDisplay_valueCommitHandler()
        if (labelDisplay.isTruncated)
            labelDisplayShadow.text = labelDisplay.text;
    }

    /**
     *  Position the background of the skin. Override this function to re-position the background.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    mx_internal function layoutBorder(unscaledWidth:Number, unscaledHeight:Number):void
    {
        setElementSize(border, unscaledWidth, unscaledHeight);
        setElementPosition(border, 0, 0);
    }

    override protected function drawBackground(unscaledWidth:Number, unscaledHeight:Number):void
    {
        super.drawBackground(unscaledWidth, unscaledHeight);

        var chromeColor:uint = getStyle(fillColorStyleName);

        // In the down state, the fill shadow is defined in the FXG asset
        if (currentState == "down")
        {
            graphics.beginFill(chromeColor);
        }
        else
        {
            var colors:Array = [];
            colorMatrix.createGradientBox(unscaledWidth, unscaledHeight, Math.PI / 2, 0, 0);
            colors[0] = ColorUtil.adjustBrightness2(chromeColor, 70);
            colors[1] = chromeColor;

            graphics.beginGradientFill(GradientType.LINEAR, colors, CHROME_COLOR_ALPHAS, CHROME_COLOR_RATIOS, colorMatrix);
        }

        // inset chrome color by BORDER_SIZE
        // bottom line is a shadow
        graphics.drawRoundRect(layoutBorderSize, layoutBorderSize,
            unscaledWidth - (layoutBorderSize * 2),
            unscaledHeight - (layoutBorderSize * 2),
            layoutCornerEllipseSize, layoutCornerEllipseSize);
        graphics.endFill();
    }

    /**
     *  Returns the borderClass to use based on the currentState.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 2.5
     *  @productversion Flex 4.5
     */
    protected function getBorderClassForCurrentState():Class
    {
        if (currentState == "down")
            return downBorderSkin;
        else
            return upBorderSkin;
    }

    //--------------------------------------------------------------------------
    //
    //  Event Handlers
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function labelDisplay_valueCommitHandler(event:FlexEvent):void
    {
        super.labelDisplay_valueCommitHandler(event);
        labelDisplayShadow.text = labelDisplay.text;
    }

}
}

Fix for https://issues.apache.org/jira/browse/FLEX-34117 Changed the layoutCornerEllipseSize in ButtonSkin.as for various DPIs