package { import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageDisplayState; import flash.display.StageScaleMode; import flash.events.Event; import flash.events.FullScreenEvent; import flash.events.MouseEvent; import flash.events.NetStatusEvent; import flash.events.TimerEvent; import flash.external.ExternalInterface; import flash.media.SoundTransform; import flash.media.Video; import flash.net.NetConnection; import flash.net.NetStream; import flash.system.Security; import flash.ui.ContextMenu; import flash.ui.ContextMenuItem; import flash.utils.Timer; import flash.utils.getTimer; import flash.utils.setTimeout; import flashx.textLayout.formats.Float; public class srs_player extends Sprite { // user set id. private var js_id:String = null; // user set callback private var js_on_player_ready:String = null; private var js_on_player_metadata:String = null; private var js_on_player_timer:String = null; private var js_on_player_empty:String = null; private var js_on_player_full:String = null; private var js_on_player_status:String = null; // play param, user set width and height private var user_w:int = 0; private var user_h:int = 0; private var user_buffer_time:Number = 0; private var user_max_buffer_time:Number = 0; private var user_volume:Number = 0; // user set dar den:num private var user_dar_den:int = 0; private var user_dar_num:int = 0; // user set fs(fullscreen) refer and percent. private var user_fs_refer:String = null; private var user_fs_percent:int = 0; // media specified. private var media_video:Video = null; private var media_metadata:Object = {}; private var media_timer:Timer = new Timer(300); // controls. // flash donot allow js to set to fullscreen, // only allow user click to enter fullscreen. private var control_fs_mask:Sprite = new Sprite(); // the common player to play stream. private var player:Player = null; // the flashvars config. private var config:Object = null; public function srs_player() { 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.stage.addEventListener(FullScreenEvent.FULL_SCREEN, this.user_on_stage_fullscreen); Security.allowDomain("*"); this.addChild(this.control_fs_mask); this.control_fs_mask.buttonMode = true; this.control_fs_mask.addEventListener(MouseEvent.CLICK, user_on_click_video); 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.config = flashvars; this.js_id = flashvars.id; this.js_on_player_ready = flashvars.on_player_ready; this.js_on_player_metadata = flashvars.on_player_metadata; this.js_on_player_timer = flashvars.on_player_timer; this.js_on_player_empty = flashvars.on_player_empty; this.js_on_player_full = flashvars.on_player_full; this.js_on_player_status = flashvars.on_player_status; this.media_timer.addEventListener(TimerEvent.TIMER, this.system_on_timer); this.media_timer.start(); 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) { log("js not ready, try later."); flash.utils.setTimeout(this.system_on_js_ready, 100); return; } flash.external.ExternalInterface.addCallback("__play", this.js_call_play); flash.external.ExternalInterface.addCallback("__stop", this.js_call_stop); flash.external.ExternalInterface.addCallback("__pause", this.js_call_pause); flash.external.ExternalInterface.addCallback("__resume", this.js_call_resume); flash.external.ExternalInterface.addCallback("__set_dar", this.js_call_set_dar); flash.external.ExternalInterface.addCallback("__set_fs", this.js_call_set_fs_size); flash.external.ExternalInterface.addCallback("__set_bt", this.js_call_set_bt); flash.external.ExternalInterface.addCallback("__set_mbt", this.js_call_set_mbt); flash.external.ExternalInterface.addCallback("__dump_log", this.js_call_dump_log); flash.external.ExternalInterface.call(this.js_on_player_ready, this.js_id); } /** * system callack event, timer to do some regular tasks. */ private function system_on_timer(evt:TimerEvent):void { if (!player) { return; } var ms:NetStream = player.stream(); if (!ms) { //log("stream is null, ignore timer event."); return; } var rtime:Number = flash.utils.getTimer(); var bitrate:Number = Number((ms.info.videoBytesPerSecond + ms.info.audioBytesPerSecond) * 8 / 1000); log("on timer, time=" + ms.time.toFixed(2) + "s, buffer=" + ms.bufferLength.toFixed(2) + "s" + ", bitrate=" + bitrate.toFixed(1) + "kbps" + ", fps=" + ms.currentFPS.toFixed(1) + ", rtime=" + rtime.toFixed(0) ); flash.external.ExternalInterface.call( this.js_on_player_timer, this.js_id, ms.time, ms.bufferLength, bitrate, ms.currentFPS, rtime ); } /** * system callback event, when stream is empty. */ private function system_on_buffer_empty():void { var time:Number = flash.utils.getTimer(); log("stream is empty at " + time + "ms"); flash.external.ExternalInterface.call(this.js_on_player_empty, this.js_id, time); } private function system_on_buffer_full():void { var time:Number = flash.utils.getTimer(); log("stream is full at " + time + "ms"); flash.external.ExternalInterface.call(this.js_on_player_full, this.js_id, time); } /** * system callack event, when got metadata from stream. * or got video dimension change event(the DAR notification), to update the metadata manually. */ private function system_on_metadata(metadata:Object):void { if (!media_metadata) { media_metadata = {}; } for (var k:String in metadata) { media_metadata[k] = metadata[k]; } // update the debug info. on_debug_info(media_metadata); update_context_items(); // for js. var obj:Object = __get_video_size_object(); obj.server = 'srs'; obj.contributor = 'winlin'; if (srs_server != null) { obj.server = srs_server; } if (srs_primary != null) { obj.contributor = srs_primary; } if (srs_authors != null) { obj.contributor = srs_authors; } if (srs_id != null) { obj.cid = srs_id; } if (srs_pid != null) { obj.pid = srs_pid; } if (srs_server_ip != null) { obj.ip = srs_server_ip; } var s:String = ""; for (var key:String in obj) { s += key + "=" + obj[key] + " "; } log("metadata is " + s); var code:int = flash.external.ExternalInterface.call(js_on_player_metadata, js_id, obj); if (code != 0) { throw new Error("callback on_player_metadata failed. code=" + code); } } /** * player callack event, when user click video to enter or leave fullscreen. */ private function user_on_stage_fullscreen(evt:FullScreenEvent):void { if (!evt.fullScreen) { __execute_user_set_dar(); } else { __execute_user_enter_fullscreen(); } } /** * user event callback, js cannot enter the fullscreen mode, user must click to. */ private function user_on_click_video(evt:MouseEvent):void { if (!this.stage.allowsFullScreen) { log("donot allow fullscreen."); return; } // enter fullscreen to get the fullscreen size correctly. if (this.stage.displayState == StageDisplayState.FULL_SCREEN) { this.stage.displayState = StageDisplayState.NORMAL; } else { this.stage.displayState = StageDisplayState.FULL_SCREEN; } } /** * function for js to call: to pause the stream. ignore if not play. */ private function js_call_pause():void { if (player && player.stream()) { player.stream().pause(); log("user pause play"); } } /** * function for js to call: to resume the stream. ignore if not play. */ private function js_call_resume():void { if (player && player.stream()) { player.stream().resume(); log("user resume play"); } } /** * dumps all log data. */ private function js_call_dump_log():String { return Utility.logData; } /** * to set the DAR, for example, DAR=16:9 where num=16,den=9. * @param num, for example, 16. * use metadata width if 0. * use user specified width if -1. * @param den, for example, 9. * use metadata height if 0. * use user specified height if -1. */ private function js_call_set_dar(num:int, den:int):void { user_dar_num = num; user_dar_den = den; flash.utils.setTimeout(__execute_user_set_dar, 0); log("user set dar to " + num + "/" + den); } /** * set the fullscreen size data. * @refer the refer fullscreen mode. it can be: * video: use video orignal size. * screen: use screen size to rescale video. * @param percent, the rescale percent, where * 100 means 100%. */ private function js_call_set_fs_size(refer:String, percent:int):void { user_fs_refer = refer; user_fs_percent = percent; log("user set refer to " + refer + ", percent to" + percent); } /** * set the stream buffer time in seconds. * @buffer_time the buffer time in seconds. */ private function js_call_set_bt(buffer_time:Number):void { if (player && player.stream()) { player.stream().bufferTime = buffer_time; log("user set bufferTime to " + buffer_time.toFixed(2) + "s"); } } /** * set the max stream buffer time in seconds. * @max_buffer_time the max buffer time in seconds. * @remark this is the key feature for realtime communication by flash. */ private function js_call_set_mbt(max_buffer_time:Number):void { if (player && player.stream()) { player.stream().bufferTimeMax = max_buffer_time; log("user set bufferTimeMax to " + max_buffer_time.toFixed(2) + "s"); } } /** * function for js to call: to stop the stream. ignore if not play. */ private function js_call_stop():void { if (this.media_video) { this.removeChild(this.media_video); this.media_video = null; } if (player) { player.close(); player = null; } log("player stopped"); } // 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; } /** * server can set the debug info in _result of RTMP connect, or metadata. */ private function on_debug_info(data:*):void { if (data.hasOwnProperty("srs_server")) { srs_server = data.srs_server; } if (data.hasOwnProperty("srs_primary")) { srs_primary = data.srs_primary; } if (data.hasOwnProperty("srs_authors")) { srs_authors = data.srs_authors; } if (data.hasOwnProperty("srs_id")) { srs_id = data.srs_id; } if (data.hasOwnProperty("srs_pid")) { srs_pid = data.srs_pid; } if (data.hasOwnProperty("srs_server_ip")) { srs_server_ip = data.srs_server_ip; } } /** * function for js to call: to play the stream. stop then play. * @param url, the rtmp/http url to play. * @param _width, the player width. * @param _height, the player height. * @param buffer_time, the buffer time in seconds. recommend to >=0.5 * @param max_buffer_time, the max buffer time in seconds. recommend to 3 x buffer_time. * @param volume, the volume, 0 is mute, 1 is 100%, 2 is 200%. */ private function js_call_play(url:String, _width:int, _height:int, buffer_time:Number, max_buffer_time:Number, volume:Number):void { this.user_w = _width; this.user_h = _height; this.user_buffer_time = buffer_time; this.user_max_buffer_time = max_buffer_time; this.user_volume = volume; log("start to play url: " + url + ", w=" + this.user_w + ", h=" + this.user_h + ", buffer=" + buffer_time.toFixed(2) + "s, max_buffer=" + max_buffer_time.toFixed(2) + "s, volume=" + volume.toFixed(2) ); js_call_stop(); // trim last ? while (Utility.stringEndswith(url, "?")) { url = url.substr(0, url.length - 1); } // create player. player = new Player(this); // init player by config. player.init(config); // play the url. player.play(url); } public function on_player_before_play():void { if (!player) { return; } var ms:NetStream = player.stream(); if (!ms) { return; } ms.soundTransform = new SoundTransform(user_volume); ms.bufferTime = user_buffer_time; ms.bufferTimeMax = user_max_buffer_time; ms.client = {}; ms.client.onMetaData = system_on_metadata; } public function on_player_play():void { if (!player) { return; } media_video = new Video(); media_video.width = user_w; media_video.height = user_h; media_video.attachNetStream(player.stream()); media_video.smoothing = true; addChild(media_video); __draw_black_background(user_w, user_h); // lowest layer, for mask to cover it. setChildIndex(media_video, 0); } public function on_player_metadata(data:Object):void { system_on_metadata(data); } public function on_player_302(url:String):void { setTimeout(function():void{ log("Async RTMP 302 Redirected."); js_call_play(url, user_w, user_h, user_buffer_time, user_max_buffer_time, user_volume); }, 1000); } public function on_player_dimension_change():void { system_on_metadata(media_metadata); } public function on_player_buffer_empty():void { system_on_buffer_empty(); } public function on_player_buffer_full():void { system_on_buffer_full(); } public function on_player_status(code:String, desc:String):void { log("[STATUS] code=" + code + ", desc=" + desc); flash.external.ExternalInterface.call(this.js_on_player_status, this.js_id, code, desc); } /** * get the "right" size of video, * 1. initialize with the original video object size. * 2. override with metadata size if specified. * 3. override with codec size if specified. */ private function __get_video_size_object():Object { if (!media_video) { return {}; } var obj:Object = { width: media_video.width, height: media_video.height }; // override with metadata size. if (this.media_metadata.hasOwnProperty("width")) { obj.width = this.media_metadata.width; } if (this.media_metadata.hasOwnProperty("height")) { obj.height = this.media_metadata.height; } // override with codec size. if (media_video.videoWidth > 0) { obj.width = media_video.videoWidth; } if (media_video.videoHeight > 0) { obj.height = media_video.videoHeight; } return obj; } /** * execute the enter fullscreen action. */ private function __execute_user_enter_fullscreen():void { if (!user_fs_refer || user_fs_percent <= 0) { return; } // change to video size if refer to video. var obj:Object = __get_video_size_object(); // get the DAR var den:int = user_dar_den; var num:int = user_dar_num; if (den == 0) { den = obj.height; } if (den == -1) { den = this.stage.fullScreenHeight; } if (num == 0) { num = obj.width; } if (num == -1) { num = this.stage.fullScreenWidth; } // for refer is screen. if (user_fs_refer == "screen") { obj = { width: this.stage.fullScreenWidth, height: this.stage.fullScreenHeight }; } // rescale to fs __update_video_size(num, den, obj.width * user_fs_percent / 100, obj.height * user_fs_percent / 100, this.stage.fullScreenWidth, this.stage.fullScreenHeight ); } /** * for user set dar, or leave fullscreen to recover the dar. */ private function __execute_user_set_dar():void { // get the DAR var den:int = user_dar_den; var num:int = user_dar_num; var obj:Object = __get_video_size_object(); if (den == 0) { den = obj.height; } if (den == -1) { den = this.user_h; } if (num == 0) { num = obj.width; } if (num == -1) { num = this.user_w; } __update_video_size(num, den, this.user_w, this.user_h, this.user_w, this.user_h); } /** * update the video width and height, * according to the specifies DAR(den:num) and max size(w:h). * set the position of video(x,y) specifies by size(sw:sh), * and update the bg to size(sw:sh). * @param _num/_den the DAR. use to rescale the player together with paper size. * @param _w/_h the video draw paper size. used to rescale the player together with DAR. * @param _sw/_wh the stage size, >= paper size. used to center the player. */ private function __update_video_size(_num:int, _den:int, _w:int, _h:int, _sw:int, _sh:int):void { if (!this.media_video || _den <= 0 || _num <= 0) { return; } // set DAR. // calc the height by DAR var _height:int = _w * _den / _num; if (_height <= _h) { this.media_video.width = _w; this.media_video.height = _height; } else { // height overflow, calc the width by DAR var _width:int = _h * _num / _den; this.media_video.width = _width; this.media_video.height = _h; } // align center. this.media_video.x = (_sw - this.media_video.width) / 2; this.media_video.y = (_sh - this.media_video.height) / 2; __draw_black_background(_sw, _sh); } /** * 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(); } private function log(msg:String):void { Utility.log(js_id, msg); } } }