package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.events.Event;
    import flash.events.NetStatusEvent;
    import flash.external.ExternalInterface;
    import flash.media.Camera;
    import flash.media.H264Profile;
    import flash.media.H264VideoStreamSettings;
    import flash.media.Microphone;
    import flash.media.MicrophoneEnhancedMode;
    import flash.media.MicrophoneEnhancedOptions;
    import flash.media.SoundCodec;
    import flash.media.Video;
    import flash.net.NetConnection;
    import flash.net.NetStream;
    import flash.system.Security;
    import flash.system.SecurityPanel;
    import flash.ui.ContextMenu;
    import flash.ui.ContextMenuItem;
    import flash.utils.setTimeout;
    
    public class srs_publisher extends Sprite
    {
        // user set id.
        private var js_id:String = null;
        // user set callback
        private var js_on_publisher_ready:String = null;
        private var js_on_publisher_error:String = null;
        private var js_on_publisher_warn:String = null;
        
        // publish param url.
        private var user_url:String = null;
        // play param, user set width and height
        private var user_w:int = 0;
        private var user_h:int = 0;
        private var user_vcodec:Object = {};
        private var user_acodec:Object = {};
        
        // media specified.
        private var media_conn:NetConnection = null;
        private var media_stream:NetStream = null;
        private var media_video:Video = null;
        private var media_camera:Camera = null;
        private var media_microphone:Microphone = null;
        
        // error code.
        private const error_camera_get:int = 100;
        private const error_microphone_get:int = 101;
        private const error_camera_muted:int = 102;
        private const error_connection_closed:int = 103;
        private const error_connection_failed:int = 104;
        
        public function srs_publisher()
        {
            if (!this.stage) {
                this.addEventListener(Event.ADDED_TO_STAGE, this.system_on_add_to_stage);
            } else {
                this.system_on_add_to_stage(null);
            }
        }
        
        /**
         * system event callback, when this control added to stage.
         * the main function.
         */
        private function system_on_add_to_stage(evt:Event):void {
            this.removeEventListener(Event.ADDED_TO_STAGE, this.system_on_add_to_stage);
            
            this.stage.align = StageAlign.TOP_LEFT;
            this.stage.scaleMode = StageScaleMode.NO_SCALE;
            
            this.contextMenu = new ContextMenu();
            this.contextMenu.hideBuiltInItems();
            
            var flashvars:Object = this.root.loaderInfo.parameters;
            
            if (!flashvars.hasOwnProperty("id")) {
                throw new Error("must specifies the id");
            }
            
            this.js_id = flashvars.id;
            this.js_on_publisher_ready = flashvars.on_publisher_ready;
            this.js_on_publisher_error = flashvars.on_publisher_error;
            this.js_on_publisher_warn = flashvars.on_publisher_warn;
            
            // initialized size.
            this.user_w = flashvars.width;
            this.user_h = flashvars.height;
            
            // try to get the camera, if muted, alert the security and requires user to open it.
            var c:Camera = Camera.getCamera();
            if (c.muted) {
                Security.showSettings(SecurityPanel.PRIVACY);
            }
            
            __show_local_camera(c);
            
            flash.utils.setTimeout(this.system_on_js_ready, 0);
        }
        
        /**
         * system callack event, when js ready, register callback for js.
         * the actual main function.
         */
        private function system_on_js_ready():void {
            if (!flash.external.ExternalInterface.available) {
                trace("js not ready, try later.");
                flash.utils.setTimeout(this.system_on_js_ready, 100);
                return;
            }
            
            flash.external.ExternalInterface.addCallback("__publish", this.js_call_publish);
            flash.external.ExternalInterface.addCallback("__stop", this.js_call_stop);
            
            var cameras:Array = Camera.names;
            var microphones:Array = Microphone.names;
            trace("retrieve system cameras(" + cameras + ") and microphones(" + microphones + ")");
            
            flash.external.ExternalInterface.call(this.js_on_publisher_ready, this.js_id, cameras, microphones);
        }
        
        /**
        * notify the js an error occur.
        */
        private function system_error(code:int, desc:String):void {
            trace("system error, code=" + code + ", error=" + desc);
            flash.external.ExternalInterface.call(this.js_on_publisher_error, this.js_id, code);
        }
        private function system_warn(code:int, desc:String):void {
            trace("system warn, code=" + code + ", error=" + desc);
            flash.external.ExternalInterface.call(this.js_on_publisher_warn, this.js_id, code);
        }
        
        // srs infos
        private var srs_server:String = null;
        private var srs_primary:String = null;
        private var srs_authors:String = null;
        private var srs_id:String = null;
        private var srs_pid:String = null;
        private var srs_server_ip:String = null;
        private function update_context_items():void {
            // for context menu
            var customItems:Array = [new ContextMenuItem("SrsPlayer")];
            if (srs_server != null) {
                customItems.push(new ContextMenuItem("Server: " + srs_server));
            }
            if (srs_primary != null) {
                customItems.push(new ContextMenuItem("Primary: " + srs_primary));
            }
            if (srs_authors != null) {
                customItems.push(new ContextMenuItem("Authors: " + srs_authors));
            }
            if (srs_server_ip != null) {
                customItems.push(new ContextMenuItem("SrsIp: " + srs_server_ip));
            }
            if (srs_pid != null) {
                customItems.push(new ContextMenuItem("SrsPid: " + srs_pid));
            }
            if (srs_id != null) {
                customItems.push(new ContextMenuItem("SrsId: " + srs_id));
            }
            contextMenu.customItems = customItems;
        }
        
        /**
         * publish stream to server.
         * @param url a string indicates the rtmp url to publish.
         * @param _width, the player width.
         * @param _height, the player height.
         * @param vcodec an object contains the video codec info.
         * @param acodec an object contains the audio codec info.
         */
        private function js_call_publish(url:String, _width:int, _height:int, vcodec:Object, acodec:Object):void {
            trace("start to publish to " + url + ", vcodec " + JSON.stringify(vcodec) + ", acodec " + JSON.stringify(acodec));
            
            this.user_url = url;
            this.user_w = _width;
            this.user_h = _height;
            this.user_vcodec = vcodec;
            this.user_acodec = acodec;
            
            this.js_call_stop();
            
            // microphone and camera
            var microphone:Microphone = null;
            //microphone = Microphone.getEnhancedMicrophone(acodec.device_code);
            if (!microphone) {
                microphone = Microphone.getMicrophone(acodec.device_code);
            }
            if(microphone == null){
                this.system_error(this.error_microphone_get, "failed to open microphone " + acodec.device_code + "(" + acodec.device_name + ")");
                return;
            }
            // ignore muted, for flash will require user to access it.
            
            // Remark: the name is the index!
            var camera:Camera = Camera.getCamera(vcodec.device_code);
            if(camera == null){
                this.system_error(this.error_camera_get, "failed to open camera " + vcodec.device_code + "(" + vcodec.device_name + ")");
                return;
            }
            // ignore muted, for flash will require user to access it.
            // but we still warn user.
            if(camera && camera.muted){
                this.system_warn(this.error_camera_muted, "Access Denied, camera " + vcodec.device_code + "(" + vcodec.device_name + ") is muted");
            }
            
            this.media_camera = camera;
            this.media_microphone = microphone;
            
            this.media_conn = new NetConnection();
            this.media_conn.client = {};
            this.media_conn.client.onBWDone = function():void {};
            this.media_conn.addEventListener(NetStatusEvent.NET_STATUS, function(evt:NetStatusEvent):void {
                trace ("NetConnection: code=" + evt.info.code);
                
                if (evt.info.hasOwnProperty("data") && evt.info.data) {
                    if (evt.info.data.hasOwnProperty("srs_server")) {
                        srs_server = evt.info.data.srs_server;
                    }
                    if (evt.info.data.hasOwnProperty("srs_primary")) {
                        srs_primary = evt.info.data.srs_primary;
                    }
                    if (evt.info.data.hasOwnProperty("srs_authors")) {
                        srs_authors = evt.info.data.srs_authors;
                    }
                    if (evt.info.data.hasOwnProperty("srs_id")) {
                        srs_id = evt.info.data.srs_id;
                    }
                    if (evt.info.data.hasOwnProperty("srs_pid")) {
                        srs_pid = evt.info.data.srs_pid;
                    }
                    if (evt.info.data.hasOwnProperty("srs_server_ip")) {
                        srs_server_ip = evt.info.data.srs_server_ip;
                    }
                    update_context_items();
                }

                if (evt.info.code == "NetConnection.Connect.Closed") {
                    js_call_stop();
                    system_error(error_connection_closed, "server closed the connection");
                    return;
                }
                if (evt.info.code == "NetConnection.Connect.Failed") {
                    js_call_stop();
                    system_error(error_connection_failed, "connect to server failed");
                    return;
                }
                
                // TODO: FIXME: failed event.
                if (evt.info.code != "NetConnection.Connect.Success") {
                    return;
                }
                
                media_stream = new NetStream(media_conn);
                media_stream.client = {};
                media_stream.addEventListener(NetStatusEvent.NET_STATUS, function(evt:NetStatusEvent):void {
                    trace ("NetStream: code=" + evt.info.code);
                    
                    // TODO: FIXME: failed event.
                });
                
                __build_video_codec(media_stream, camera, vcodec);
                __build_audio_codec(media_stream, microphone, acodec);
                
                if (media_microphone) {
                    media_stream.attachAudio(microphone);
                }
                if (media_camera) {
                    media_stream.attachCamera(camera);
                }
                
                var streamName:String = url.substr(url.lastIndexOf("/"));
                media_stream.publish(streamName);
                
                __show_local_camera(media_camera);
            });
            
            var tcUrl:String = this.user_url.substr(0, this.user_url.lastIndexOf("/"));
            this.media_conn.connect(tcUrl);
        }
        
        /**
         * function for js to call: to stop the stream. ignore if not publish.
         */
        private function js_call_stop():void {
            if (this.media_stream) {
                this.media_stream.attachAudio(null);
                this.media_stream.attachCamera(null);
                this.media_stream.close();
                this.media_stream = null;
            }
            if (this.media_conn) {
                this.media_conn.close();
                this.media_conn = null;
            }
        }
        
        private function __build_audio_codec(stream:NetStream, m:Microphone, acodec:Object):void {
            if (!m) {
                return;
            }
            
            // if no microphone, donot set the params.
            if(m == null){
                return;
            }
            
            // use default values.
            var microEncodeQuality:int = 8;
            var microRate:int = 22; // 22 === 22050 Hz
            
            trace("[Publish] audio encoding parameters: " 
                + "audio(microphone) codec=" + acodec.codec + "encodeQuality=" + microEncodeQuality
                + ", rate=" + microRate + "(22050Hz)"
            );
            
            // The encoded speech quality when using the Speex codec. Possible values are from 0 to 10. The default value is 6. Higher numbers 
            // represent higher quality but require more bandwidth, as shown in the following table. The bit rate values that are listed represent 
            // net bit rates and do not include packetization overhead.
            m.encodeQuality = microEncodeQuality;
            
            // The rate at which the microphone is capturing sound, in kHz. Acceptable values are 5, 8, 11, 22, and 44. The default value is 8 kHz 
            // if your sound capture device supports this value. Otherwise, the default value is the next available capture level above 8 kHz that 
            // your sound capture device supports, usually 11 kHz.
            m.rate = microRate;
            
            // see: http://www.adobe.com/cn/devnet/flashplayer/articles/acoustic-echo-cancellation.html
            if (acodec.codec == "nellymoser") {
                m.codec = SoundCodec.NELLYMOSER;
            } else if (acodec.codec == "pcma") {
                m.codec = SoundCodec.PCMA;
            } else if (acodec.codec == "pcmu") {
                m.codec = SoundCodec.PCMU;
            } else {
                m.codec = SoundCodec.SPEEX;
            }
            m.framesPerPacket = 1;
        }
        private function __build_video_codec(stream:NetStream, c:Camera, vcodec:Object):void {
            if (!c) {
                return;
            }
            
            if(vcodec.codec == "vp6"){
                trace("use VP6, donot use H.264 publish encoding.");
                return;
            }

            var x264profile:String = (vcodec.profile == "main") ? H264Profile.MAIN : H264Profile.BASELINE;
            var x264level:String = vcodec.level;
            var cameraFps:Number = Number(vcodec.fps);
            var x264KeyFrameInterval:int = int(vcodec.gop * cameraFps);
            var cameraWidth:int = String(vcodec.size).split("x")[0];
            var cameraHeight:int = String(vcodec.size).split("x")[1];
            var cameraBitrate:int = int(vcodec.bitrate);
            
            // use default values.
            var cameraQuality:int = 85;
            
            trace("[Publish] video h.264(x264) encoding parameters: " 
                + "profile=" + x264profile 
                + ", level=" + x264level
                + ", keyFrameInterval(gop)=" + x264KeyFrameInterval
                + "; video(camera) width=" + cameraWidth
                + ", height=" + cameraHeight
                + ", fps=" + cameraFps
                + ", bitrate=" + cameraBitrate
                + ", quality=" + cameraQuality
            );
            
            var h264Settings:H264VideoStreamSettings = new H264VideoStreamSettings();
            // we MUST set its values first, then set the NetStream.videoStreamSettings, or it will keep the origin values.
            h264Settings.setProfileLevel(x264profile, x264level); 
            stream.videoStreamSettings = h264Settings;
            // the setKeyFrameInterval/setMode/setQuality use the camera settings.
            // http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/media/VideoStreamSettings.html
            // Note This feature will be supported in future releases of Flash Player and AIR, for now, Camera parameters are used.
            //
            //h264Settings.setKeyFrameInterval(4);
            //h264Settings.setMode(800, 600, 15);
            //h264Settings.setQuality(500, 0);
            
            // set the camera and microphone.
            
            // setKeyFrameInterval(keyFrameInterval:int):void
            // 	keyFrameInterval:int — A value that specifies which video frames are transmitted in full (as keyframes) instead of being 
            //		interpolated by the video compression algorithm. A value of 1 means that every frame is a keyframe, a value of 3 means 
            //		that every third frame is a keyframe, and so on. Acceptable values are 1 through 48.
            c.setKeyFrameInterval(x264KeyFrameInterval);
            
            // setMode(width:int, height:int, fps:Number, favorArea:Boolean = true):void
            //  width:int — The requested capture width, in pixels. The default value is 160.
            //  height:int — The requested capture height, in pixels. The default value is 120.
            //  fps:Number — The requested rate at which the camera should capture data, in frames per second. The default value is 15.
            c.setMode(cameraWidth, cameraHeight, cameraFps);
            
            // setQuality(bandwidth:int, quality:int):void
            //  bandwidth:int — Specifies the maximum amount of bandwidth that the current outgoing video feed can use, in bytes per second. 
            //		To specify that the video can use as much bandwidth as needed to maintain the value of quality, pass 0 for bandwidth. 
            //		The default value is 16384.
            //  quality:int — An integer that specifies the required level of picture quality, as determined by the amount of compression 
            // 		being applied to each video frame. Acceptable values range from 1 (lowest quality, maximum compression) to 100 
            //		(highest quality, no compression). To specify that picture quality can vary as needed to avoid exceeding bandwidth, 
            //		pass 0 for quality.
            //  winlin:
            //		bandwidth is in Bps not kbps. 
            //		quality=1 is lowest quality, 100 is highest quality.
            c.setQuality(cameraBitrate / 8.0 * 1000, cameraQuality);
        }
        
        private function __show_local_camera(c:Camera):void {
            if (this.media_video) {
                this.media_video.attachCamera(null);
                this.removeChild(this.media_video);
                this.media_video = null;
            }
            
            // show local camera
            media_video = new Video();
            media_video.attachCamera(c);
            media_video.smoothing = true;
            addChild(media_video);
            
            // rescale the local camera.
            var cw:Number = user_h * c.width / c.height;
            if (cw > user_w) {
                var ch:Number = user_w * c.height / c.width;
                media_video.width = user_w;
                media_video.height = ch;
            } else {
                media_video.width = cw;
                media_video.height = user_h;
            }
            media_video.x = (user_w - media_video.width) / 2;
            media_video.y = (user_h - media_video.height) / 2;
            
            __draw_black_background(user_w, user_h);
            
            // lowest layer, for mask to cover it.
            setChildIndex(media_video, 0);
        }
        
        /**
         * draw black background and draw the fullscreen mask.
         */
        private function __draw_black_background(_width:int, _height:int):void {
            // draw black bg.
            this.graphics.beginFill(0x00, 1.0);
            this.graphics.drawRect(0, 0, _width, _height);
            this.graphics.endFill();
            
            // draw the fs mask.
            //this.control_fs_mask.graphics.beginFill(0xff0000, 0);
            //this.control_fs_mask.graphics.drawRect(0, 0, _width, _height);
            //this.control_fs_mask.graphics.endFill();
        }
    }
}