From 4b4f317e090f15f60aceaae0a2b7c0a7b28385f3 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 25 Dec 2013 23:42:23 +0800 Subject: [PATCH] refine the ui, link the players to the api server static dir --- trunk/auto/depends.sh | 4 + trunk/research/api-server/server.py | 6 +- trunk/research/players/js/json2.js | 486 ++++++++++++++++++ trunk/research/players/srs_chat.html | 15 +- trunk/research/players/srs_player.html | 1 + trunk/research/players/srs_publisher.html | 1 + .../srs_publisher/release/srs_publisher.swf | Bin 4742 -> 4757 bytes .../srs_publisher/src/srs_publisher.as | 3 + trunk/scripts/dev.sh | 12 +- trunk/scripts/run.sh | 1 + 10 files changed, 514 insertions(+), 15 deletions(-) create mode 100755 trunk/research/players/js/json2.js diff --git a/trunk/auto/depends.sh b/trunk/auto/depends.sh index dded4bba3..56f01dc1e 100755 --- a/trunk/auto/depends.sh +++ b/trunk/auto/depends.sh @@ -300,6 +300,10 @@ else echo "#undef SRS_HTTP" >> $SRS_AUTO_HEADERS_H fi +echo "link players to cherrypy static-dir" +rm -f research/api-server/static-dir/players && +ln -sf `pwd`/research/players research/api-server/static-dir/players + ##################################################################################### # openssl, for rtmp complex handshake ##################################################################################### diff --git a/trunk/research/api-server/server.py b/trunk/research/api-server/server.py index 99918cd87..01b7bf929 100755 --- a/trunk/research/api-server/server.py +++ b/trunk/research/api-server/server.py @@ -474,7 +474,8 @@ if len(sys.argv) <= 1: # parse port from user options. port = int(sys.argv[1]) -trace("api server listen at port: %s"%(port)) +static_dir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "static-dir")) +trace("api server listen at port: %s, static_dir: %s"%(port, static_dir)) # cherrypy config. conf = { @@ -483,9 +484,12 @@ conf = { 'server.socket_host': '0.0.0.0', 'server.socket_port': port, 'tools.encode.on': True, + 'tools.staticdir.on': True, 'tools.encode.encoding': "utf-8" }, '/': { + 'tools.staticdir.dir': static_dir, + 'tools.staticdir.index': "index.html", # for cherrypy RESTful api support 'request.dispatch': cherrypy.dispatch.MethodDispatcher() } diff --git a/trunk/research/players/js/json2.js b/trunk/research/players/js/json2.js new file mode 100755 index 000000000..d89ecc7a2 --- /dev/null +++ b/trunk/research/players/js/json2.js @@ -0,0 +1,486 @@ +/* + json2.js + 2013-05-26 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, regexp: true */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== 'object') { + JSON = {}; +} + +(function () { + 'use strict'; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' + : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function () { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' + ? c + : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? '[]' + : gap + ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' + : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? '{}' + : gap + ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' + : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' + ? walk({'': j}, '') + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/trunk/research/players/srs_chat.html b/trunk/research/players/srs_chat.html index 63f786471..09839a09b 100755 --- a/trunk/research/players/srs_chat.html +++ b/trunk/research/players/srs_chat.html @@ -7,6 +7,7 @@ + @@ -82,6 +83,8 @@ realtime_player.on_player_metadata = function(metadata) { this.set_dar(0, 0); this.set_fs("screen", 100); + + info("推流到服务器成功。请戴耳机聊天,否则音箱的声音会进入麦克风造成回声。"); } realtime_player.start(); } @@ -98,6 +101,8 @@ // remove border of row. $("#lst_chats").find("td").css("border", "none"); + $("#txt_name").focus(); + api_server = "http://" + query.hostname + ":" + srs_get_api_server_port() + "/api/v1/chats"; refresh(); }); @@ -536,15 +541,15 @@
Usage: - 输入名字,设置编码参数后,加入会议室 + 输入名字,设点“加入会议”按钮
- - - - + + + +
diff --git a/trunk/research/players/srs_player.html b/trunk/research/players/srs_player.html index 5ebc545fb..77afeb482 100755 --- a/trunk/research/players/srs_player.html +++ b/trunk/research/players/srs_player.html @@ -7,6 +7,7 @@ + diff --git a/trunk/research/players/srs_publisher.html b/trunk/research/players/srs_publisher.html index 6bdcabcc7..a40382c68 100755 --- a/trunk/research/players/srs_publisher.html +++ b/trunk/research/players/srs_publisher.html @@ -7,6 +7,7 @@ + diff --git a/trunk/research/players/srs_publisher/release/srs_publisher.swf b/trunk/research/players/srs_publisher/release/srs_publisher.swf index c4d2fd2fe6645b5cd1b52be5c726c071623a1da8..0c22b9c9561ddb1e6aea27567f4c67cbadc77811 100755 GIT binary patch literal 4757 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 literal 4742 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