From 83c615aa8ae8511ec213fb680e9b43077cdffad2 Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 4 Mar 2021 22:45:43 +0800 Subject: [PATCH 001/563] SquashSRS3: Docker: Add conf/docker.conf, daemon off, log console, enable RTC --- trunk/conf/docker.conf | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 trunk/conf/docker.conf diff --git a/trunk/conf/docker.conf b/trunk/conf/docker.conf new file mode 100644 index 000000000..a819e3379 --- /dev/null +++ b/trunk/conf/docker.conf @@ -0,0 +1,44 @@ +# main config for srs. +# @see full.conf for detail config. + +listen 1935; +max_connections 1000; +srs_log_tank console; +daemon off; +http_api { + enabled on; + listen 1985; +} +http_server { + enabled on; + listen 8080; + dir ./objs/nginx/html; +} +stats { + network 0; + disk sda sdb xvda xvdb; +} +rtc_server { + enabled on; + # Listen at udp://8000 + listen 8000; + # + # The $CANDIDATE means fetch from env, if not configed, use * as default. + # + # The * means retrieving server IP automatically, from all network interfaces, + # @see https://github.com/ossrs/srs/issues/307#issuecomment-599028124 + candidate $CANDIDATE; +} +vhost __defaultVhost__ { + hls { + enabled on; + } + http_remux { + enabled on; + mount [vhost]/[app]/[stream].flv; + } + rtc { + enabled on; + bframe discard; + } +} From f80a52f265936af8003e7ecda5a40b31b3239c8e Mon Sep 17 00:00:00 2001 From: winlin Date: Fri, 5 Mar 2021 07:58:32 +0800 Subject: [PATCH 002/563] Update README for docker --- README.md | 106 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 58ded9b82..bad93ce62 100755 --- a/README.md +++ b/README.md @@ -13,6 +13,56 @@ SRS is a RTMP/HLS/WebRTC/SRT/GB28181 streaming cluster, high efficiency, stable ## Usage +Recommend to run SRS by [docker][docker-srs4]: + +```bash +# From Aliyun CR, for developers in China. +docker run --rm -p 1935:1935 -p 1985:1985 -p 8080:8080 \ + registry.cn-hangzhou.aliyuncs.com/ossrs/srs:v4.0.76 + +# From docker hub, depends on your network. +docker run --rm -p 1935:1935 -p 1985:1985 -p 8080:8080 \ + ossrs/srs:v4.0.76 +``` + +> Note: All [tags](https://github.com/ossrs/srs/tags) are available, such as +> `ossrs/srs:v3.0-r3` for tag [v3.0-r3](https://github.com/ossrs/srs/releases/tag/v3.0-r3), +> please check at [here](https://cr.console.aliyun.com/repository/cn-hangzhou/ossrs/srs/images) +> or [there](https://hub.docker.com/r/ossrs/srs/tags). + +> To enable WebRTC, user MUST set the env `CANDIDATE`, see [#307](https://github.com/ossrs/srs/issues/307#issue-76908382). + +If success, please open [http://localhost:8080/](http://localhost:8080/), then publish +[stream](https://github.com/ossrs/srs/blob/3.0release/trunk/doc/source.200kbps.768x320.flv) by: + +```bash +ffmpeg -re -i doc/source.200kbps.768x320.flv -c copy \ + -f flv rtmp://127.0.0.1/live/livestream +``` + +> Note: If WebRTC enabled, you can publish by [H5](http://localhost:8080/players/rtc_publisher.html?autostart=true). + +Play the following streams by players: + +* VLC: rtmp://127.0.0.1/live/livestream +* H5: [http://localhost:8080/live/livestream.flv](http://localhost:8080/players/srs_player.html?autostart=true&stream=livestream.flv&port=8080&schema=http) +* H5: [http://localhost:8080/live/livestream.m3u8](http://localhost:8080/players/srs_player.html?autostart=true&stream=livestream.m3u8&port=8080&schema=http) + +> The online demos and players are available on [ossrs.net](https://ossrs.net). + +Strongly recommend reading bellow wikis: + +* How to deliver RTMP streaming?([CN][v1_CN_SampleRTMP], [EN][v1_EN_SampleRTMP]) +* How to build RTMP Edge-Cluster?([CN][v3_CN_SampleRTMPCluster], [EN][v3_EN_SampleRTMPCluster]) +* How to build RTMP Origin-Cluster?([CN][v3_CN_SampleOriginCluster], [EN][v3_EN_SampleOriginCluster]) +* How to deliver HTTP-FLV streaming?([CN][v3_CN_SampleHttpFlv], [EN][v3_EN_SampleHttpFlv]) +* How to deliver HLS streaming?([CN][v3_CN_SampleHLS], [EN][v3_EN_SampleHLS]) +* How to deliver low-latency streaming?([CN][v3_CN_SampleRealtime], [EN][v3_EN_SampleRealtime]) +* Usage: How to play WebRTC from SRS? [#307](https://github.com/ossrs/srs/issues/307) +* Usage: How to publish WebRTC to SRS? [#307](https://github.com/ossrs/srs/issues/307) + +It's also very easy to build from source: + **>>> Step 1:** Get SRS. ``` @@ -39,52 +89,21 @@ git checkout 4.0release && git pull ./objs/srs -c conf/srs.conf ``` -**>>> Whatever**, you can also directly run SRS in [docker][docker-srs3]: - -``` -docker run -p 1935:1935 -p 1985:1985 -p 8080:8080 \ - registry.cn-hangzhou.aliyuncs.com/ossrs/srs:3 -``` - -> Note: Again, we use [ACR](https://cr.console.aliyun.com/) here, you can directly run in docker hub by `docker run -p 1935:1935 -p 1985:1985 -p 8080:8080 ossrs/srs:3` - -**>>> From here,** strongly recommend to read bellow wikis: - -* Usage: How to delivery RTMP?([CN][v1_CN_SampleRTMP], [EN][v1_EN_SampleRTMP]) -* Usage: How to delivery RTMP-Edge Cluster?([CN][v3_CN_SampleRTMPCluster], [EN][v3_EN_SampleRTMPCluster]) -* Usage: How to create a RTMP-Origin Cluster?([CN][v3_CN_SampleOriginCluster], [EN][v3_EN_SampleOriginCluster]) -* Usage: How to delivery HTTP-FLV?([CN][v3_CN_SampleHttpFlv], [EN][v3_EN_SampleHttpFlv]) -* Usage: How to delivery HTTP-FLV Cluster?([CN][v3_CN_SampleHttpFlvCluster], [EN][v3_EN_SampleHttpFlvCluster]) -* Usage: How to delivery HLS?([CN][v3_CN_SampleHLS], [EN][v3_EN_SampleHLS]) -* Usage: How to publish GB28181 to SRS? [#1500](https://github.com/ossrs/srs/issues/1500#issuecomment-606695679) -* Usage: How to play WebRTC from SRS? [#307](https://github.com/ossrs/srs/issues/307) -* Usage: How to publish WebRTC to SRS? [#307](https://github.com/ossrs/srs/issues/307) -* Usage: How to publish SRT(Experimental) to SRS?([CN][v4_CN_SampleSRT], [EN][v4_EN_SampleSRT]) -* Usage: How to transode stream by FFMPEG?([CN][v2_CN_SampleFFMPEG], [EN][v2_EN_SampleFFMPEG]) -* Usage: How to forward stream to other servers?([CN][v3_CN_SampleForward], [EN][v3_EN_SampleForward]) -* Usage: How to enable low lantency live streaming?([CN][v3_CN_SampleRealtime], [EN][v3_EN_SampleRealtime]) -* Usage: How to ingest file/stream/device to SRS?([CN][v1_CN_SampleIngest], [EN][v1_EN_SampleIngest]) -* Usage: How to delivery DASH(Experimental)?([CN][v3_CN_SampleDASH], [EN][v3_EN_SampleDASH]) -* Usage: How to enable multiple processes? ([CN][v3_CN_REUSEPORT], [EN][v3_EN_REUSEPORT]) -* Usage: Want to contact us? ([CN][v1_CN_Contact], [EN][v1_EN_Contact]) Or file an issue [here](https://github.com/ossrs/srs/issues/new)? - -## Wiki - -Please select according to languages: + +Other documents: +* Usage: How to publish GB28181 to SRS? [#1500](https://github.com/ossrs/srs/issues/1500#issuecomment-606695679) +* Usage: How to delivery DASH(Experimental)?([CN][v3_CN_SampleDASH], [EN][v3_EN_SampleDASH]) +* Usage: How to transode RTMP stream by FFMPEG?([CN][v2_CN_SampleFFMPEG], [EN][v2_EN_SampleFFMPEG]) +* Usage: How to delivery HTTP FLV Live Streaming Cluster?([CN][v3_CN_SampleHttpFlvCluster], [EN][v3_EN_SampleHttpFlvCluster]) +* Usage: How to ingest file/stream/device to RTMP?([CN][v1_CN_SampleIngest], [EN][v1_EN_SampleIngest]) +* Usage: How to forward stream to other servers?([CN][v3_CN_SampleForward], [EN][v3_EN_SampleForward]) +* Usage: How to improve edge performance for multiple CPUs? ([CN][v3_CN_REUSEPORT], [EN][v3_EN_REUSEPORT]) +* Usage: How to file a bug or contact us? ([CN][v1_CN_Contact], [EN][v1_EN_Contact]) * [SRS 4.0 English Wiki][v4_EN_Home] * [SRS 4.0 Chinese Wiki][v4_CN_Home] -For previous versions, please read: - -* [SRS 3.0 English Wiki][v3_EN_Home] -* [SRS 3.0 Chinese Wiki][v3_CN_Home] -* [SRS 2.0 English Wiki][v2_EN_Home] -* [SRS 2.0 Chinese Wiki][v2_CN_Home] -* [SRS 1.0 English Wiki][v1_EN_Home] -* [SRS 1.0 Chinese Wiki][v1_CN_Home] - ## Features - [x] Using coroutine by ST, it's really simple and stupid enough. @@ -1273,7 +1292,7 @@ git clone https://github.com/ossrs/srs.git Supported operating systems and hardware: * All Linux, both 32 and 64 bits -* Other OS, such as Windows, please use [docker][docker-srs3]. +* Other OS, such as Windows, please use [docker][docker-srs4]. Beijing, 2013.10
Winlin @@ -1327,7 +1346,8 @@ Winlin [console]: http://ossrs.net:1985/console [player]: http://ossrs.net/players/srs_player.html [modules]: https://github.com/ossrs/srs/blob/develop/trunk/modules/readme.txt -[docker-srs3]: https://github.com/ossrs/srs-docker#srs3 +[docker-srs3]: https://github.com/ossrs/srs-docker/tree/v3#usage +[docker-srs4]: https://github.com/ossrs/srs-docker/tree/v4#usage [docker-dev]: https://github.com/ossrs/srs-docker/tree/dev#usage [v1_CN_Git]: https://github.com/ossrs/srs/wiki/v1_CN_Git From c143b6bfd0649bf23ee7df36137747e449145d4c Mon Sep 17 00:00:00 2001 From: winlin Date: Fri, 5 Mar 2021 09:53:15 +0800 Subject: [PATCH 003/563] Squash SRS3: Update README for docker --- README.md | 27 +++++++++++++++++++-------- trunk/src/core/srs_core_version3.hpp | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bad93ce62..0d258e561 100755 --- a/README.md +++ b/README.md @@ -16,13 +16,17 @@ SRS is a RTMP/HLS/WebRTC/SRT/GB28181 streaming cluster, high efficiency, stable Recommend to run SRS by [docker][docker-srs4]: ```bash -# From Aliyun CR, for developers in China. +docker run --rm -p 1935:1935 -p 1985:1985 -p 8080:8080 \ + ossrs/srs:v4.0.76 + +# Or, for developers in China to speedup. docker run --rm -p 1935:1935 -p 1985:1985 -p 8080:8080 \ registry.cn-hangzhou.aliyuncs.com/ossrs/srs:v4.0.76 -# From docker hub, depends on your network. +# For macOS to enable WebRTC, other OS please see #307. docker run --rm -p 1935:1935 -p 1985:1985 -p 8080:8080 \ - ossrs/srs:v4.0.76 + --env CANDIDATE=$(ifconfig en0 inet| grep 'inet '|awk '{print $2}') -p 8000:8000/udp \ + registry.cn-hangzhou.aliyuncs.com/ossrs/srs:v4.0.76 ``` > Note: All [tags](https://github.com/ossrs/srs/tags) are available, such as @@ -32,21 +36,27 @@ docker run --rm -p 1935:1935 -p 1985:1985 -p 8080:8080 \ > To enable WebRTC, user MUST set the env `CANDIDATE`, see [#307](https://github.com/ossrs/srs/issues/307#issue-76908382). -If success, please open [http://localhost:8080/](http://localhost:8080/), then publish +If it works, open [http://localhost:8080/](http://localhost:8080/) to check it, then publish [stream](https://github.com/ossrs/srs/blob/3.0release/trunk/doc/source.200kbps.768x320.flv) by: ```bash ffmpeg -re -i doc/source.200kbps.768x320.flv -c copy \ - -f flv rtmp://127.0.0.1/live/livestream + -f flv rtmp://localhost/live/livestream + +# Or by FFmpeg docker +docker run --rm --network=host registry.cn-hangzhou.aliyuncs.com/ossrs/srs:encoder \ + ffmpeg -re -i ./doc/source.200kbps.768x320.flv -c copy \ + -f flv -y rtmp://localhost/live/livestream ``` > Note: If WebRTC enabled, you can publish by [H5](http://localhost:8080/players/rtc_publisher.html?autostart=true). Play the following streams by players: -* VLC: rtmp://127.0.0.1/live/livestream -* H5: [http://localhost:8080/live/livestream.flv](http://localhost:8080/players/srs_player.html?autostart=true&stream=livestream.flv&port=8080&schema=http) -* H5: [http://localhost:8080/live/livestream.m3u8](http://localhost:8080/players/srs_player.html?autostart=true&stream=livestream.m3u8&port=8080&schema=http) +* VLC(RTMP): rtmp://localhost/live/livestream +* H5(HTTP-FLV): [http://localhost:8080/live/livestream.flv](http://localhost:8080/players/srs_player.html?autostart=true&stream=livestream.flv&port=8080&schema=http) +* H5(HLS): [http://localhost:8080/live/livestream.m3u8](http://localhost:8080/players/srs_player.html?autostart=true&stream=livestream.m3u8&port=8080&schema=http) +* H5(WebRTC): [webrtc://localhost/live/livestream](http://localhost:8080/players/rtc_player.html?autostart=true) > The online demos and players are available on [ossrs.net](https://ossrs.net). @@ -247,6 +257,7 @@ Other documents: ## V3 changes +* v3.0, 2021-03-05, Refine usage to docker by default. 3.0.158 * v3.0, 2021-01-07, Change id from int to string for the statistics. 3.0.157 * v3.0, 2021-01-02, [3.0 release3(3.0.156)][r3.0r3] released. 122736 lines. * v3.0, 2020-12-26, For RTMP edge/forward, pass vhost in tcUrl, not in stream. 3.0.156 diff --git a/trunk/src/core/srs_core_version3.hpp b/trunk/src/core/srs_core_version3.hpp index a3d519d3f..b4c8f7273 100644 --- a/trunk/src/core/srs_core_version3.hpp +++ b/trunk/src/core/srs_core_version3.hpp @@ -24,6 +24,6 @@ #ifndef SRS_CORE_VERSION3_HPP #define SRS_CORE_VERSION3_HPP -#define SRS_VERSION3_REVISION 157 +#define SRS_VERSION3_REVISION 158 #endif From fc4f53990785cab81dba1adb4a64c280d6ec9597 Mon Sep 17 00:00:00 2001 From: winlin Date: Fri, 5 Mar 2021 16:47:47 +0800 Subject: [PATCH 004/563] Should check bridger status when publish stream. --- trunk/src/app/srs_app_rtc_source.cpp | 2 ++ trunk/src/app/srs_app_source.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index 96a6bcc18..d111d5e57 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -433,6 +433,8 @@ void SrsRtcStream::on_consumer_destroy(SrsRtcConsumer* consumer) bool SrsRtcStream::can_publish() { + // TODO: FIXME: Should check the status of bridger. + return !is_created_; } diff --git a/trunk/src/app/srs_app_source.cpp b/trunk/src/app/srs_app_source.cpp index 82b2177a1..103d8e20d 100755 --- a/trunk/src/app/srs_app_source.cpp +++ b/trunk/src/app/srs_app_source.cpp @@ -2123,6 +2123,8 @@ void SrsSource::update_auth(SrsRequest* r) bool SrsSource::can_publish(bool is_edge) { + // TODO: FIXME: Should check the status of bridger. + if (is_edge) { return publish_edge->can_publish(); } From 43028c99c8bfd81248353f1d973631ecd661e86e Mon Sep 17 00:00:00 2001 From: winlin Date: Mon, 8 Mar 2021 10:41:42 +0800 Subject: [PATCH 005/563] Fix bug when client DTLS is passive. 4.0.82 --- README.md | 1 + trunk/src/app/srs_app_rtc_conn.cpp | 2 +- trunk/src/app/srs_app_rtc_sdp.hpp | 3 ++- trunk/src/app/srs_app_rtc_server.cpp | 13 +++++++++---- trunk/src/core/srs_core_version4.hpp | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0d258e561..5bc0dec70 100755 --- a/README.md +++ b/README.md @@ -186,6 +186,7 @@ Other documents: ## V4 changes +* v4.0, 2021-03-08, Fix bug when client DTLS is passive. 4.0.82 * v4.0, 2021-03-03, Fix [#2106][bug #2106], [#2011][bug #2011], RTMP/AAC transcode to Opus bug. 4.0.81 * v4.0, 2021-03-02, Refine build script for FFmpeg and SRTP. 4.0.80 * v4.0, 2021-03-02, Upgrade libsrtp from 2.0.0 to 2.3.0, with source code. 4.0.79 diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 27dd2a903..c82241052 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -1920,7 +1920,7 @@ srs_error_t SrsRtcConnection::initialize(SrsRequest* r, bool dtls, bool srtp, st } } - SrsSessionConfig* cfg = &local_sdp.session_config_; + SrsSessionConfig* cfg = &local_sdp.session_negotiate_; if ((err = transport_->initialize(cfg)) != srs_success) { return srs_error_wrap(err, "init"); } diff --git a/trunk/src/app/srs_app_rtc_sdp.hpp b/trunk/src/app/srs_app_rtc_sdp.hpp index 372c893a1..16c3f8e01 100644 --- a/trunk/src/app/srs_app_rtc_sdp.hpp +++ b/trunk/src/app/srs_app_rtc_sdp.hpp @@ -37,7 +37,7 @@ const std::string kTWCCExt = "http://www.ietf.org/id/draft-holmer-rmcat-transpor // TDOO: FIXME: Rename it, and add utest. extern std::vector split_str(const std::string& str, const std::string& delim); -struct SrsSessionConfig +class SrsSessionConfig { public: std::string dtls_role; @@ -237,6 +237,7 @@ public: SrsSessionInfo session_info_; SrsSessionConfig session_config_; + SrsSessionConfig session_negotiate_; std::vector groups_; std::string group_policy_; diff --git a/trunk/src/app/srs_app_rtc_server.cpp b/trunk/src/app/srs_app_rtc_server.cpp index 617337889..9b0300682 100644 --- a/trunk/src/app/srs_app_rtc_server.cpp +++ b/trunk/src/app/srs_app_rtc_server.cpp @@ -590,18 +590,23 @@ srs_error_t SrsRtcServer::do_create_session( srs_trace("RTC: Use candidates %s", srs_join_vector_string(candidate_ips, ", ").c_str()); } + // Setup the negotiate DTLS by config. + local_sdp.session_negotiate_ = local_sdp.session_config_; + + // Setup the negotiate DTLS role. if (remote_sdp.get_dtls_role() == "active") { - local_sdp.set_dtls_role("passive"); + local_sdp.session_negotiate_.dtls_role = "passive"; } else if (remote_sdp.get_dtls_role() == "passive") { - local_sdp.set_dtls_role("active"); + local_sdp.session_negotiate_.dtls_role = "active"; } else if (remote_sdp.get_dtls_role() == "actpass") { - local_sdp.set_dtls_role(local_sdp.session_config_.dtls_role); + local_sdp.session_negotiate_.dtls_role = local_sdp.session_config_.dtls_role; } else { // @see: https://tools.ietf.org/html/rfc4145#section-4.1 // The default value of the setup attribute in an offer/answer exchange // is 'active' in the offer and 'passive' in the answer. - local_sdp.set_dtls_role("passive"); + local_sdp.session_negotiate_.dtls_role = "passive"; } + local_sdp.set_dtls_role(local_sdp.session_negotiate_.dtls_role); session->set_remote_sdp(remote_sdp); // We must setup the local SDP, then initialize the session object. diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index 63540429b..44a734725 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -24,6 +24,6 @@ #ifndef SRS_CORE_VERSION4_HPP #define SRS_CORE_VERSION4_HPP -#define SRS_VERSION4_REVISION 81 +#define SRS_VERSION4_REVISION 82 #endif From 1ed567a005a2a795c6279962bb6e7163ddfadfbd Mon Sep 17 00:00:00 2001 From: winlin Date: Mon, 8 Mar 2021 12:34:15 +0800 Subject: [PATCH 006/563] DTLS: Fix dead loop by duplicated Alert message --- trunk/src/app/srs_app_rtc_dtls.cpp | 30 +++++++++++++++++++++++------- trunk/src/app/srs_app_rtc_dtls.hpp | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_dtls.cpp b/trunk/src/app/srs_app_rtc_dtls.cpp index d2d6bc364..0d813132c 100644 --- a/trunk/src/app/srs_app_rtc_dtls.cpp +++ b/trunk/src/app/srs_app_rtc_dtls.cpp @@ -471,17 +471,33 @@ srs_error_t SrsDtlsImpl::do_on_dtls(char* data, int nb_data) return srs_error_wrap(err, "do handshake"); } - while (BIO_ctrl_pending(bio_in) > 0) { + // If there is data in bio_in, read it to let SSL consume it. + // @remark Limit the max loop, to avoid the dead loop. + for (int i = 0; i < 1024 && BIO_ctrl_pending(bio_in) > 0; i++) { char buf[8092]; - int nb = SSL_read(dtls, buf, sizeof(buf)); - if (nb <= 0) { + int r0 = SSL_read(dtls, buf, sizeof(buf)); + int r1 = SSL_get_error(dtls, r0); + + if (r0 <= 0) { + // SSL_ERROR_ZERO_RETURN + // + // The TLS/SSL connection has been closed. If the protocol version is SSL 3.0 or higher, + // this result code is returned only if a closure alert has occurred in the protocol, + // i.e. if the connection has been closed cleanly. + // @see https://www.openssl.org/docs/man1.1.0/man3/SSL_get_error.html + // @remark Already close, never read again, because padding always exsists. + if (r1 != SSL_ERROR_WANT_READ && r1 != SSL_ERROR_WANT_WRITE) { + break; + } continue; } - srs_trace("DTLS: read nb=%d, data=[%s]", nb, srs_string_dumps_hex(buf, nb, 32).c_str()); - if ((err = callback_->on_dtls_application_data(buf, nb)) != srs_success) { - return srs_error_wrap(err, "on DTLS data, size=%u, data=[%s]", nb, - srs_string_dumps_hex(buf, nb, 32).c_str()); + srs_trace("DTLS: read r0=%d, r1=%d, padding=%d, done=%d, data=[%s]", + r0, r1, BIO_ctrl_pending(bio_in), handshake_done_for_us, srs_string_dumps_hex(buf, r0, 32).c_str()); + + if ((err = callback_->on_dtls_application_data(buf, r0)) != srs_success) { + return srs_error_wrap(err, "on DTLS data, done=%d, r1=%d, size=%u, data=[%s]", handshake_done_for_us, + r1, r0, srs_string_dumps_hex(buf, r0, 32).c_str()); } } diff --git a/trunk/src/app/srs_app_rtc_dtls.hpp b/trunk/src/app/srs_app_rtc_dtls.hpp index 75e099f83..f72f6ef0d 100644 --- a/trunk/src/app/srs_app_rtc_dtls.hpp +++ b/trunk/src/app/srs_app_rtc_dtls.hpp @@ -118,7 +118,7 @@ protected: // @remark: dtls_version_ default value is SrsDtlsVersionAuto. SrsDtlsVersion version_; protected: - // Whether the handhshake is done, for us only. + // Whether the handshake is done, for us only. // @remark For us only, means peer maybe not done, we also need to handle the DTLS packet. bool handshake_done_for_us; // DTLS packet cache, only last out-going packet. From e4df2eb2ce3120cf184a323d9911b25ae33fc380 Mon Sep 17 00:00:00 2001 From: winlin Date: Mon, 8 Mar 2021 12:35:16 +0800 Subject: [PATCH 007/563] DTLS: Fix dead loop by duplicated Alert message. 4.0.83 --- README.md | 1 + trunk/src/core/srs_core_version4.hpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5bc0dec70..58baae36f 100755 --- a/README.md +++ b/README.md @@ -186,6 +186,7 @@ Other documents: ## V4 changes +* v4.0, 2021-03-08, DTLS: Fix dead loop by duplicated Alert message. 4.0.83 * v4.0, 2021-03-08, Fix bug when client DTLS is passive. 4.0.82 * v4.0, 2021-03-03, Fix [#2106][bug #2106], [#2011][bug #2011], RTMP/AAC transcode to Opus bug. 4.0.81 * v4.0, 2021-03-02, Refine build script for FFmpeg and SRTP. 4.0.80 diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index 44a734725..c67ed23d7 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -24,6 +24,6 @@ #ifndef SRS_CORE_VERSION4_HPP #define SRS_CORE_VERSION4_HPP -#define SRS_VERSION4_REVISION 82 +#define SRS_VERSION4_REVISION 83 #endif From 55bdc354f5104938673f0eff8d078ef5cacb2568 Mon Sep 17 00:00:00 2001 From: winlin Date: Mon, 8 Mar 2021 21:05:02 +0800 Subject: [PATCH 008/563] Add important comment for disposing and thread stop --- trunk/src/app/srs_app_conn.hpp | 3 +++ trunk/src/app/srs_app_st.cpp | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/trunk/src/app/srs_app_conn.hpp b/trunk/src/app/srs_app_conn.hpp index db10ee423..ac95ca52d 100644 --- a/trunk/src/app/srs_app_conn.hpp +++ b/trunk/src/app/srs_app_conn.hpp @@ -47,8 +47,11 @@ public: virtual ~ISrsDisposingHandler(); public: // When before disposing resource, trigger when manager.remove(c), sync API. + // @remark Recommend to unref c, after this, no other objects refs to c. virtual void on_before_dispose(ISrsResource* c) = 0; // When disposing resource, async API, c is freed after it. + // @remark Recommend to stop any thread/timer of c, after this, fields of c is able + // to be deleted in any order. virtual void on_disposing(ISrsResource* c) = 0; }; diff --git a/trunk/src/app/srs_app_st.cpp b/trunk/src/app/srs_app_st.cpp index 5a1954a4b..9985f2b44 100755 --- a/trunk/src/app/srs_app_st.cpp +++ b/trunk/src/app/srs_app_st.cpp @@ -207,13 +207,14 @@ srs_error_t SrsFastCoroutine::start() void SrsFastCoroutine::stop() { if (disposed) { + // TODO: FIXME: If previous stop is wait on st_thread_join, this call should assert fail. return; } disposed = true; interrupt(); - // When not started, the rd is NULL. + // When not started, the trd is NULL. if (trd) { void* res = NULL; int r0 = st_thread_join((st_thread_t)trd, &res); @@ -245,7 +246,9 @@ void SrsFastCoroutine::interrupt() if (trd_err == srs_success) { trd_err = srs_error_new(ERROR_THREAD_INTERRUPED, "interrupted"); } - + + // Note that if another thread is stopping thread and waiting in st_thread_join, + // the interrupt will make the st_thread_join fail. st_thread_interrupt((st_thread_t)trd); } From 079c54bbe77156a078d069d72319db564980638c Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 9 Mar 2021 07:21:07 +0800 Subject: [PATCH 009/563] Add important comment for ST --- trunk/src/app/srs_app_st.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trunk/src/app/srs_app_st.cpp b/trunk/src/app/srs_app_st.cpp index 9985f2b44..911680df7 100755 --- a/trunk/src/app/srs_app_st.cpp +++ b/trunk/src/app/srs_app_st.cpp @@ -163,6 +163,8 @@ SrsFastCoroutine::SrsFastCoroutine(string n, ISrsCoroutineHandler* h, SrsContext SrsFastCoroutine::~SrsFastCoroutine() { stop(); + + // TODO: FIXME: We must assert the cycle is done. srs_freep(trd_err); } @@ -207,7 +209,6 @@ srs_error_t SrsFastCoroutine::start() void SrsFastCoroutine::stop() { if (disposed) { - // TODO: FIXME: If previous stop is wait on st_thread_join, this call should assert fail. return; } disposed = true; From 3a5d88b43526f234311f050d6c5117e1d175b663 Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 9 Mar 2021 07:22:11 +0800 Subject: [PATCH 010/563] DTLS: Disable QueryMTU for openssl, or the packet get fragmented --- trunk/src/app/srs_app_rtc_dtls.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/trunk/src/app/srs_app_rtc_dtls.cpp b/trunk/src/app/srs_app_rtc_dtls.cpp index 0d813132c..bdd3b7a4a 100644 --- a/trunk/src/app/srs_app_rtc_dtls.cpp +++ b/trunk/src/app/srs_app_rtc_dtls.cpp @@ -418,6 +418,11 @@ srs_error_t SrsDtlsImpl::initialize(std::string version) SSL_set_ex_data(dtls, 0, this); SSL_set_info_callback(dtls, ssl_on_info); + // set dtls fragment + // @see https://stackoverflow.com/questions/62413602/openssl-server-packets-get-fragmented-into-270-bytes-per-packet + SSL_set_options(dtls, SSL_OP_NO_QUERY_MTU); + SSL_set_mtu(dtls, kRtpPacketSize); + if ((bio_in = BIO_new(BIO_s_mem())) == NULL) { return srs_error_new(ERROR_OpenSslBIONew, "BIO_new in"); } From 3c6e46628001c6fb7efeb5731b8b4049e185e8b2 Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 9 Mar 2021 11:54:27 +0800 Subject: [PATCH 011/563] DTLS: Use specified init API, to decrease packet size --- trunk/src/app/srs_app_rtc_dtls.cpp | 28 ++++++++++++++++++---------- trunk/src/app/srs_app_rtc_dtls.hpp | 6 +++--- trunk/src/utest/srs_utest_rtc.cpp | 4 ++-- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_dtls.cpp b/trunk/src/app/srs_app_rtc_dtls.cpp index bdd3b7a4a..daa894fc1 100644 --- a/trunk/src/app/srs_app_rtc_dtls.cpp +++ b/trunk/src/app/srs_app_rtc_dtls.cpp @@ -96,16 +96,24 @@ void ssl_on_info(const SSL* dtls, int where, int ret) } } -SSL_CTX* srs_build_dtls_ctx(SrsDtlsVersion version) +SSL_CTX* srs_build_dtls_ctx(SrsDtlsVersion version, std::string role) { SSL_CTX* dtls_ctx; #if OPENSSL_VERSION_NUMBER < 0x10002000L // v1.0.2 dtls_ctx = SSL_CTX_new(DTLSv1_method()); #else if (version == SrsDtlsVersion1_0) { - dtls_ctx = SSL_CTX_new(DTLSv1_method()); + if (role == "active") { + dtls_ctx = SSL_CTX_new(DTLSv1_client_method()); + } else { + dtls_ctx = SSL_CTX_new(DTLSv1_server_method()); + } } else if (version == SrsDtlsVersion1_2) { - dtls_ctx = SSL_CTX_new(DTLSv1_2_method()); + if (role == "active") { + dtls_ctx = SSL_CTX_new(DTLS_client_method()); + } else { + dtls_ctx = SSL_CTX_new(DTLS_server_method()); + } } else { // SrsDtlsVersionAuto, use version-flexible DTLS methods dtls_ctx = SSL_CTX_new(DTLS_method()); @@ -397,7 +405,7 @@ SrsDtlsImpl::~SrsDtlsImpl() srs_freepa(last_outgoing_packet_cache); } -srs_error_t SrsDtlsImpl::initialize(std::string version) +srs_error_t SrsDtlsImpl::initialize(std::string version, std::string role) { srs_error_t err = srs_success; @@ -409,7 +417,7 @@ srs_error_t SrsDtlsImpl::initialize(std::string version) version_ = SrsDtlsVersionAuto; } - dtls_ctx = srs_build_dtls_ctx(version_); + dtls_ctx = srs_build_dtls_ctx(version_, role); if ((dtls = SSL_new(dtls_ctx)) == NULL) { return srs_error_new(ERROR_OpenSslCreateSSL, "SSL_new dtls"); @@ -648,11 +656,11 @@ SrsDtlsClientImpl::~SrsDtlsClientImpl() srs_freep(trd); } -srs_error_t SrsDtlsClientImpl::initialize(std::string version) +srs_error_t SrsDtlsClientImpl::initialize(std::string version, std::string role) { srs_error_t err = srs_success; - if ((err = SrsDtlsImpl::initialize(version)) != srs_success) { + if ((err = SrsDtlsImpl::initialize(version, role)) != srs_success) { return err; } @@ -824,11 +832,11 @@ SrsDtlsServerImpl::~SrsDtlsServerImpl() { } -srs_error_t SrsDtlsServerImpl::initialize(std::string version) +srs_error_t SrsDtlsServerImpl::initialize(std::string version, std::string role) { srs_error_t err = srs_success; - if ((err = SrsDtlsImpl::initialize(version)) != srs_success) { + if ((err = SrsDtlsImpl::initialize(version, role)) != srs_success) { return err; } @@ -897,7 +905,7 @@ srs_error_t SrsDtls::initialize(std::string role, std::string version) impl = new SrsDtlsServerImpl(callback_); } - return impl->initialize(version); + return impl->initialize(version, role); } srs_error_t SrsDtls::start_active_handshake() diff --git a/trunk/src/app/srs_app_rtc_dtls.hpp b/trunk/src/app/srs_app_rtc_dtls.hpp index f72f6ef0d..1e28eaf7d 100644 --- a/trunk/src/app/srs_app_rtc_dtls.hpp +++ b/trunk/src/app/srs_app_rtc_dtls.hpp @@ -130,7 +130,7 @@ public: SrsDtlsImpl(ISrsDtlsCallback* callback); virtual ~SrsDtlsImpl(); public: - virtual srs_error_t initialize(std::string version); + virtual srs_error_t initialize(std::string version, std::string role); virtual srs_error_t start_active_handshake() = 0; virtual srs_error_t on_dtls(char* data, int nb_data); protected: @@ -162,7 +162,7 @@ public: SrsDtlsClientImpl(ISrsDtlsCallback* callback); virtual ~SrsDtlsClientImpl(); public: - virtual srs_error_t initialize(std::string version); + virtual srs_error_t initialize(std::string version, std::string role); virtual srs_error_t start_active_handshake(); virtual srs_error_t on_dtls(char* data, int nb_data); protected: @@ -183,7 +183,7 @@ public: SrsDtlsServerImpl(ISrsDtlsCallback* callback); virtual ~SrsDtlsServerImpl(); public: - virtual srs_error_t initialize(std::string version); + virtual srs_error_t initialize(std::string version, std::string role); virtual srs_error_t start_active_handshake(); protected: virtual void on_ssl_out_data(uint8_t*& data, int& size, bool& cached); diff --git a/trunk/src/utest/srs_utest_rtc.cpp b/trunk/src/utest/srs_utest_rtc.cpp index 845f03b53..bf839aca3 100644 --- a/trunk/src/utest/srs_utest_rtc.cpp +++ b/trunk/src/utest/srs_utest_rtc.cpp @@ -563,7 +563,7 @@ VOID TEST(KernelRTCTest, StringDumpHexTest) } } -extern SSL_CTX* srs_build_dtls_ctx(SrsDtlsVersion version); +extern SSL_CTX* srs_build_dtls_ctx(SrsDtlsVersion version, std::string role); class MockDtls { @@ -625,7 +625,7 @@ srs_error_t MockDtls::initialize(std::string role, std::string version) version_ = SrsDtlsVersionAuto; } - dtls_ctx = srs_build_dtls_ctx(version_); + dtls_ctx = srs_build_dtls_ctx(version_, role); dtls = SSL_new(dtls_ctx); srs_assert(dtls); From 02aac0fea4fa6ec5759608d0c00e52c5f9421b08 Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 9 Mar 2021 19:36:45 +0800 Subject: [PATCH 012/563] DTLS: Fix ARQ bug, use openssl timeout. 4.0.84 --- README.md | 1 + trunk/src/app/srs_app_rtc_dtls.cpp | 246 +++++++++++++++++---------- trunk/src/app/srs_app_rtc_dtls.hpp | 20 +-- trunk/src/core/srs_core_version4.hpp | 2 +- 4 files changed, 165 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 58baae36f..f1d66ed6e 100755 --- a/README.md +++ b/README.md @@ -186,6 +186,7 @@ Other documents: ## V4 changes +* v4.0, 2021-03-09, DTLS: Fix ARQ bug, use openssl timeout. 4.0.84 * v4.0, 2021-03-08, DTLS: Fix dead loop by duplicated Alert message. 4.0.83 * v4.0, 2021-03-08, Fix bug when client DTLS is passive. 4.0.82 * v4.0, 2021-03-03, Fix [#2106][bug #2106], [#2011][bug #2011], RTMP/AAC transcode to Opus bug. 4.0.81 diff --git a/trunk/src/app/srs_app_rtc_dtls.cpp b/trunk/src/app/srs_app_rtc_dtls.cpp index daa894fc1..c4b5b4ffa 100644 --- a/trunk/src/app/srs_app_rtc_dtls.cpp +++ b/trunk/src/app/srs_app_rtc_dtls.cpp @@ -35,6 +35,7 @@ using namespace std; #include #include #include +#include #include #include @@ -43,6 +44,35 @@ using namespace std; // Defined in HTTP/HTTPS client. extern int srs_verify_callback(int preverify_ok, X509_STORE_CTX *ctx); +// Setup the openssl timeout for DTLS packet. +// @see https://www.openssl.org/docs/man1.1.1/man3/DTLS_set_timer_cb.html +// +// Use step timeout for ARQ, [50, 100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600, 51200] in ms, +// then total timeout is sum([50, 100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600, 51200]) = 102350ms. +// +// @remark The connection might be closed for timeout in about 30s by default, which stop the DTLS ARQ. +unsigned int dtls_timer_cb(SSL* dtls, unsigned int previous_us) +{ + SrsDtlsImpl* dtls_impl = (SrsDtlsImpl*)SSL_get_ex_data(dtls, 0); + srs_assert(dtls_impl); + + // Double the timeout. Note that it can be 0. + unsigned int timeout_us = previous_us * 2; + + // If previous_us is 0, for example, the HelloVerifyRequest, we should response it ASAP. + // When got ServerHello, we should reset the timer. + if (previous_us == 0 || dtls_impl->should_reset_timer()) { + timeout_us = 50 * 1000; // in us + } + + // Never exceed the max timeout. + timeout_us = srs_min(timeout_us, 30 * 1000 * 1000); // in us + + srs_info("DTLS: ARQ timer cb timeout=%ums, previous=%ums", timeout_us, previous_us); + + return timeout_us; +} + // Print the information of SSL, DTLS alert as such. void ssl_on_info(const SSL* dtls, int where, int ret) { @@ -377,8 +407,6 @@ SrsDtlsImpl::SrsDtlsImpl(ISrsDtlsCallback* callback) callback_ = callback; handshake_done_for_us = false; - last_outgoing_packet_cache = new uint8_t[kRtpPacketSize]; - nn_last_outgoing_packet = 0; nn_arq_packets = 0; version_ = SrsDtlsVersionAuto; @@ -401,8 +429,6 @@ SrsDtlsImpl::~SrsDtlsImpl() SSL_free(dtls); dtls = NULL; } - - srs_freepa(last_outgoing_packet_cache); } srs_error_t SrsDtlsImpl::initialize(std::string version, std::string role) @@ -431,6 +457,19 @@ srs_error_t SrsDtlsImpl::initialize(std::string version, std::string role) SSL_set_options(dtls, SSL_OP_NO_QUERY_MTU); SSL_set_mtu(dtls, kRtpPacketSize); + // @see https://linux.die.net/man/3/openssl_version_number + // MM NN FF PP S + // 0x1010102fL = 0x1 01 01 02 fL // 1.1.1b release + // MM(major) = 0x1 // 1.* + // NN(minor) = 0x01 // 1.1.* + // FF(fix) = 0x01 // 1.1.1* + // PP(patch) = 'a' + 0x02 - 1 = 'b' // 1.1.1b * + // S(status) = 0xf = release // 1.1.1b release + // @note Status 0 for development, 1 to e for betas 1 to 14, and f for release. +#if OPENSSL_VERSION_NUMBER >= 0x1010102fL // 1.1.1b + DTLS_set_timer_cb(dtls, dtls_timer_cb); +#endif + if ((bio_in = BIO_new(BIO_s_mem())) == NULL) { return srs_error_new(ERROR_OpenSslBIONew, "BIO_new in"); } @@ -461,6 +500,12 @@ srs_error_t SrsDtlsImpl::do_on_dtls(char* data, int nb_data) { srs_error_t err = srs_success; + // When already done, only for us, we still got message from client, + // it might be our response is lost, or application data. + if (handshake_done_for_us) { + srs_trace("DTLS: After done, got %d bytes", nb_data); + } + int r0 = 0; // TODO: FIXME: Why reset it before writing? if ((r0 = BIO_reset(bio_in)) != 1) { @@ -471,7 +516,7 @@ srs_error_t SrsDtlsImpl::do_on_dtls(char* data, int nb_data) } // Trace the detail of DTLS packet. - state_trace((uint8_t*)data, nb_data, true, r0, SSL_ERROR_NONE, false, false); + state_trace((uint8_t*)data, nb_data, true, r0, SSL_ERROR_NONE, false); if ((r0 = BIO_write(bio_in, data, nb_data)) <= 0) { // TODO: 0 or -1 maybe block, use BIO_should_retry to check. @@ -502,6 +547,18 @@ srs_error_t SrsDtlsImpl::do_on_dtls(char* data, int nb_data) if (r1 != SSL_ERROR_WANT_READ && r1 != SSL_ERROR_WANT_WRITE) { break; } + + // We got data in memory, which can not read by SSL_read, generally, it's handshake data. + uint8_t* data = NULL; + int size = BIO_get_mem_data(bio_out, (char**)&data); + + // Logging when got SSL original data. + state_trace((uint8_t*)data, size, true, r0, r1, false); + + if (size > 0 && (err = callback_->write_dtls_data(data, size)) != srs_success) { + return srs_error_wrap(err, "dtls send size=%u, data=[%s]", size, + srs_string_dumps_hex((char*)data, size, 32).c_str()); + } continue; } @@ -537,18 +594,10 @@ srs_error_t SrsDtlsImpl::do_handshake() // The data to send out to peer. uint8_t* data = NULL; - int size = BIO_get_mem_data(bio_out, &data); + int size = BIO_get_mem_data(bio_out, (char**)&data); - // Callback when got SSL original data. - bool cache = false; - on_ssl_out_data(data, size, cache); - state_trace((uint8_t*)data, size, false, r0, r1, cache, false); - - // Update the packet cache. - if (size > 0 && data != last_outgoing_packet_cache && size < kRtpPacketSize) { - memcpy(last_outgoing_packet_cache, data, size); - nn_last_outgoing_packet = size; - } + // Logging when got SSL original data. + state_trace((uint8_t*)data, size, false, r0, r1, false); // Callback for the final output data, before send-out. if ((err = on_final_out_data(data, size)) != srs_success) { @@ -569,7 +618,7 @@ srs_error_t SrsDtlsImpl::do_handshake() return err; } -void SrsDtlsImpl::state_trace(uint8_t* data, int length, bool incoming, int r0, int r1, bool cache, bool arq) +void SrsDtlsImpl::state_trace(uint8_t* data, int length, bool incoming, int r0, int r1, bool arq) { // change_cipher_spec(20), alert(21), handshake(22), application_data(23) // @see https://tools.ietf.org/html/rfc2246#section-6.2.1 @@ -588,8 +637,8 @@ void SrsDtlsImpl::state_trace(uint8_t* data, int length, bool incoming, int r0, handshake_type = (uint8_t)data[13]; } - srs_trace("DTLS: %s %s, done=%u, cache=%u, arq=%u/%u, r0=%d, r1=%d, len=%u, cnt=%u, size=%u, hs=%u", - (is_dtls_client()? "Active":"Passive"), (incoming? "RECV":"SEND"), handshake_done_for_us, cache, arq, + srs_trace("DTLS: State %s %s, done=%u, arq=%u/%u, r0=%d, r1=%d, len=%u, cnt=%u, size=%u, hs=%u", + (is_dtls_client()? "Active":"Passive"), (incoming? "RECV":"SEND"), handshake_done_for_us, arq, nn_arq_packets, r0, r1, length, content_type, size, handshake_type); } @@ -640,15 +689,9 @@ SrsDtlsClientImpl::SrsDtlsClientImpl(ISrsDtlsCallback* callback) : SrsDtlsImpl(c trd = NULL; state_ = SrsDtlsStateInit; - // The first wait and base interval for ARQ. - arq_interval = 10 * SRS_UTIME_MILLISECONDS; - - // Use step timeout for ARQ, the total timeout is sum(arq_to_ratios)*arq_interval. - // for example, if arq_interval is 10ms, arq_to_ratios is [3, 6, 9, 15, 20, 40, 80, 160], - // then total timeout is sum([3, 6, 9, 15, 20, 40, 80, 160]) * 10ms = 3330ms. - int ratios[] = {3, 6, 9, 15, 20, 40, 80, 160}; - srs_assert(sizeof(arq_to_ratios) == sizeof(ratios)); - memcpy(arq_to_ratios, ratios, sizeof(ratios)); + // the max dtls retry num is 12 in openssl. + arq_max_retry = 12 * 2; // ARQ for ClientHello and Certificate. + reset_timer_ = true; } SrsDtlsClientImpl::~SrsDtlsClientImpl() @@ -672,60 +715,45 @@ srs_error_t SrsDtlsClientImpl::initialize(std::string version, std::string role) } srs_error_t SrsDtlsClientImpl::start_active_handshake() -{ - return do_handshake(); -} - -srs_error_t SrsDtlsClientImpl::on_dtls(char* data, int nb_data) { srs_error_t err = srs_success; - // When got packet, stop the ARQ if server in the first ARQ state SrsDtlsStateServerHello. - // @note But for ARQ state, we should never stop the ARQ, for example, we are in the second ARQ sate - // SrsDtlsStateServerDone, but we got previous late wrong packet ServeHello, which is not the expect - // packet SessionNewTicket, we should never stop the ARQ thread. - if (state_ == SrsDtlsStateServerHello) { - stop_arq(); + if ((err = do_handshake()) != srs_success) { + return srs_error_wrap(err, "start handshake"); } - if ((err = SrsDtlsImpl::on_dtls(data, nb_data)) != srs_success) { - return err; + if ((err = start_arq()) != srs_success) { + return srs_error_wrap(err, "start arq"); } return err; } -void SrsDtlsClientImpl::on_ssl_out_data(uint8_t*& data, int& size, bool& cached) +bool SrsDtlsClientImpl::should_reset_timer() { - // DTLS client use ARQ thread to send cached packet. - cached = false; + bool v = reset_timer_; + reset_timer_ = false; + return v; } +// Note that only handshake sending packets drives the state, neither ARQ nor the +// final-packets(after handshake done) drives it. srs_error_t SrsDtlsClientImpl::on_final_out_data(uint8_t* data, int size) { srs_error_t err = srs_success; - // Driven ARQ and state for DTLS client. // If we are sending client hello, change from init to new state. - if (state_ == SrsDtlsStateInit && size > 14 && data[13] == 1) { + if (state_ == SrsDtlsStateInit && size > 14 && data[0] == 22 && data[13] == 1) { state_ = SrsDtlsStateClientHello; } + // If we are sending certificate, change from SrsDtlsStateServerHello to new state. - if (state_ == SrsDtlsStateServerHello && size > 14 && data[13] == 11) { + if (state_ == SrsDtlsStateServerHello && size > 14 && data[0] == 22 && data[13] == 11) { state_ = SrsDtlsStateClientCertificate; - } - // Try to start the ARQ for client. - if ((state_ == SrsDtlsStateClientHello || state_ == SrsDtlsStateClientCertificate)) { - if (state_ == SrsDtlsStateClientHello) { - state_ = SrsDtlsStateServerHello; - } else if (state_ == SrsDtlsStateClientCertificate) { - state_ = SrsDtlsStateServerDone; - } - - if ((err = start_arq()) != srs_success) { - return srs_error_wrap(err, "start arq"); - } + // When we send out the certificate, we should reset the timer. + reset_timer_ = true; + srs_info("DTLS: Reset the timer for ServerHello"); } return err; @@ -735,8 +763,15 @@ srs_error_t SrsDtlsClientImpl::on_handshake_done() { srs_error_t err = srs_success; - // When handshake done, stop the ARQ. + // Ignore if done. + if (state_ == SrsDtlsStateClientDone) { + return err; + } + + // Change to done state. state_ = SrsDtlsStateClientDone; + + // When handshake done, stop the ARQ. stop_arq(); // Notify connection the DTLS is done. @@ -756,8 +791,6 @@ srs_error_t SrsDtlsClientImpl::start_arq() { srs_error_t err = srs_success; - srs_info("start arq, state=%u", state_); - // Dispose the previous ARQ thread. srs_freep(trd); trd = new SrsSTCoroutine("dtls", this, _srs_context->get_id()); @@ -772,20 +805,23 @@ srs_error_t SrsDtlsClientImpl::start_arq() void SrsDtlsClientImpl::stop_arq() { - srs_info("stop arq, state=%u", state_); srs_freep(trd); - srs_info("stop arq, done"); } srs_error_t SrsDtlsClientImpl::cycle() { srs_error_t err = srs_success; - // Limit the max retry for ARQ. - for (int i = 0; i < (int)(sizeof(arq_to_ratios) / sizeof(int)); i++) { - srs_utime_t arq_to = arq_interval * arq_to_ratios[i]; - srs_usleep(arq_to); + // Limit the max retry for ARQ, to avoid infinite loop. + // Note that we set the timeout to [50, 100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600, 51200] in ms, + // but the actual timeout is limit to 1s: + // 50ms, 100ms, 200ms, 400ms, 800ms, (1000ms,600ms), (200ms,1000ms,1000ms,1000ms), + // (400ms,1000ms,1000ms,1000ms,1000ms,1000ms,1000ms), ... + // So when the max ARQ limit to 12 times, the max loop is about 103. + const int max_loop = 103; + int arq_count = 0; + for (int i = 0; arq_count < arq_max_retry && i < max_loop; i++) { // We ignore any error for ARQ thread. if ((err = trd->pull()) != srs_success) { srs_freep(err); @@ -798,27 +834,57 @@ srs_error_t SrsDtlsClientImpl::cycle() } // For DTLS client ARQ, the state should be specified. - if (state_ != SrsDtlsStateServerHello && state_ != SrsDtlsStateServerDone) { + if (state_ != SrsDtlsStateClientHello && state_ != SrsDtlsStateClientCertificate) { return err; } - // Try to retransmit the packet. - uint8_t* data = last_outgoing_packet_cache; - int size = nn_last_outgoing_packet; + // If there is a timeout in progress, it sets *out to the time remaining + // and returns one. Otherwise, it returns zero. + int r0 = 0; timeval to = {0}; + if ((r0 = DTLSv1_get_timeout(dtls, &to)) == 0) { + // No timeout, for example?, wait for a default 50ms. + srs_usleep(50 * SRS_UTIME_MILLISECONDS); + continue; + } + srs_utime_t timeout = to.tv_sec + to.tv_usec; - if (size) { - // Trace the detail of DTLS packet. - state_trace((uint8_t*)data, size, false, 1, SSL_ERROR_NONE, true, true); - nn_arq_packets++; - - if ((err = callback_->write_dtls_data(data, size)) != srs_success) { - return srs_error_wrap(err, "dtls send size=%u, data=[%s]", size, - srs_string_dumps_hex((char*)data, size, 32).c_str()); - } + // There is timeout to wait, so we should wait, because there is no packet in openssl. + if (timeout > 0) { + // Note that if we use very small timeout, say 10ms, the client might got two ClientHello, + // then it confused and send HelloVerifyRequest(3) to check it, this is not the efficiency + // way, so we limit the min timeout here to make it faster. + // TODO: FIXME: Config it. + srs_usleep(srs_max(50 * SRS_UTIME_MILLISECONDS, timeout)); + continue; } - srs_info("arq cycle, done=%u, state=%u, retry=%d, interval=%dms, to=%dms, size=%d, nn=%d", handshake_done_for_us, - state_, i, srsu2msi(arq_interval), srsu2msi(arq_to), size, nn_arq_packets); + // The timeout is 0, so there must be a ARQ packet to transmit in openssl. + r0 = BIO_reset(bio_out); int r1 = SSL_get_error(dtls, r0); + if (r0 != 1) { + return srs_error_new(ERROR_OpenSslBIOReset, "BIO_reset r0=%d, r1=%d", r0, r1); + } + + // DTLSv1_handle_timeout is called when a DTLS handshake timeout expires. If no timeout + // had expired, it returns 0. Otherwise, it retransmits the previous flight of handshake + // messages and returns 1. If too many timeouts had expired without progress or an error + // occurs, it returns -1. + r0 = DTLSv1_handle_timeout(dtls); r1 = SSL_get_error(dtls, r0); + if (r0 != 1) { + return srs_error_new(ERROR_RTC_DTLS, "ARQ r0=%d, r1=%d", r0, r1); + } + + // The data to send out to peer. + uint8_t* data = NULL; + int size = BIO_get_mem_data(bio_out, (char**)&data); + + arq_count++; + nn_arq_packets++; + state_trace((uint8_t*)data, size, false, r0, r1, true); + + if (size > 0 && (err = callback_->write_dtls_data(data, size)) != srs_success) { + return srs_error_wrap(err, "dtls send size=%u, data=[%s]", size, + srs_string_dumps_hex((char*)data, size, 32).c_str()); + } } return err; @@ -848,23 +914,19 @@ srs_error_t SrsDtlsServerImpl::initialize(std::string version, std::string role) srs_error_t SrsDtlsServerImpl::start_active_handshake() { + // For DTLS server, we do nothing, because DTLS client drive it. return srs_success; } -void SrsDtlsServerImpl::on_ssl_out_data(uint8_t*& data, int& size, bool& cached) +bool SrsDtlsServerImpl::should_reset_timer() { - // If outgoing packet is empty, we use the last cache. - // @remark Only for DTLS server, because DTLS client use ARQ thread to send cached packet. - if (size <= 0 && nn_last_outgoing_packet) { - size = nn_last_outgoing_packet; - data = last_outgoing_packet_cache; - nn_arq_packets++; - cached = true; - } + // For DTLS server, we never use timer for ARQ, because DTLS client drive it. + return false; } srs_error_t SrsDtlsServerImpl::on_final_out_data(uint8_t* data, int size) { + // No ARQ, driven by DTLS client packets. return srs_success; } diff --git a/trunk/src/app/srs_app_rtc_dtls.hpp b/trunk/src/app/srs_app_rtc_dtls.hpp index 1e28eaf7d..61916a72f 100644 --- a/trunk/src/app/srs_app_rtc_dtls.hpp +++ b/trunk/src/app/srs_app_rtc_dtls.hpp @@ -121,9 +121,6 @@ protected: // Whether the handshake is done, for us only. // @remark For us only, means peer maybe not done, we also need to handle the DTLS packet. bool handshake_done_for_us; - // DTLS packet cache, only last out-going packet. - uint8_t* last_outgoing_packet_cache; - int nn_last_outgoing_packet; // The stat for ARQ packets. int nn_arq_packets; public: @@ -132,16 +129,16 @@ public: public: virtual srs_error_t initialize(std::string version, std::string role); virtual srs_error_t start_active_handshake() = 0; + virtual bool should_reset_timer() = 0; virtual srs_error_t on_dtls(char* data, int nb_data); protected: srs_error_t do_on_dtls(char* data, int nb_data); srs_error_t do_handshake(); - void state_trace(uint8_t* data, int length, bool incoming, int r0, int r1, bool cache, bool arq); + void state_trace(uint8_t* data, int length, bool incoming, int r0, int r1, bool arq); public: srs_error_t get_srtp_key(std::string& recv_key, std::string& send_key); void callback_by_ssl(std::string type, std::string desc); protected: - virtual void on_ssl_out_data(uint8_t*& data, int& size, bool& cached) = 0; virtual srs_error_t on_final_out_data(uint8_t* data, int size) = 0; virtual srs_error_t on_handshake_done() = 0; virtual bool is_dtls_client() = 0; @@ -155,18 +152,19 @@ private: SrsCoroutine* trd; // The DTLS-client state to drive the ARQ thread. SrsDtlsState state_; - // The timeout for ARQ. - srs_utime_t arq_interval; - int arq_to_ratios[8]; + // The max ARQ retry. + int arq_max_retry; + // Should we reset the timer? + // It's true when init, or in state ServerHello. + bool reset_timer_; public: SrsDtlsClientImpl(ISrsDtlsCallback* callback); virtual ~SrsDtlsClientImpl(); public: virtual srs_error_t initialize(std::string version, std::string role); virtual srs_error_t start_active_handshake(); - virtual srs_error_t on_dtls(char* data, int nb_data); + virtual bool should_reset_timer(); protected: - virtual void on_ssl_out_data(uint8_t*& data, int& size, bool& cached); virtual srs_error_t on_final_out_data(uint8_t* data, int size); virtual srs_error_t on_handshake_done(); virtual bool is_dtls_client(); @@ -185,8 +183,8 @@ public: public: virtual srs_error_t initialize(std::string version, std::string role); virtual srs_error_t start_active_handshake(); + virtual bool should_reset_timer(); protected: - virtual void on_ssl_out_data(uint8_t*& data, int& size, bool& cached); virtual srs_error_t on_final_out_data(uint8_t* data, int size); virtual srs_error_t on_handshake_done(); virtual bool is_dtls_client(); diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index c67ed23d7..8a76cdb2f 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -24,6 +24,6 @@ #ifndef SRS_CORE_VERSION4_HPP #define SRS_CORE_VERSION4_HPP -#define SRS_VERSION4_REVISION 83 +#define SRS_VERSION4_REVISION 84 #endif From 62987aa01fe22c3f95fbe5f7c517c62676853b66 Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 9 Mar 2021 22:12:23 +0800 Subject: [PATCH 013/563] DTLS: Refine retransmit between ClientHello and Certificate. --- trunk/src/app/srs_app_rtc_dtls.cpp | 35 +++-- trunk/src/utest/srs_utest_rtc.cpp | 239 ++++++----------------------- 2 files changed, 71 insertions(+), 203 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_dtls.cpp b/trunk/src/app/srs_app_rtc_dtls.cpp index c4b5b4ffa..59b563f57 100644 --- a/trunk/src/app/srs_app_rtc_dtls.cpp +++ b/trunk/src/app/srs_app_rtc_dtls.cpp @@ -68,7 +68,7 @@ unsigned int dtls_timer_cb(SSL* dtls, unsigned int previous_us) // Never exceed the max timeout. timeout_us = srs_min(timeout_us, 30 * 1000 * 1000); // in us - srs_info("DTLS: ARQ timer cb timeout=%ums, previous=%ums", timeout_us, previous_us); + srs_info("DTLS: ARQ timer cb timeout=%ums, previous=%ums", timeout_us/1000, previous_us/1000); return timeout_us; } @@ -503,7 +503,7 @@ srs_error_t SrsDtlsImpl::do_on_dtls(char* data, int nb_data) // When already done, only for us, we still got message from client, // it might be our response is lost, or application data. if (handshake_done_for_us) { - srs_trace("DTLS: After done, got %d bytes", nb_data); + srs_info("DTLS: After done, got %d bytes", nb_data); } int r0 = 0; @@ -553,7 +553,7 @@ srs_error_t SrsDtlsImpl::do_on_dtls(char* data, int nb_data) int size = BIO_get_mem_data(bio_out, (char**)&data); // Logging when got SSL original data. - state_trace((uint8_t*)data, size, true, r0, r1, false); + state_trace((uint8_t*)data, size, false, r0, r1, false); if (size > 0 && (err = callback_->write_dtls_data(data, size)) != srs_success) { return srs_error_wrap(err, "dtls send size=%u, data=[%s]", size, @@ -578,6 +578,12 @@ srs_error_t SrsDtlsImpl::do_handshake() { srs_error_t err = srs_success; + // Done for use, ignore handshake packets. If need to ARQ the handshake packets, + // we should use SSL_read to handle it. + if (handshake_done_for_us) { + return err; + } + // Do handshake and get the result. int r0 = SSL_do_handshake(dtls); int r1 = SSL_get_error(dtls, r0); @@ -690,7 +696,7 @@ SrsDtlsClientImpl::SrsDtlsClientImpl(ISrsDtlsCallback* callback) : SrsDtlsImpl(c state_ = SrsDtlsStateInit; // the max dtls retry num is 12 in openssl. - arq_max_retry = 12 * 2; // ARQ for ClientHello and Certificate. + arq_max_retry = 12 * 2; // Max ARQ limit shared for ClientHello and Certificate. reset_timer_ = true; } @@ -745,15 +751,17 @@ srs_error_t SrsDtlsClientImpl::on_final_out_data(uint8_t* data, int size) // If we are sending client hello, change from init to new state. if (state_ == SrsDtlsStateInit && size > 14 && data[0] == 22 && data[13] == 1) { state_ = SrsDtlsStateClientHello; + return err; } - // If we are sending certificate, change from SrsDtlsStateServerHello to new state. - if (state_ == SrsDtlsStateServerHello && size > 14 && data[0] == 22 && data[13] == 11) { + // If we are sending certificate, change from SrsDtlsStateClientHello to new state. + if (state_ == SrsDtlsStateClientHello && size > 14 && data[0] == 22 && data[13] == 11) { state_ = SrsDtlsStateClientCertificate; // When we send out the certificate, we should reset the timer. reset_timer_ = true; srs_info("DTLS: Reset the timer for ServerHello"); + return err; } return err; @@ -850,11 +858,13 @@ srs_error_t SrsDtlsClientImpl::cycle() // There is timeout to wait, so we should wait, because there is no packet in openssl. if (timeout > 0) { - // Note that if we use very small timeout, say 10ms, the client might got two ClientHello, - // then it confused and send HelloVerifyRequest(3) to check it, this is not the efficiency - // way, so we limit the min timeout here to make it faster. - // TODO: FIXME: Config it. - srs_usleep(srs_max(50 * SRS_UTIME_MILLISECONDS, timeout)); + // Never wait too long, because we might need to retransmit other messages. + // For example, we have transmit 2 ClientHello as [50ms, 100ms] then we sleep(200ms), + // during this we reset the openssl timer to 50ms and need to retransmit Certificate, + // we still need to wait 200ms not 50ms. + timeout = srs_min(100 * SRS_UTIME_MILLISECONDS, timeout); + timeout = srs_max(50 * SRS_UTIME_MILLISECONDS, timeout); + srs_usleep(timeout); continue; } @@ -869,6 +879,9 @@ srs_error_t SrsDtlsClientImpl::cycle() // messages and returns 1. If too many timeouts had expired without progress or an error // occurs, it returns -1. r0 = DTLSv1_handle_timeout(dtls); r1 = SSL_get_error(dtls, r0); + if (r0 == 0) { + continue; // No timeout had expired. + } if (r0 != 1) { return srs_error_new(ERROR_RTC_DTLS, "ARQ r0=%d, r1=%d", r0, r1); } diff --git a/trunk/src/utest/srs_utest_rtc.cpp b/trunk/src/utest/srs_utest_rtc.cpp index bf839aca3..beabf73bf 100644 --- a/trunk/src/utest/srs_utest_rtc.cpp +++ b/trunk/src/utest/srs_utest_rtc.cpp @@ -871,9 +871,12 @@ srs_error_t MockDtlsCallback::cycle() } // Wait for mock io to done, try to switch to coroutine many times. -void mock_wait_dtls_io_done(int count = 100, int interval = 0) +void mock_wait_dtls_io_done(SrsDtlsImpl* client_impl, int count = 100, int interval = 0) { for (int i = 0; i < count; i++) { + if (client_impl) { + dynamic_cast(client_impl)->reset_timer_ = true; + } srs_usleep(interval * SRS_UTIME_MILLISECONDS); } } @@ -895,138 +898,6 @@ public: } }; -VOID TEST(KernelRTCTest, DTLSARQLimitTest) -{ - srs_error_t err = srs_success; - - // ClientHello lost, client retransmit the ClientHello. - if (true) { - MockDtlsCallback cio; SrsDtls client(&cio); - MockDtlsCallback sio; SrsDtls server(&sio); - MockBridgeDtlsIO b0(&cio, &server, NULL); MockBridgeDtlsIO b1(&sio, &client, NULL); - HELPER_EXPECT_SUCCESS(client.initialize("active", "dtls1.0")); - HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); - - // Use very short interval for utest. - dynamic_cast(client.impl)->arq_interval = 1 * SRS_UTIME_MILLISECONDS; - HELPER_ARRAY_INIT(dynamic_cast(client.impl)->arq_to_ratios, 8, 1); - - // Lost 10 packets, total packets should be 9(max to 9). - // Note that only one server hello. - cio.nn_client_hello_lost = 10; - - HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(10, 3); - - EXPECT_TRUE(sio.r0 == srs_success); - EXPECT_TRUE(cio.r0 == srs_success); - - EXPECT_FALSE(cio.done); - EXPECT_FALSE(sio.done); - - EXPECT_EQ(9, cio.nn_client_hello); - EXPECT_EQ(0, sio.nn_server_hello); - EXPECT_EQ(0, cio.nn_certificate); - EXPECT_EQ(0, sio.nn_new_session); - EXPECT_EQ(0, sio.nn_change_cipher); - } - - // Certificate lost, client retransmit the Certificate. - if (true) { - MockDtlsCallback cio; SrsDtls client(&cio); - MockDtlsCallback sio; SrsDtls server(&sio); - MockBridgeDtlsIO b0(&cio, &server, NULL); MockBridgeDtlsIO b1(&sio, &client, NULL); - HELPER_EXPECT_SUCCESS(client.initialize("active", "dtls1.0")); - HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); - - // Use very short interval for utest. - dynamic_cast(client.impl)->arq_interval = 1 * SRS_UTIME_MILLISECONDS; - HELPER_ARRAY_INIT(dynamic_cast(client.impl)->arq_to_ratios, 8, 1); - - // Lost 10 packets, total packets should be 9(max to 9). - // Note that only one server NewSessionTicket. - cio.nn_certificate_lost = 10; - - HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(10, 3); - - EXPECT_TRUE(sio.r0 == srs_success); - EXPECT_TRUE(cio.r0 == srs_success); - - EXPECT_FALSE(cio.done); - EXPECT_FALSE(sio.done); - - EXPECT_EQ(1, cio.nn_client_hello); - EXPECT_EQ(1, sio.nn_server_hello); - EXPECT_EQ(9, cio.nn_certificate); - EXPECT_EQ(0, sio.nn_new_session); - EXPECT_EQ(0, sio.nn_change_cipher); - } - - // ServerHello lost, client retransmit the ClientHello. - if (true) { - MockDtlsCallback cio; SrsDtls client(&cio); - MockDtlsCallback sio; SrsDtls server(&sio); - MockBridgeDtlsIO b0(&cio, &server, NULL); MockBridgeDtlsIO b1(&sio, &client, NULL); - HELPER_EXPECT_SUCCESS(client.initialize("active", "dtls1.0")); - HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); - - // Use very short interval for utest. - dynamic_cast(client.impl)->arq_interval = 1 * SRS_UTIME_MILLISECONDS; - HELPER_ARRAY_INIT(dynamic_cast(client.impl)->arq_to_ratios, 8, 1); - - // Lost 10 packets, total packets should be 9(max to 9). - sio.nn_server_hello_lost = 10; - - HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(10, 3); - - EXPECT_TRUE(sio.r0 == srs_success); - EXPECT_TRUE(cio.r0 == srs_success); - - EXPECT_FALSE(cio.done); - EXPECT_FALSE(sio.done); - - EXPECT_EQ(9, cio.nn_client_hello); - EXPECT_EQ(9, sio.nn_server_hello); - EXPECT_EQ(0, cio.nn_certificate); - EXPECT_EQ(0, sio.nn_new_session); - EXPECT_EQ(0, sio.nn_change_cipher); - } - - // NewSessionTicket lost, client retransmit the Certificate. - if (true) { - MockDtlsCallback cio; SrsDtls client(&cio); - MockDtlsCallback sio; SrsDtls server(&sio); - MockBridgeDtlsIO b0(&cio, &server, NULL); MockBridgeDtlsIO b1(&sio, &client, NULL); - HELPER_EXPECT_SUCCESS(client.initialize("active", "dtls1.0")); - HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); - - // Use very short interval for utest. - dynamic_cast(client.impl)->arq_interval = 1 * SRS_UTIME_MILLISECONDS; - HELPER_ARRAY_INIT(dynamic_cast(client.impl)->arq_to_ratios, 8, 1); - - // Lost 10 packets, total packets should be 9(max to 9). - sio.nn_new_session_lost = 10; - - HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(10, 3); - - EXPECT_TRUE(sio.r0 == srs_success); - EXPECT_TRUE(cio.r0 == srs_success); - - // Although the packet is lost, but it's done for server, and not done for client. - EXPECT_FALSE(cio.done); - EXPECT_TRUE(sio.done); - - EXPECT_EQ(1, cio.nn_client_hello); - EXPECT_EQ(1, sio.nn_server_hello); - EXPECT_EQ(9, cio.nn_certificate); - EXPECT_EQ(9, sio.nn_new_session); - EXPECT_EQ(0, sio.nn_change_cipher); - } -} - VOID TEST(KernelRTCTest, DTLSClientARQTest) { srs_error_t err = srs_success; @@ -1040,7 +911,7 @@ VOID TEST(KernelRTCTest, DTLSClientARQTest) HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(30, 1); + mock_wait_dtls_io_done(client.impl, 15, 5); EXPECT_TRUE(sio.r0 == srs_success); EXPECT_TRUE(cio.r0 == srs_success); @@ -1050,8 +921,8 @@ VOID TEST(KernelRTCTest, DTLSClientARQTest) EXPECT_EQ(1, cio.nn_client_hello); EXPECT_EQ(1, sio.nn_server_hello); - EXPECT_TRUE(1 <= cio.nn_certificate); - EXPECT_TRUE(1 <= sio.nn_new_session); + EXPECT_EQ(1, cio.nn_certificate); + EXPECT_EQ(1, sio.nn_new_session); EXPECT_EQ(0, sio.nn_change_cipher); } @@ -1063,16 +934,12 @@ VOID TEST(KernelRTCTest, DTLSClientARQTest) HELPER_EXPECT_SUCCESS(client.initialize("active", "dtls1.0")); HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); - // Use very short interval for utest. - dynamic_cast(client.impl)->arq_interval = 1 * SRS_UTIME_MILLISECONDS; - HELPER_ARRAY_INIT(dynamic_cast(client.impl)->arq_to_ratios, 8, 1); - // Lost 2 packets, total packets should be 3. // Note that only one server hello. - cio.nn_client_hello_lost = 2; + cio.nn_client_hello_lost = 1; HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(10, 3); + mock_wait_dtls_io_done(client.impl, 15, 5); EXPECT_TRUE(sio.r0 == srs_success); EXPECT_TRUE(cio.r0 == srs_success); @@ -1080,10 +947,10 @@ VOID TEST(KernelRTCTest, DTLSClientARQTest) EXPECT_TRUE(cio.done); EXPECT_TRUE(sio.done); - EXPECT_TRUE(3 <= cio.nn_client_hello); - EXPECT_TRUE(1 <= sio.nn_server_hello); - EXPECT_TRUE(1 <= cio.nn_certificate); - EXPECT_TRUE(1 <= sio.nn_new_session); + EXPECT_EQ(2, cio.nn_client_hello); + EXPECT_EQ(1, sio.nn_server_hello); + EXPECT_EQ(1, cio.nn_certificate); + EXPECT_EQ(1, sio.nn_new_session); EXPECT_EQ(0, sio.nn_change_cipher); } @@ -1095,16 +962,12 @@ VOID TEST(KernelRTCTest, DTLSClientARQTest) HELPER_EXPECT_SUCCESS(client.initialize("active", "dtls1.0")); HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); - // Use very short interval for utest. - dynamic_cast(client.impl)->arq_interval = 1 * SRS_UTIME_MILLISECONDS; - HELPER_ARRAY_INIT(dynamic_cast(client.impl)->arq_to_ratios, 8, 1); - // Lost 2 packets, total packets should be 3. // Note that only one server NewSessionTicket. - cio.nn_certificate_lost = 2; + cio.nn_certificate_lost = 1; HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(10, 3); + mock_wait_dtls_io_done(client.impl, 15, 5); EXPECT_TRUE(sio.r0 == srs_success); EXPECT_TRUE(cio.r0 == srs_success); @@ -1113,9 +976,9 @@ VOID TEST(KernelRTCTest, DTLSClientARQTest) EXPECT_TRUE(sio.done); EXPECT_EQ(1, cio.nn_client_hello); - EXPECT_EQ(1, sio.nn_server_hello); - EXPECT_TRUE(3 <= cio.nn_certificate); - EXPECT_TRUE(1 <= sio.nn_new_session); + EXPECT_EQ(2, sio.nn_server_hello); + EXPECT_EQ(2, cio.nn_certificate); + EXPECT_EQ(0, sio.nn_new_session); EXPECT_EQ(0, sio.nn_change_cipher); } } @@ -1133,7 +996,7 @@ VOID TEST(KernelRTCTest, DTLSServerARQTest) HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(30, 1); + mock_wait_dtls_io_done(client.impl, 15, 5); EXPECT_TRUE(sio.r0 == srs_success); EXPECT_TRUE(cio.r0 == srs_success); @@ -1143,8 +1006,8 @@ VOID TEST(KernelRTCTest, DTLSServerARQTest) EXPECT_EQ(1, cio.nn_client_hello); EXPECT_EQ(1, sio.nn_server_hello); - EXPECT_TRUE(1 <= cio.nn_certificate); - EXPECT_TRUE(1 <= sio.nn_new_session); + EXPECT_EQ(1, cio.nn_certificate); + EXPECT_EQ(1, sio.nn_new_session); EXPECT_EQ(0, sio.nn_change_cipher); } @@ -1156,15 +1019,11 @@ VOID TEST(KernelRTCTest, DTLSServerARQTest) HELPER_EXPECT_SUCCESS(client.initialize("active", "dtls1.0")); HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); - // Use very short interval for utest. - dynamic_cast(client.impl)->arq_interval = 1 * SRS_UTIME_MILLISECONDS; - HELPER_ARRAY_INIT(dynamic_cast(client.impl)->arq_to_ratios, 8, 1); - // Lost 2 packets, total packets should be 3. - sio.nn_server_hello_lost = 2; + sio.nn_server_hello_lost = 1; HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(10, 3); + mock_wait_dtls_io_done(client.impl, 15, 5); EXPECT_TRUE(sio.r0 == srs_success); EXPECT_TRUE(cio.r0 == srs_success); @@ -1172,10 +1031,10 @@ VOID TEST(KernelRTCTest, DTLSServerARQTest) EXPECT_TRUE(cio.done); EXPECT_TRUE(sio.done); - EXPECT_EQ(3, cio.nn_client_hello); - EXPECT_EQ(3, sio.nn_server_hello); - EXPECT_TRUE(1 <= cio.nn_certificate); - EXPECT_TRUE(1 <= sio.nn_new_session); + EXPECT_EQ(2, cio.nn_client_hello); + EXPECT_EQ(2, sio.nn_server_hello); + EXPECT_EQ(1, cio.nn_certificate); + EXPECT_EQ(1, sio.nn_new_session); EXPECT_EQ(0, sio.nn_change_cipher); } @@ -1187,15 +1046,11 @@ VOID TEST(KernelRTCTest, DTLSServerARQTest) HELPER_EXPECT_SUCCESS(client.initialize("active", "dtls1.0")); HELPER_EXPECT_SUCCESS(server.initialize("passive", "dtls1.0")); - // Use very short interval for utest. - dynamic_cast(client.impl)->arq_interval = 1 * SRS_UTIME_MILLISECONDS; - HELPER_ARRAY_INIT(dynamic_cast(client.impl)->arq_to_ratios, 8, 1); - // Lost 2 packets, total packets should be 3. - sio.nn_new_session_lost = 2; + sio.nn_new_session_lost = 1; HELPER_EXPECT_SUCCESS(client.start_active_handshake()); - mock_wait_dtls_io_done(10, 3); + mock_wait_dtls_io_done(client.impl, 15, 5); EXPECT_TRUE(sio.r0 == srs_success); EXPECT_TRUE(cio.r0 == srs_success); @@ -1205,8 +1060,8 @@ VOID TEST(KernelRTCTest, DTLSServerARQTest) EXPECT_EQ(1, cio.nn_client_hello); EXPECT_EQ(1, sio.nn_server_hello); - EXPECT_EQ(3, cio.nn_certificate); - EXPECT_EQ(3, sio.nn_new_session); + EXPECT_EQ(2, cio.nn_certificate); + EXPECT_EQ(2, sio.nn_new_session); EXPECT_EQ(0, sio.nn_change_cipher); } } @@ -1250,10 +1105,10 @@ VOID TEST(KernelRTCTest, DTLSClientFlowTest) {4, "auto", "dtls1.0", true, true, false, false}, // OK, Client: DTLS auto(v1.0 or v1.2), Server: DTLS v1.0 {5, "auto", "dtls1.2", true, true, false, false}, - // Fail, Client: DTLS v1.0, Server: DTLS v1.2 - {6, "dtls1.0", "dtls1.2", false, false, false, true}, - // Fail, Client: DTLS v1.2, Server: DTLS v1.0 - {7, "dtls1.2", "dtls1.0", false, false, true, false}, + // OK?, Client: DTLS v1.0, Server: DTLS v1.2 + {6, "dtls1.0", "dtls1.2", true, true, false, false}, + // OK?, Client: DTLS v1.2, Server: DTLS v1.0 + {7, "dtls1.2", "dtls1.0", true, true, false, false}, }; for (int i = 0; i < (int)(sizeof(cases) / sizeof(DTLSFlowCase)); i++) { @@ -1266,14 +1121,14 @@ VOID TEST(KernelRTCTest, DTLSClientFlowTest) HELPER_EXPECT_SUCCESS(server.initialize("passive", c.ServerVersion)) << c; HELPER_EXPECT_SUCCESS(client.start_active_handshake()) << c; - mock_wait_dtls_io_done(); + mock_wait_dtls_io_done(client.impl, 15, 5); // Note that the cio error is generated from server, vice versa. - EXPECT_EQ(c.ClientError, sio.r0 != srs_success) << c; - EXPECT_EQ(c.ServerError, cio.r0 != srs_success) << c; - EXPECT_EQ(c.ClientDone, cio.done) << c; EXPECT_EQ(c.ServerDone, sio.done) << c; + + EXPECT_EQ(c.ClientError, sio.r0 != srs_success) << c; + EXPECT_EQ(c.ServerError, cio.r0 != srs_success) << c; } } @@ -1294,10 +1149,10 @@ VOID TEST(KernelRTCTest, DTLSServerFlowTest) {4, "auto", "dtls1.0", true, true, false, false}, // OK, Client: DTLS auto(v1.0 or v1.2), Server: DTLS v1.0 {5, "auto", "dtls1.2", true, true, false, false}, - // Fail, Client: DTLS v1.0, Server: DTLS v1.2 - {6, "dtls1.0", "dtls1.2", false, false, false, true}, - // Fail, Client: DTLS v1.2, Server: DTLS v1.0 - {7, "dtls1.2", "dtls1.0", false, false, true, false}, + // OK?, Client: DTLS v1.0, Server: DTLS v1.2 + {6, "dtls1.0", "dtls1.2", true, true, false, false}, + // OK?, Client: DTLS v1.2, Server: DTLS v1.0 + {7, "dtls1.2", "dtls1.0", true, true, false, false}, }; for (int i = 0; i < (int)(sizeof(cases) / sizeof(DTLSFlowCase)); i++) { @@ -1310,14 +1165,14 @@ VOID TEST(KernelRTCTest, DTLSServerFlowTest) HELPER_EXPECT_SUCCESS(server.initialize("passive", c.ServerVersion)) << c; HELPER_EXPECT_SUCCESS(client.start_active_handshake()) << c; - mock_wait_dtls_io_done(); + mock_wait_dtls_io_done(NULL, 15, 5); // Note that the cio error is generated from server, vice versa. - EXPECT_EQ(c.ClientError, sio.r0 != srs_success) << c; - EXPECT_EQ(c.ServerError, cio.r0 != srs_success) << c; - EXPECT_EQ(c.ClientDone, cio.done) << c; EXPECT_EQ(c.ServerDone, sio.done) << c; + + EXPECT_EQ(c.ClientError, sio.r0 != srs_success) << c; + EXPECT_EQ(c.ServerError, cio.r0 != srs_success) << c; } } From d4d11c2c188347271cefd8979a6541f83281e8fb Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 9 Mar 2021 22:19:32 +0800 Subject: [PATCH 014/563] DTLS: Change max loop to larger --- trunk/src/app/srs_app_rtc_dtls.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trunk/src/app/srs_app_rtc_dtls.cpp b/trunk/src/app/srs_app_rtc_dtls.cpp index 59b563f57..a528f2631 100644 --- a/trunk/src/app/srs_app_rtc_dtls.cpp +++ b/trunk/src/app/srs_app_rtc_dtls.cpp @@ -826,7 +826,8 @@ srs_error_t SrsDtlsClientImpl::cycle() // 50ms, 100ms, 200ms, 400ms, 800ms, (1000ms,600ms), (200ms,1000ms,1000ms,1000ms), // (400ms,1000ms,1000ms,1000ms,1000ms,1000ms,1000ms), ... // So when the max ARQ limit to 12 times, the max loop is about 103. - const int max_loop = 103; + // @remark We change the max sleep to 100ms, so we limit about (103*10)/2=500. + const int max_loop = 512; int arq_count = 0; for (int i = 0; arq_count < arq_max_retry && i < max_loop; i++) { From 06f2e1462ed0d0f515d64967510dc17da829ea0b Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 9 Mar 2021 22:26:50 +0800 Subject: [PATCH 015/563] DTLS: Update regression tests --- trunk/3rdparty/srs-bench/LICENSE | 20 + trunk/3rdparty/srs-bench/Makefile | 6 +- trunk/3rdparty/srs-bench/README.md | 46 +- trunk/3rdparty/srs-bench/main.go | 24 +- .../3rdparty/srs-bench/rtc/pion_constants.go | 5 - .../srs-bench/rtc/pion_mediaengine.go | 27 - trunk/3rdparty/srs-bench/rtc/pion_rtpcodec.go | 27 - .../srs-bench/rtc/pion_track_local_static.go | 246 --- trunk/3rdparty/srs-bench/rtc/pion_util.go | 10 - trunk/3rdparty/srs-bench/srs/player.go | 31 +- trunk/3rdparty/srs-bench/srs/publisher.go | 282 +-- trunk/3rdparty/srs-bench/srs/rtc_test.go | 1848 +++++++++++++---- trunk/3rdparty/srs-bench/srs/stat.go | 20 + trunk/3rdparty/srs-bench/srs/util.go | 326 +++ .../srs-bench/vnet/example_udpproxy_test.go | 278 +++ trunk/3rdparty/srs-bench/vnet/udpproxy.go | 222 ++ .../srs-bench/vnet/udpproxy_direct.go | 61 + .../srs-bench/vnet/udpproxy_direct_test.go | 184 ++ .../3rdparty/srs-bench/vnet/udpproxy_test.go | 615 ++++++ 19 files changed, 3329 insertions(+), 949 deletions(-) create mode 100644 trunk/3rdparty/srs-bench/LICENSE delete mode 100644 trunk/3rdparty/srs-bench/rtc/pion_constants.go delete mode 100644 trunk/3rdparty/srs-bench/rtc/pion_mediaengine.go delete mode 100644 trunk/3rdparty/srs-bench/rtc/pion_rtpcodec.go delete mode 100644 trunk/3rdparty/srs-bench/rtc/pion_track_local_static.go delete mode 100644 trunk/3rdparty/srs-bench/rtc/pion_util.go create mode 100644 trunk/3rdparty/srs-bench/vnet/example_udpproxy_test.go create mode 100644 trunk/3rdparty/srs-bench/vnet/udpproxy.go create mode 100644 trunk/3rdparty/srs-bench/vnet/udpproxy_direct.go create mode 100644 trunk/3rdparty/srs-bench/vnet/udpproxy_direct_test.go create mode 100644 trunk/3rdparty/srs-bench/vnet/udpproxy_test.go diff --git a/trunk/3rdparty/srs-bench/LICENSE b/trunk/3rdparty/srs-bench/LICENSE new file mode 100644 index 000000000..77ba5769d --- /dev/null +++ b/trunk/3rdparty/srs-bench/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2021 srs-bench(ossrs) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/trunk/3rdparty/srs-bench/Makefile b/trunk/3rdparty/srs-bench/Makefile index f4ee3f9d0..dedfc5fe5 100644 --- a/trunk/3rdparty/srs-bench/Makefile +++ b/trunk/3rdparty/srs-bench/Makefile @@ -5,18 +5,18 @@ default: bench test clean: rm -f ./objs/srs_bench ./objs/srs_test -.format.txt: *.go rtc/*.go srs/*.go +.format.txt: *.go srs/*.go vnet/*.go gofmt -w . echo "done" > .format.txt bench: ./objs/srs_bench -./objs/srs_bench: .format.txt *.go rtc/*.go srs/*.go Makefile +./objs/srs_bench: .format.txt *.go srs/*.go vnet/*.go Makefile go build -mod=vendor -o objs/srs_bench . test: ./objs/srs_test -./objs/srs_test: .format.txt *.go rtc/*.go srs/*.go Makefile +./objs/srs_test: .format.txt *.go srs/*.go vnet/*.go Makefile go test ./srs -mod=vendor -c -o ./objs/srs_test help: diff --git a/trunk/3rdparty/srs-bench/README.md b/trunk/3rdparty/srs-bench/README.md index 061976383..415cb195f 100644 --- a/trunk/3rdparty/srs-bench/README.md +++ b/trunk/3rdparty/srs-bench/README.md @@ -102,8 +102,7 @@ ffmpeg -re -i doc/source.200kbps.768x320.flv -c copy -f flv -y rtmp://localhost/ 回归测试需要先启动[SRS](https://github.com/ossrs/srs/issues/307),支持WebRTC推拉流: ```bash -eip=$(ifconfig en0 inet| grep 'inet '|awk '{print $2}') -if [[ ! -z $eip ]]; then +if [[ ! -z $(ifconfig en0 inet| grep 'inet '|awk '{print $2}') ]]; then docker run -p 1935:1935 -p 8080:8080 -p 1985:1985 -p 8000:8000/udp \ --rm --env CANDIDATE=$(ifconfig en0 inet| grep 'inet '|awk '{print $2}')\ registry.cn-hangzhou.aliyuncs.com/ossrs/srs:v4.0.76 objs/srs -c conf/rtc.conf @@ -119,7 +118,20 @@ go test ./srs -mod=vendor -v 也可以用make编译出重复使用的二进制: ```bash -make test && ./objs/srs_test -test.v +make && ./objs/srs_test -test.v +``` + +> Note: 注意由于pion不支持`DTLS 1.0`,所以SFU必须要支持`DTLS 1.2`才行。 + +运行结果如下: + +```bash +$ make && ./objs/srs_test -test.v +=== RUN TestRTCServerVersion +--- PASS: TestRTCServerVersion (0.00s) +=== RUN TestRTCServerPublishPlay +--- PASS: TestRTCServerPublishPlay (1.28s) +PASS ``` 可以给回归测试传参数,这样可以测试不同的序列,比如: @@ -127,23 +139,43 @@ make test && ./objs/srs_test -test.v ```bash go test ./srs -mod=vendor -v -srs-server=127.0.0.1 # Or -make test && ./objs/srs_test -test.v -srs-server=127.0.0.1 +make && ./objs/srs_test -test.v -srs-server=127.0.0.1 ``` 支持的参数如下: * `-srs-server`,RTC服务器地址。默认值:`127.0.0.1` * `-srs-stream`,RTC流地址。默认值:`/rtc/regression` -* `-srs-log`,是否开启详细日志。默认值:`false` * `-srs-timeout`,每个Case的超时时间,毫秒。默认值:`3000`,即3秒。 -* `-srs-play-pli`,播放时,PLI的间隔,毫秒。默认值:`5000`,即5秒。 -* `-srs-play-ok-packets`,播放时,收到多少个包认为是测试通过,默认值:`10` * `-srs-publish-audio`,推流时,使用的音频文件。默认值:`avatar.ogg` * `-srs-publish-video`,推流时,使用的视频文件。默认值:`avatar.h264` * `-srs-publish-video-fps`,推流时,视频文件的FPS。默认值:`25` +* `-srs-vnet-client-ip`,设置[pion/vnet](https://github.com/ossrs/srs-bench/blob/feature/rtc/vnet/example_test.go)客户端的虚拟IP,不能和服务器IP冲突。默认值:`192.168.168.168` 其他不常用参数: +* `-srs-log`,是否开启详细日志。默认值:`false` +* `-srs-play-ok-packets`,播放时,收到多少个包认为是测试通过,默认值:`10` +* `-srs-publish-ok-packets`,推流时,发送多少个包认为时测试通过,默认值:`10` * `-srs-https`,是否连接HTTPS-API。默认值:`false`,即连接HTTP-API。 +* `-srs-play-pli`,播放时,PLI的间隔,毫秒。默认值:`5000`,即5秒。 +* `-srs-dtls-drop-packets`,DTLS丢包测试,丢了多少个包算成功,默认值:`5` + +## GCOVR + +本机生成覆盖率时,我们使用工具[gcovr](https://gcovr.com/en/stable/guide.html)。 + +在macOS上安装gcovr: + +```bash +pip3 install gcovr +``` + +在CentOS上安装gcovr: + +```bash +yum install -y python2-pip && +pip install lxml && pip install gcovr +``` 2021.01, Winlin diff --git a/trunk/3rdparty/srs-bench/main.go b/trunk/3rdparty/srs-bench/main.go index a2384a524..d56fa4995 100644 --- a/trunk/3rdparty/srs-bench/main.go +++ b/trunk/3rdparty/srs-bench/main.go @@ -1,3 +1,23 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package main import ( @@ -42,7 +62,7 @@ func main() { flag.IntVar(&delay, "delay", 50, "") var statListen string - flag.StringVar(&statListen, "stat", ":18000", "") + flag.StringVar(&statListen, "stat", "", "") flag.Usage = func() { fmt.Println(fmt.Sprintf("Usage: %v [Options]", os.Args[0])) @@ -52,7 +72,7 @@ func main() { fmt.Println(fmt.Sprintf(" -delay The start delay in ms for each client or stream to simulate. Default: 50")) fmt.Println(fmt.Sprintf(" -al [Optional] Whether enable audio-level. Default: true")) fmt.Println(fmt.Sprintf(" -twcc [Optional] Whether enable vdieo-twcc. Default: true")) - fmt.Println(fmt.Sprintf(" -stat [Optional] The stat server API listen port. Default: :18000")) + fmt.Println(fmt.Sprintf(" -stat [Optional] The stat server API listen port.")) fmt.Println(fmt.Sprintf("Player or Subscriber:")) fmt.Println(fmt.Sprintf(" -sr The url to play/subscribe. If sn exceed 1, auto append variable %%d.")) fmt.Println(fmt.Sprintf(" -da [Optional] The file path to dump audio, ignore if empty.")) diff --git a/trunk/3rdparty/srs-bench/rtc/pion_constants.go b/trunk/3rdparty/srs-bench/rtc/pion_constants.go deleted file mode 100644 index 4b44a7064..000000000 --- a/trunk/3rdparty/srs-bench/rtc/pion_constants.go +++ /dev/null @@ -1,5 +0,0 @@ -package rtc - -const ( - rtpOutboundMTU = 1200 -) diff --git a/trunk/3rdparty/srs-bench/rtc/pion_mediaengine.go b/trunk/3rdparty/srs-bench/rtc/pion_mediaengine.go deleted file mode 100644 index a86e71f24..000000000 --- a/trunk/3rdparty/srs-bench/rtc/pion_mediaengine.go +++ /dev/null @@ -1,27 +0,0 @@ -package rtc - -import ( - "github.com/pion/rtp" - "github.com/pion/rtp/codecs" - "github.com/pion/webrtc/v3" - "strings" -) - -func payloaderForCodec(codec webrtc.RTPCodecCapability) (rtp.Payloader, error) { - switch strings.ToLower(codec.MimeType) { - case strings.ToLower(webrtc.MimeTypeH264): - return &codecs.H264Payloader{}, nil - case strings.ToLower(webrtc.MimeTypeOpus): - return &codecs.OpusPayloader{}, nil - case strings.ToLower(webrtc.MimeTypeVP8): - return &codecs.VP8Payloader{}, nil - case strings.ToLower(webrtc.MimeTypeVP9): - return &codecs.VP9Payloader{}, nil - case strings.ToLower(webrtc.MimeTypeG722): - return &codecs.G722Payloader{}, nil - case strings.ToLower(webrtc.MimeTypePCMU), strings.ToLower(webrtc.MimeTypePCMA): - return &codecs.G711Payloader{}, nil - default: - return nil, webrtc.ErrNoPayloaderForCodec - } -} diff --git a/trunk/3rdparty/srs-bench/rtc/pion_rtpcodec.go b/trunk/3rdparty/srs-bench/rtc/pion_rtpcodec.go deleted file mode 100644 index c7f85828b..000000000 --- a/trunk/3rdparty/srs-bench/rtc/pion_rtpcodec.go +++ /dev/null @@ -1,27 +0,0 @@ -package rtc - -import ( - "github.com/pion/webrtc/v3" - "strings" -) - -// Do a fuzzy find for a codec in the list of codecs -// Used for lookup up a codec in an existing list to find a match -func codecParametersFuzzySearch(needle webrtc.RTPCodecParameters, haystack []webrtc.RTPCodecParameters) (webrtc.RTPCodecParameters, error) { - // First attempt to match on MimeType + SDPFmtpLine - for _, c := range haystack { - if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) && - c.RTPCodecCapability.SDPFmtpLine == needle.RTPCodecCapability.SDPFmtpLine { - return c, nil - } - } - - // Fallback to just MimeType - for _, c := range haystack { - if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) { - return c, nil - } - } - - return webrtc.RTPCodecParameters{}, webrtc.ErrCodecNotFound -} diff --git a/trunk/3rdparty/srs-bench/rtc/pion_track_local_static.go b/trunk/3rdparty/srs-bench/rtc/pion_track_local_static.go deleted file mode 100644 index 4bf40cdb9..000000000 --- a/trunk/3rdparty/srs-bench/rtc/pion_track_local_static.go +++ /dev/null @@ -1,246 +0,0 @@ -package rtc - -import ( - "github.com/pion/rtp" - "github.com/pion/webrtc/v3" - "github.com/pion/webrtc/v3/pkg/media" - "strings" - "sync" -) - -// trackBinding is a single bind for a Track -// Bind can be called multiple times, this stores the -// result for a single bind call so that it can be used when writing -type trackBinding struct { - id string - ssrc webrtc.SSRC - payloadType webrtc.PayloadType - writeStream webrtc.TrackLocalWriter -} - -// TrackLocalStaticRTP is a TrackLocal that has a pre-set codec and accepts RTP Packets. -// If you wish to send a media.Sample use TrackLocalStaticSample -type TrackLocalStaticRTP struct { - mu sync.RWMutex - bindings []trackBinding - codec webrtc.RTPCodecCapability - id, streamID string -} - -// NewTrackLocalStaticRTP returns a TrackLocalStaticRTP. -func NewTrackLocalStaticRTP(c webrtc.RTPCodecCapability, id, streamID string) (*TrackLocalStaticRTP, error) { - return &TrackLocalStaticRTP{ - codec: c, - bindings: []trackBinding{}, - id: id, - streamID: streamID, - }, nil -} - -// Bind is called by the PeerConnection after negotiation is complete -// This asserts that the code requested is supported by the remote peer. -// If so it setups all the state (SSRC and PayloadType) to have a call -func (s *TrackLocalStaticRTP) Bind(t webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) { - s.mu.Lock() - defer s.mu.Unlock() - - parameters := webrtc.RTPCodecParameters{RTPCodecCapability: s.codec} - if codec, err := codecParametersFuzzySearch(parameters, t.CodecParameters()); err == nil { - s.bindings = append(s.bindings, trackBinding{ - ssrc: t.SSRC(), - payloadType: codec.PayloadType, - writeStream: t.WriteStream(), - id: t.ID(), - }) - return codec, nil - } - - return webrtc.RTPCodecParameters{}, webrtc.ErrUnsupportedCodec -} - -// Unbind implements the teardown logic when the track is no longer needed. This happens -// because a track has been stopped. -func (s *TrackLocalStaticRTP) Unbind(t webrtc.TrackLocalContext) error { - s.mu.Lock() - defer s.mu.Unlock() - - for i := range s.bindings { - if s.bindings[i].id == t.ID() { - s.bindings[i] = s.bindings[len(s.bindings)-1] - s.bindings = s.bindings[:len(s.bindings)-1] - return nil - } - } - - return webrtc.ErrUnbindFailed -} - -// ID is the unique identifier for this Track. This should be unique for the -// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' -// and StreamID would be 'desktop' or 'webcam' -func (s *TrackLocalStaticRTP) ID() string { return s.id } - -// StreamID is the group this track belongs too. This must be unique -func (s *TrackLocalStaticRTP) StreamID() string { return s.streamID } - -// Kind controls if this TrackLocal is audio or video -func (s *TrackLocalStaticRTP) Kind() webrtc.RTPCodecType { - switch { - case strings.HasPrefix(s.codec.MimeType, "audio/"): - return webrtc.RTPCodecTypeAudio - case strings.HasPrefix(s.codec.MimeType, "video/"): - return webrtc.RTPCodecTypeVideo - default: - return webrtc.RTPCodecType(0) - } -} - -// Codec gets the Codec of the track -func (s *TrackLocalStaticRTP) Codec() webrtc.RTPCodecCapability { - return s.codec -} - -// WriteRTP writes a RTP Packet to the TrackLocalStaticRTP -// If one PeerConnection fails the packets will still be sent to -// all PeerConnections. The error message will contain the ID of the failed -// PeerConnections so you can remove them -func (s *TrackLocalStaticRTP) WriteRTP(p *rtp.Packet) error { - s.mu.RLock() - defer s.mu.RUnlock() - - writeErrs := []error{} - outboundPacket := *p - - for _, b := range s.bindings { - outboundPacket.Header.SSRC = uint32(b.ssrc) - outboundPacket.Header.PayloadType = uint8(b.payloadType) - if _, err := b.writeStream.WriteRTP(&outboundPacket.Header, outboundPacket.Payload); err != nil { - writeErrs = append(writeErrs, err) - } - } - - return FlattenErrs(writeErrs) -} - -// Write writes a RTP Packet as a buffer to the TrackLocalStaticRTP -// If one PeerConnection fails the packets will still be sent to -// all PeerConnections. The error message will contain the ID of the failed -// PeerConnections so you can remove them -func (s *TrackLocalStaticRTP) Write(b []byte) (n int, err error) { - packet := &rtp.Packet{} - if err = packet.Unmarshal(b); err != nil { - return 0, err - } - - return len(b), s.WriteRTP(packet) -} - -// TrackLocalStaticSample is a TrackLocal that has a pre-set codec and accepts Samples. -// If you wish to send a RTP Packet use TrackLocalStaticRTP -type TrackLocalStaticSample struct { - packetizer rtp.Packetizer - rtpTrack *TrackLocalStaticRTP - clockRate float64 - - // Set the callback before write RTP packet. - OnBeforeWritePacket func(rtp *rtp.Packet) -} - -// NewTrackLocalStaticSample returns a TrackLocalStaticSample -func NewTrackLocalStaticSample(c webrtc.RTPCodecCapability, id, streamID string) (*TrackLocalStaticSample, error) { - rtpTrack, err := NewTrackLocalStaticRTP(c, id, streamID) - if err != nil { - return nil, err - } - - return &TrackLocalStaticSample{ - rtpTrack: rtpTrack, - }, nil -} - -// ID is the unique identifier for this Track. This should be unique for the -// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' -// and StreamID would be 'desktop' or 'webcam' -func (s *TrackLocalStaticSample) ID() string { return s.rtpTrack.ID() } - -// StreamID is the group this track belongs too. This must be unique -func (s *TrackLocalStaticSample) StreamID() string { return s.rtpTrack.StreamID() } - -// Kind controls if this TrackLocal is audio or video -func (s *TrackLocalStaticSample) Kind() webrtc.RTPCodecType { return s.rtpTrack.Kind() } - -// Codec gets the Codec of the track -func (s *TrackLocalStaticSample) Codec() webrtc.RTPCodecCapability { - return s.rtpTrack.Codec() -} - -// Bind is called by the PeerConnection after negotiation is complete -// This asserts that the code requested is supported by the remote peer. -// If so it setups all the state (SSRC and PayloadType) to have a call -func (s *TrackLocalStaticSample) Bind(t webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) { - codec, err := s.rtpTrack.Bind(t) - if err != nil { - return codec, err - } - - s.rtpTrack.mu.Lock() - defer s.rtpTrack.mu.Unlock() - - // We only need one packetizer - if s.packetizer != nil { - return codec, nil - } - - payloader, err := payloaderForCodec(codec.RTPCodecCapability) - if err != nil { - return codec, err - } - - s.packetizer = rtp.NewPacketizer( - rtpOutboundMTU, - 0, // Value is handled when writing - 0, // Value is handled when writing - payloader, - rtp.NewRandomSequencer(), - codec.ClockRate, - ) - s.clockRate = float64(codec.RTPCodecCapability.ClockRate) - return codec, nil -} - -// Unbind implements the teardown logic when the track is no longer needed. This happens -// because a track has been stopped. -func (s *TrackLocalStaticSample) Unbind(t webrtc.TrackLocalContext) error { - return s.rtpTrack.Unbind(t) -} - -// WriteSample writes a Sample to the TrackLocalStaticSample -// If one PeerConnection fails the packets will still be sent to -// all PeerConnections. The error message will contain the ID of the failed -// PeerConnections so you can remove them -func (s *TrackLocalStaticSample) WriteSample(sample media.Sample) error { - s.rtpTrack.mu.RLock() - p := s.packetizer - clockRate := s.clockRate - s.rtpTrack.mu.RUnlock() - - if p == nil { - return nil - } - - samples := sample.Duration.Seconds() * clockRate - packets := p.(rtp.Packetizer).Packetize(sample.Data, uint32(samples)) - - writeErrs := []error{} - for _, p := range packets { - if s.OnBeforeWritePacket != nil { - s.OnBeforeWritePacket(p) - } - - if err := s.rtpTrack.WriteRTP(p); err != nil { - writeErrs = append(writeErrs, err) - } - } - - return FlattenErrs(writeErrs) -} diff --git a/trunk/3rdparty/srs-bench/rtc/pion_util.go b/trunk/3rdparty/srs-bench/rtc/pion_util.go deleted file mode 100644 index 75ffe4601..000000000 --- a/trunk/3rdparty/srs-bench/rtc/pion_util.go +++ /dev/null @@ -1,10 +0,0 @@ -package rtc - -import "fmt" - -func FlattenErrs(errors []error) error { - if len(errors) == 0 { - return nil - } - return fmt.Errorf("%v", errors) -} diff --git a/trunk/3rdparty/srs-bench/srs/player.go b/trunk/3rdparty/srs-bench/srs/player.go index 8977248e6..0947ad41c 100644 --- a/trunk/3rdparty/srs-bench/srs/player.go +++ b/trunk/3rdparty/srs-bench/srs/player.go @@ -1,3 +1,23 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package srs import ( @@ -65,7 +85,14 @@ func StartPlay(ctx context.Context, r, dumpAudio, dumpVideo string, enableAudioL if err != nil { return errors.Wrapf(err, "Create PC") } - defer pc.Close() + + var receivers []*webrtc.RTPReceiver + defer func() { + pc.Close() + for _, receiver := range receivers { + receiver.Stop() + } + }() pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{ Direction: webrtc.RTPTransceiverDirectionRecvonly, @@ -132,6 +159,8 @@ func StartPlay(ctx context.Context, r, dumpAudio, dumpVideo string, enableAudioL } }() + receivers = append(receivers, receiver) + codec := track.Codec() trackDesc := fmt.Sprintf("channels=%v", codec.Channels) diff --git a/trunk/3rdparty/srs-bench/srs/publisher.go b/trunk/3rdparty/srs-bench/srs/publisher.go index 172ff9e21..8d38fb055 100644 --- a/trunk/3rdparty/srs-bench/srs/publisher.go +++ b/trunk/3rdparty/srs-bench/srs/publisher.go @@ -1,20 +1,33 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package srs import ( "context" "github.com/ossrs/go-oryx-lib/errors" "github.com/ossrs/go-oryx-lib/logger" - "github.com/ossrs/srs-bench/rtc" "github.com/pion/interceptor" - "github.com/pion/rtp" "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" - "github.com/pion/webrtc/v3/pkg/media" - "github.com/pion/webrtc/v3/pkg/media/h264reader" - "github.com/pion/webrtc/v3/pkg/media/oggreader" "io" - "os" - "strings" "sync" "time" ) @@ -26,7 +39,12 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i logger.Tf(ctx, "Start publish url=%v, audio=%v, video=%v, fps=%v, audio-level=%v, twcc=%v", r, sourceAudio, sourceVideo, fps, enableAudioLevel, enableTWCC) - // For audio-level. + // Filter for SPS/PPS marker. + var aIngester *audioIngester + var vIngester *videoIngester + + // For audio-level and sps/pps marker. + // TODO: FIXME: Should share with player. webrtcNewPeerConnection := func(configuration webrtc.Configuration) (*webrtc.PeerConnection, error) { m := &webrtc.MediaEngine{} if err := m.RegisterDefaultCodecs(); err != nil { @@ -53,12 +71,21 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i } } - i := &interceptor.Registry{} - if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { + registry := &interceptor.Registry{} + if err := webrtc.RegisterDefaultInterceptors(m, registry); err != nil { return nil, err } - api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)) + if sourceAudio != "" { + aIngester = NewAudioIngester(sourceAudio) + registry.Add(aIngester.audioLevelInterceptor) + } + if sourceVideo != "" { + vIngester = NewVideoIngester(sourceVideo) + registry.Add(vIngester.markerInterceptor) + } + + api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(registry)) return api.NewPeerConnection(configuration) } @@ -66,46 +93,30 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i if err != nil { return errors.Wrapf(err, "Create PC") } - defer pc.Close() - var sVideoTrack *rtc.TrackLocalStaticSample - var sVideoSender *webrtc.RTPSender - if sourceVideo != "" { - mimeType, trackID := "video/H264", "video" - if strings.HasSuffix(sourceVideo, ".ivf") { - mimeType = "video/VP8" + doClose := func() { + if pc != nil { + pc.Close() } + if vIngester != nil { + vIngester.Close() + } + if aIngester != nil { + aIngester.Close() + } + } + defer doClose() - sVideoTrack, err = rtc.NewTrackLocalStaticSample( - webrtc.RTPCodecCapability{MimeType: mimeType, ClockRate: 90000}, trackID, "pion", - ) - if err != nil { - return errors.Wrapf(err, "Create video track") + if vIngester != nil { + if err := vIngester.AddTrack(pc, fps); err != nil { + return errors.Wrapf(err, "Add track") } - - sVideoSender, err = pc.AddTrack(sVideoTrack) - if err != nil { - return errors.Wrapf(err, "Add video track") - } - sVideoSender.Stop() } - var sAudioTrack *rtc.TrackLocalStaticSample - var sAudioSender *webrtc.RTPSender - if sourceAudio != "" { - mimeType, trackID := "audio/opus", "audio" - sAudioTrack, err = rtc.NewTrackLocalStaticSample( - webrtc.RTPCodecCapability{MimeType: mimeType, ClockRate: 48000, Channels: 2}, trackID, "pion", - ) - if err != nil { - return errors.Wrapf(err, "Create audio track") + if aIngester != nil { + if err := aIngester.AddTrack(pc); err != nil { + return errors.Wrapf(err, "Add track") } - - sAudioSender, err = pc.AddTrack(sAudioTrack) - if err != nil { - return errors.Wrapf(err, "Add audio track") - } - defer sAudioSender.Stop() } offer, err := pc.CreateOffer(nil) @@ -139,9 +150,11 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i logger.Tf(ctx, "Signaling state %v", state) }) - sAudioSender.Transport().OnStateChange(func(state webrtc.DTLSTransportState) { - logger.Tf(ctx, "DTLS state %v", state) - }) + if aIngester != nil { + aIngester.sAudioSender.Transport().OnStateChange(func(state webrtc.DTLSTransportState) { + logger.Tf(ctx, "DTLS state %v", state) + }) + } ctx, cancel := context.WithCancel(ctx) pcDone, pcDoneCancel := context.WithCancel(context.Background()) @@ -168,8 +181,15 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i wg.Add(1) go func() { defer wg.Done() + <-ctx.Done() + doClose() // Interrupt the RTCP read. + }() - if sAudioSender == nil { + wg.Add(1) + go func() { + defer wg.Done() + + if aIngester == nil { return } @@ -181,7 +201,7 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i buf := make([]byte, 1500) for ctx.Err() == nil { - if _, _, err := sAudioSender.Read(buf); err != nil { + if _, _, err := aIngester.sAudioSender.Read(buf); err != nil { return } } @@ -191,7 +211,7 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i go func() { defer wg.Done() - if sAudioTrack == nil { + if aIngester == nil { return } @@ -201,8 +221,9 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i logger.Tf(ctx, "PC(ICE+DTLS+SRTP) done, start ingest audio %v", sourceAudio) } + // Read audio and send out. for ctx.Err() == nil { - if err := readAudioTrackFromDisk(ctx, sourceAudio, sAudioSender, sAudioTrack); err != nil { + if err := aIngester.Ingest(ctx); err != nil { if errors.Cause(err) == io.EOF { logger.Tf(ctx, "EOF, restart ingest audio %v", sourceAudio) continue @@ -216,7 +237,7 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i go func() { defer wg.Done() - if sVideoSender == nil { + if vIngester == nil { return } @@ -228,7 +249,7 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i buf := make([]byte, 1500) for ctx.Err() == nil { - if _, _, err := sVideoSender.Read(buf); err != nil { + if _, _, err := vIngester.sVideoSender.Read(buf); err != nil { return } } @@ -238,7 +259,7 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i go func() { defer wg.Done() - if sVideoTrack == nil { + if vIngester == nil { return } @@ -249,7 +270,7 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i } for ctx.Err() == nil { - if err := readVideoTrackFromDisk(ctx, sourceVideo, sVideoSender, fps, sVideoTrack); err != nil { + if err := vIngester.Ingest(ctx); err != nil { if errors.Cause(err) == io.EOF { logger.Tf(ctx, "EOF, restart ingest video %v", sourceVideo) continue @@ -276,154 +297,3 @@ func StartPublish(ctx context.Context, r, sourceAudio, sourceVideo string, fps i wg.Wait() return nil } - -func readAudioTrackFromDisk(ctx context.Context, source string, sender *webrtc.RTPSender, track *rtc.TrackLocalStaticSample) error { - f, err := os.Open(source) - if err != nil { - return errors.Wrapf(err, "Open file %v", source) - } - defer f.Close() - - ogg, _, err := oggreader.NewWith(f) - if err != nil { - return errors.Wrapf(err, "Open ogg %v", source) - } - - enc := sender.GetParameters().Encodings[0] - codec := sender.GetParameters().Codecs[0] - headers := sender.GetParameters().HeaderExtensions - logger.Tf(ctx, "Audio %v, tbn=%v, channels=%v, ssrc=%v, pt=%v, header=%v", - codec.MimeType, codec.ClockRate, codec.Channels, enc.SSRC, codec.PayloadType, headers) - - // Whether should encode the audio-level in RTP header. - var audioLevel *webrtc.RTPHeaderExtensionParameter - for _, h := range headers { - if h.URI == sdp.AudioLevelURI { - audioLevel = &h - } - } - - clock := newWallClock() - var lastGranule uint64 - - for ctx.Err() == nil { - pageData, pageHeader, err := ogg.ParseNextPage() - if err == io.EOF { - return nil - } - if err != nil { - return errors.Wrapf(err, "Read ogg") - } - - // The amount of samples is the difference between the last and current timestamp - sampleCount := uint64(pageHeader.GranulePosition - lastGranule) - lastGranule = pageHeader.GranulePosition - sampleDuration := time.Duration(uint64(time.Millisecond) * 1000 * sampleCount / uint64(codec.ClockRate)) - - // For audio-level, set the extensions if negotiated. - track.OnBeforeWritePacket = func(p *rtp.Packet) { - if audioLevel != nil { - if b, err := new(rtp.AudioLevelExtension).Marshal(); err == nil { - p.SetExtension(uint8(audioLevel.ID), b) - } - } - } - - if err = track.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); err != nil { - return errors.Wrapf(err, "Write sample") - } - - if d := clock.Tick(sampleDuration); d > 0 { - time.Sleep(d) - } - } - - return nil -} - -func readVideoTrackFromDisk(ctx context.Context, source string, sender *webrtc.RTPSender, fps int, track *rtc.TrackLocalStaticSample) error { - f, err := os.Open(source) - if err != nil { - return errors.Wrapf(err, "Open file %v", source) - } - defer f.Close() - - // TODO: FIXME: Support ivf for vp8. - h264, err := h264reader.NewReader(f) - if err != nil { - return errors.Wrapf(err, "Open h264 %v", source) - } - - enc := sender.GetParameters().Encodings[0] - codec := sender.GetParameters().Codecs[0] - headers := sender.GetParameters().HeaderExtensions - logger.Tf(ctx, "Video %v, tbn=%v, fps=%v, ssrc=%v, pt=%v, header=%v", - codec.MimeType, codec.ClockRate, fps, enc.SSRC, codec.PayloadType, headers) - - clock := newWallClock() - sampleDuration := time.Duration(uint64(time.Millisecond) * 1000 / uint64(fps)) - for ctx.Err() == nil { - var sps, pps *h264reader.NAL - var oFrames []*h264reader.NAL - for ctx.Err() == nil { - frame, err := h264.NextNAL() - if err == io.EOF { - return nil - } - if err != nil { - return errors.Wrapf(err, "Read h264") - } - - oFrames = append(oFrames, frame) - logger.If(ctx, "NALU %v PictureOrderCount=%v, ForbiddenZeroBit=%v, RefIdc=%v, %v bytes", - frame.UnitType.String(), frame.PictureOrderCount, frame.ForbiddenZeroBit, frame.RefIdc, len(frame.Data)) - - if frame.UnitType == h264reader.NalUnitTypeSPS { - sps = frame - } else if frame.UnitType == h264reader.NalUnitTypePPS { - pps = frame - } else { - break - } - } - - var frames []*h264reader.NAL - // Package SPS/PPS to STAP-A - if sps != nil && pps != nil { - stapA := packageAsSTAPA(sps, pps) - frames = append(frames, stapA) - } - // Append other original frames. - for _, frame := range oFrames { - if frame.UnitType != h264reader.NalUnitTypeSPS && frame.UnitType != h264reader.NalUnitTypePPS { - frames = append(frames, frame) - } - } - - // Covert frames to sample(buffers). - for i, frame := range frames { - sample := media.Sample{Data: frame.Data, Duration: sampleDuration} - // Use the sample timestamp for frames. - if i != len(frames)-1 { - sample.Duration = 0 - } - - // For STAP-A, set marker to false, to make Chrome happy. - track.OnBeforeWritePacket = func(p *rtp.Packet) { - if i < len(frames)-1 { - p.Header.Marker = false - } - } - - if err = track.WriteSample(sample); err != nil { - return errors.Wrapf(err, "Write sample") - } - } - - if d := clock.Tick(sampleDuration); d > 0 { - time.Sleep(d) - } - } - - return nil -} diff --git a/trunk/3rdparty/srs-bench/srs/rtc_test.go b/trunk/3rdparty/srs-bench/srs/rtc_test.go index da7d89949..4ac869c42 100644 --- a/trunk/3rdparty/srs-bench/srs/rtc_test.go +++ b/trunk/3rdparty/srs-bench/srs/rtc_test.go @@ -1,449 +1,1467 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package srs import ( "context" - "encoding/json" - "flag" "fmt" "github.com/ossrs/go-oryx-lib/errors" "github.com/ossrs/go-oryx-lib/logger" - "github.com/ossrs/srs-bench/rtc" + "github.com/pion/interceptor" "github.com/pion/rtcp" - "github.com/pion/webrtc/v3" - "io" - "io/ioutil" - "net/http" + "github.com/pion/rtp" + "github.com/pion/transport/vnet" + "math/rand" "os" - "strings" "sync" "testing" "time" ) -var srsSchema = "http" -var srsHttps = flag.Bool("srs-https", false, "Whther connect to HTTPS-API") -var srsServer = flag.String("srs-server", "127.0.0.1", "The RTC server to connect to") -var srsStream = flag.String("srs-stream", "/rtc/regression", "The RTC stream to play") -var srsLog = flag.Bool("srs-log", false, "Whether enable the detail log") -var srsTimeout = flag.Int("srs-timeout", 3000, "For each case, the timeout in ms") -var srsPlayPLI = flag.Int("srs-play-pli", 5000, "The PLI interval in seconds for player.") -var srsPlayOKPackets = flag.Int("srs-play-ok-packets", 10, "If got N packets, it's ok, or fail") -var srsPublishAudio = flag.String("srs-publish-audio", "avatar.ogg", "The audio file for publisher.") -var srsPublishVideo = flag.String("srs-publish-video", "avatar.h264", "The video file for publisher.") -var srsPublishVideoFps = flag.Int("srs-publish-video-fps", 25, "The video fps for publisher.") - -func TestMain(m *testing.M) { - // Should parse it first. - flag.Parse() - - // The stream should starts with /, for example, /rtc/regression - if strings.HasPrefix(*srsStream, "/") { - *srsStream = "/" + *srsStream - } - - // Generate srs protocol from whether use HTTPS. - if *srsHttps { - srsSchema = "https" - } - - // Disable the logger during all tests. - logger.Tf(nil, "sys log %v", *srsLog) - - if *srsLog == false { - olw := logger.Switch(ioutil.Discard) - defer func() { - logger.Switch(olw) - }() - } - - // Run tests. - os.Exit(m.Run()) -} - -func TestRTCServerVersion(t *testing.T) { - api := fmt.Sprintf("http://%v:1985/api/v1/versions", *srsServer) - req, err := http.NewRequest("POST", api, nil) - if err != nil { - t.Errorf("Request %v", api) - return - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - t.Errorf("Do request %v", api) - return - } - - b, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Errorf("Read body of %v", api) - return - } - - obj := struct { - Code int `json:"code"` - Server string `json:"server"` - Data struct { - Major int `json:"major"` - Minor int `json:"minor"` - Revision int `json:"revision"` - Version string `json:"version"` - } `json:"data"` - }{} - if err := json.Unmarshal(b, &obj); err != nil { - t.Errorf("Parse %v", string(b)) - return - } - if obj.Code != 0 { - t.Errorf("Server err code=%v, server=%v", obj.Code, obj.Server) - return - } - if obj.Data.Major == 0 && obj.Data.Minor == 0 { - t.Errorf("Invalid version %v", obj.Data) - return - } -} - -func TestRTCServerPublishPlay(t *testing.T) { +// Basic use scenario, publish a stream, then play it. +func TestRtcBasic_PublishPlay(t *testing.T) { ctx := logger.WithContext(context.Background()) - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithTimeout(ctx, time.Duration(*srsTimeout)*time.Millisecond) - r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream) - publishReady, publishReadyCancel := context.WithCancel(context.Background()) - - startPlay := func(ctx context.Context) error { - logger.Tf(ctx, "Start play url=%v", r) - - pc, err := webrtc.NewPeerConnection(webrtc.Configuration{}) - if err != nil { - return errors.Wrapf(err, "Create PC") + var r0, r1, r2, r3 error + defer func(ctx context.Context) { + if err := filterTestError(ctx.Err(), r0, r1, r2, r3); err != nil { + t.Errorf("Fail for err %+v", err) + } else { + logger.Tf(ctx, "test done with err %+v", err) } - defer pc.Close() - - pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{ - Direction: webrtc.RTPTransceiverDirectionRecvonly, - }) - pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{ - Direction: webrtc.RTPTransceiverDirectionRecvonly, - }) - - offer, err := pc.CreateOffer(nil) - if err != nil { - return errors.Wrapf(err, "Create Offer") - } - - if err := pc.SetLocalDescription(offer); err != nil { - return errors.Wrapf(err, "Set offer %v", offer) - } - - answer, err := apiRtcRequest(ctx, "/rtc/v1/play", r, offer.SDP) - if err != nil { - return errors.Wrapf(err, "Api request offer=%v", offer.SDP) - } - - if err := pc.SetRemoteDescription(webrtc.SessionDescription{ - Type: webrtc.SDPTypeAnswer, SDP: answer, - }); err != nil { - return errors.Wrapf(err, "Set answer %v", answer) - } - - handleTrack := func(ctx context.Context, track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) error { - // Send a PLI on an interval so that the publisher is pushing a keyframe - go func() { - if track.Kind() == webrtc.RTPCodecTypeAudio { - return - } - - for { - select { - case <-ctx.Done(): - return - case <-time.After(time.Duration(*srsPlayPLI) * time.Millisecond): - _ = pc.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ - MediaSSRC: uint32(track.SSRC()), - }}) - } - } - }() - - // Try to read packets of track. - for i := 0; i < *srsPlayOKPackets && ctx.Err() == nil; i++ { - _, _, err := track.ReadRTP() - if err != nil { - return errors.Wrapf(err, "Read RTP") - } - } - - // Completed. - cancel() - - return nil - } - - pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { - err = handleTrack(ctx, track, receiver) - if err != nil { - codec := track.Codec() - err = errors.Wrapf(err, "Handle track %v, pt=%v", codec.MimeType, codec.PayloadType) - cancel() - } - }) - - pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { - if state == webrtc.ICEConnectionStateFailed || state == webrtc.ICEConnectionStateClosed { - err = errors.Errorf("Close for ICE state %v", state) - cancel() - } - }) - - <-ctx.Done() - return err - } - - startPublish := func(ctx context.Context) error { - sourceVideo := *srsPublishVideo - sourceAudio := *srsPublishAudio - fps := *srsPublishVideoFps - - logger.Tf(ctx, "Start publish url=%v, audio=%v, video=%v, fps=%v", - r, sourceAudio, sourceVideo, fps) - - pc, err := webrtc.NewPeerConnection(webrtc.Configuration{}) - if err != nil { - return errors.Wrapf(err, "Create PC") - } - defer pc.Close() - - var sVideoTrack *rtc.TrackLocalStaticSample - var sVideoSender *webrtc.RTPSender - if sourceVideo != "" { - mimeType, trackID := "video/H264", "video" - if strings.HasSuffix(sourceVideo, ".ivf") { - mimeType = "video/VP8" - } - - sVideoTrack, err = rtc.NewTrackLocalStaticSample( - webrtc.RTPCodecCapability{MimeType: mimeType, ClockRate: 90000}, trackID, "pion", - ) - if err != nil { - return errors.Wrapf(err, "Create video track") - } - - sVideoSender, err = pc.AddTrack(sVideoTrack) - if err != nil { - return errors.Wrapf(err, "Add video track") - } - sVideoSender.Stop() - } - - var sAudioTrack *rtc.TrackLocalStaticSample - var sAudioSender *webrtc.RTPSender - if sourceAudio != "" { - mimeType, trackID := "audio/opus", "audio" - sAudioTrack, err = rtc.NewTrackLocalStaticSample( - webrtc.RTPCodecCapability{MimeType: mimeType, ClockRate: 48000, Channels: 2}, trackID, "pion", - ) - if err != nil { - return errors.Wrapf(err, "Create audio track") - } - - sAudioSender, err = pc.AddTrack(sAudioTrack) - if err != nil { - return errors.Wrapf(err, "Add audio track") - } - defer sAudioSender.Stop() - } - - offer, err := pc.CreateOffer(nil) - if err != nil { - return errors.Wrapf(err, "Create Offer") - } - - if err := pc.SetLocalDescription(offer); err != nil { - return errors.Wrapf(err, "Set offer %v", offer) - } - - answer, err := apiRtcRequest(ctx, "/rtc/v1/publish", r, offer.SDP) - if err != nil { - return errors.Wrapf(err, "Api request offer=%v", offer.SDP) - } - - if err := pc.SetRemoteDescription(webrtc.SessionDescription{ - Type: webrtc.SDPTypeAnswer, SDP: answer, - }); err != nil { - return errors.Wrapf(err, "Set answer %v", answer) - } - - logger.Tf(ctx, "State signaling=%v, ice=%v, conn=%v", pc.SignalingState(), pc.ICEConnectionState(), pc.ConnectionState()) - - ctx, cancel := context.WithCancel(ctx) - pcDone, pcDoneCancel := context.WithCancel(context.Background()) - pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { - logger.Tf(ctx, "PC state %v", state) - - if state == webrtc.PeerConnectionStateConnected { - pcDoneCancel() - publishReadyCancel() - } - - if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateClosed { - err = errors.Errorf("Close for PC state %v", state) - cancel() - } - }) - - // Wait for event from context or tracks. - var wg sync.WaitGroup - - wg.Add(1) - go func() { - defer wg.Done() - - if sAudioSender == nil { - return - } - - select { - case <-ctx.Done(): - case <-pcDone.Done(): - } - - buf := make([]byte, 1500) - for ctx.Err() == nil { - if _, _, err := sAudioSender.Read(buf); err != nil { - return - } - } - }() - - wg.Add(1) - go func() { - defer wg.Done() - - if sAudioTrack == nil { - return - } - - select { - case <-ctx.Done(): - case <-pcDone.Done(): - } - - for ctx.Err() == nil { - if err := readAudioTrackFromDisk(ctx, sourceAudio, sAudioSender, sAudioTrack); err != nil { - if errors.Cause(err) == io.EOF { - logger.Tf(ctx, "EOF, restart ingest audio %v", sourceAudio) - continue - } - logger.Wf(ctx, "Ignore audio err %+v", err) - } - } - }() - - wg.Add(1) - go func() { - defer wg.Done() - - if sVideoSender == nil { - return - } - - select { - case <-ctx.Done(): - case <-pcDone.Done(): - logger.Tf(ctx, "PC(ICE+DTLS+SRTP) done, start read video packets") - } - - buf := make([]byte, 1500) - for ctx.Err() == nil { - if _, _, err := sVideoSender.Read(buf); err != nil { - return - } - } - }() - - wg.Add(1) - go func() { - defer wg.Done() - - if sVideoTrack == nil { - return - } - - select { - case <-ctx.Done(): - case <-pcDone.Done(): - logger.Tf(ctx, "PC(ICE+DTLS+SRTP) done, start ingest video %v", sourceVideo) - } - - for ctx.Err() == nil { - if err := readVideoTrackFromDisk(ctx, sourceVideo, sVideoSender, fps, sVideoTrack); err != nil { - if errors.Cause(err) == io.EOF { - logger.Tf(ctx, "EOF, restart ingest video %v", sourceVideo) - continue - } - logger.Wf(ctx, "Ignore video err %+v", err) - } - } - }() - - wg.Wait() - return err - } + }(ctx) var wg sync.WaitGroup - errs := make(chan error, 0) + defer wg.Wait() + // The event notify. + var thePublisher *TestPublisher + var thePlayer *TestPlayer + mainReady, mainReadyCancel := context.WithCancel(context.Background()) + publishReady, publishReadyCancel := context.WithCancel(context.Background()) + + // Objects init. wg.Add(1) go func() { defer wg.Done() + defer cancel() + + doInit := func() error { + playOK := *srsPlayOKPackets + vnetClientIP := *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("basic-publish-play-%v-%v", os.Getpid(), rand.Int()) + play := NewTestPlayer(api, func(play *TestPlayer) { + play.streamSuffix = streamSuffix + }) + defer play.Close() + + pub := NewTestPublisher(api, func(pub *TestPublisher) { + pub.streamSuffix = streamSuffix + pub.iceReadyCancel = publishReadyCancel + }) + defer pub.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nnWriteRTP, nnReadRTP, nnWriteRTCP, nnReadRTCP int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + nn, attr, err := i.nextRTPReader.Read(buf, attributes) + nnReadRTP++ + return nn, attr, err + } + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + nn, err := i.nextRTPWriter.Write(header, payload, attributes) + + nnWriteRTP++ + logger.Tf(ctx, "publish rtp=(read:%v write:%v), rtcp=(read:%v write:%v) packets", + nnReadRTP, nnWriteRTP, nnReadRTCP, nnWriteRTCP) + return nn, err + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + nn, attr, err := i.nextRTCPReader.Read(buf, attributes) + nnReadRTCP++ + return nn, attr, err + } + i.rtcpWriter = func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { + nn, err := i.nextRTCPWriter.Write(pkts, attributes) + nnWriteRTCP++ + return nn, err + } + })) + }, func(api *TestWebRTCAPI) { + var nn uint64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpReader = func(payload []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nn++; nn >= uint64(playOK) { + cancel() // Completed. + } + logger.Tf(ctx, "play got %v packets", nn) + return i.nextRTPReader.Read(payload, attributes) + } + })) + }); err != nil { + return err + } + + // Set the available objects. + mainReadyCancel() + thePublisher = pub + thePlayer = play + + <-ctx.Done() + return nil + } + + if err := doInit(); err != nil { + r1 = err + } + }() + + // Run publisher. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + select { + case <-ctx.Done(): + return + case <-mainReady.Done(): + } + + doPublish := func() error { + if err := thePublisher.Run(logger.WithContext(ctx), cancel); err != nil { + return err + } + + logger.Tf(ctx, "pub done") + return nil + } + if err := doPublish(); err != nil { + r2 = err + } + }() + + // Run player. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + select { + case <-ctx.Done(): + return + case <-mainReady.Done(): + } - // Wait for publisher to start first. select { case <-ctx.Done(): return case <-publishReady.Done(): } - errs <- startPlay(logger.WithContext(ctx)) - cancel() - }() - - wg.Add(1) - go func() { - defer wg.Done() - - errs <- startPublish(logger.WithContext(ctx)) - cancel() - }() - - wg.Add(1) - go func() { - defer wg.Done() - - select { - case <-ctx.Done(): - case <-time.After(time.Duration(*srsTimeout) * time.Millisecond): - errs <- errors.Errorf("timeout for %vms", *srsTimeout) - cancel() - } - }() - - testDone, testDoneCancel := context.WithCancel(context.Background()) - go func() { - wg.Wait() - testDoneCancel() - }() - - // Handle errs, the test result. - for { - select { - case <-testDone.Done(): - return - case err := <-errs: - if err != nil && err != context.Canceled && !t.Failed() { - t.Errorf("err %+v", err) + doPlay := func() error { + if err := thePlayer.Run(logger.WithContext(ctx), cancel); err != nil { + return err } + + logger.Tf(ctx, "play done") + return nil } + if err := doPlay(); err != nil { + r3 = err + } + + }() +} + +// The srs-server is DTLS server(passive), srs-bench is DTLS client which is active mode. +// No.1 srs-bench: ClientHello +// No.2 srs-server: ServerHello, Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// No.3 srs-bench: Certificate, ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// No.4 srs-server: ChangeCipherSpec, Finished +func TestRtcDTLS_ClientActive_Default(t *testing.T) { + if err := filterTestError(func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupActive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed { + return true + } + logger.Tf(ctx, "Chunk %v, ok=%v %v bytes", chunk, ok, len(c.UserData())) + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }()); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client(client), srs-bench is DTLS server which is passive mode. +// No.1 srs-server: ClientHello +// No.2 srs-bench: ServerHello, Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// No.3 srs-server: Certificate, ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// No.4 srs-bench: ChangeCipherSpec, Finished +func TestRtcDTLS_ClientPassive_Default(t *testing.T) { + if err := filterTestError(func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-active-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed { + return true + } + logger.Tf(ctx, "Chunk %v, ok=%v %v bytes", chunk, ok, len(c.UserData())) + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }()); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS server, srs-bench is DTLS client which is active mode. +// When srs-bench close the PC, it will send DTLS alert and might retransmit it. +func TestRtcDTLS_ClientActive_Duplicated_Alert(t *testing.T) { + if err := filterTestError(func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-active-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupActive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || chunk.chunk != ChunkTypeDTLS { + return true + } + + // Copy the alert to server, ignore error. + if chunk.content == DTLSContentTypeAlert { + _, _ = api.proxy.Deliver(c.SourceAddr(), c.DestinationAddr(), c.UserData()) + _, _ = api.proxy.Deliver(c.SourceAddr(), c.DestinationAddr(), c.UserData()) + } + + logger.Tf(ctx, "Chunk %v, ok=%v %v bytes", chunk, ok, len(c.UserData())) + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }()); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client, srs-bench is DTLS server which is passive mode. +// When srs-bench close the PC, it will send DTLS alert and might retransmit it. +func TestRtcDTLS_ClientPassive_Duplicated_Alert(t *testing.T) { + if err := filterTestError(func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-active-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || chunk.chunk != ChunkTypeDTLS { + return true + } + + // Copy the alert to server, ignore error. + if chunk.content == DTLSContentTypeAlert { + _, _ = api.proxy.Deliver(c.SourceAddr(), c.DestinationAddr(), c.UserData()) + _, _ = api.proxy.Deliver(c.SourceAddr(), c.DestinationAddr(), c.UserData()) + } + + logger.Tf(ctx, "Chunk %v, ok=%v %v bytes", chunk, ok, len(c.UserData())) + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }()); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS server, srs-bench is DTLS client which is active mode. +// [Drop] No.1 srs-bench: ClientHello(Epoch=0, Sequence=0) +// [ARQ] No.2 srs-bench: ClientHello(Epoch=0, Sequence=1) +// No.3 srs-server: ServerHello, Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// No.4 srs-bench: Certificate, ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// No.5 srs-server: ChangeCipherSpec, Finished +// +// @remark The pion is active, so it can be consider a benchmark for DTLS server. +func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ClientHello(t *testing.T) { + var r0 error + err := func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-active-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupActive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnClientHello, nnMaxDrop := 0, 1 + var lastClientHello *DTLSRecord + + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || chunk.chunk != ChunkTypeDTLS || chunk.content != DTLSContentTypeHandshake || chunk.handshake != DTLSHandshakeTypeClientHello { + return true + } + + record, err := NewDTLSRecord(c.UserData()) + if err != nil { + return true + } + + if lastClientHello != nil && record.Equals(lastClientHello) { + r0 = errors.Errorf("dup record %v", record) + } + lastClientHello = record + + nnClientHello++ + ok = (nnClientHello > nnMaxDrop) + logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnClientHello, chunk, record, ok, len(c.UserData())) + return + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }() + if err := filterTestError(err, r0); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client, srs-bench is DTLS server which is passive mode. +// [Drop] No.1 srs-server: ClientHello(Epoch=0, Sequence=0) +// [ARQ] No.2 srs-server: ClientHello(Epoch=0, Sequence=1) +// No.3 srs-bench: ServerHello, Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// No.4 srs-server: Certificate, ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// No.5 srs-bench: ChangeCipherSpec, Finished +// +// @remark If retransmit the ClientHello, with the same epoch+sequence, peer will request HelloVerifyRequest, then +// openssl will create a new ClientHello with increased sequence. It's ok, but waste a lots of duplicated ClientHello +// packets, so we fail the test, requires the epoch+sequence never dup, even for ARQ. +func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ClientHello(t *testing.T) { + var r0 error + err := func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnClientHello, nnMaxDrop := 0, 1 + var lastClientHello *DTLSRecord + + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || chunk.chunk != ChunkTypeDTLS || chunk.content != DTLSContentTypeHandshake || chunk.handshake != DTLSHandshakeTypeClientHello { + return true + } + + record, err := NewDTLSRecord(c.UserData()) + if err != nil { + return true + } + + if lastClientHello != nil && record.Equals(lastClientHello) { + r0 = errors.Errorf("dup record %v", record) + } + lastClientHello = record + + nnClientHello++ + ok = (nnClientHello > nnMaxDrop) + logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnClientHello, chunk, record, ok, len(c.UserData())) + return + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }() + if err := filterTestError(err, r0); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS server, srs-bench is DTLS client which is active mode. +// No.1 srs-bench: ClientHello(Epoch=0, Sequence=0) +// [Drop] No.2 srs-server: ServerHello(Epoch=0, Sequence=0), Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// [ARQ] No.2 srs-bench: ClientHello(Epoch=0, Sequence=1) +// [ARQ] No.3 srs-server: ServerHello(Epoch=0, Sequence=5), Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// No.4 srs-bench: Certificate, ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// No.5 srs-server: ChangeCipherSpec, Finished +// +// @remark The pion is active, so it can be consider a benchmark for DTLS server. +func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ServerHello(t *testing.T) { + var r0, r1 error + err := func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-active-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupActive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnServerHello, nnMaxDrop := 0, 1 + var lastClientHello, lastServerHello *DTLSRecord + + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || chunk.chunk != ChunkTypeDTLS || chunk.content != DTLSContentTypeHandshake || + (chunk.handshake != DTLSHandshakeTypeClientHello && chunk.handshake != DTLSHandshakeTypeServerHello) { + return true + } + + record, err := NewDTLSRecord(c.UserData()) + if err != nil { + return true + } + + if chunk.handshake == DTLSHandshakeTypeClientHello { + if lastClientHello != nil && record.Equals(lastClientHello) { + r0 = errors.Errorf("dup record %v", record) + } + lastClientHello = record + return true + } + + if lastServerHello != nil && record.Equals(lastServerHello) { + r1 = errors.Errorf("dup record %v", record) + } + lastServerHello = record + + nnServerHello++ + ok = (nnServerHello > nnMaxDrop) + logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnServerHello, chunk, record, ok, len(c.UserData())) + return + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }() + if err := filterTestError(err, r0, r1); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client, srs-bench is DTLS server which is passive mode. +// No.1 srs-server: ClientHello(Epoch=0, Sequence=0) +// [Drop] No.2 srs-bench: ServerHello(Epoch=0, Sequence=0), Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// [ARQ] No.2 srs-server: ClientHello(Epoch=0, Sequence=1) +// [ARQ] No.3 srs-bench: ServerHello(Epoch=0, Sequence=5), Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// No.4 srs-server: Certificate, ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// No.5 srs-bench: ChangeCipherSpec, Finished +// +// @remark If retransmit the ClientHello, with the same epoch+sequence, peer will request HelloVerifyRequest, then +// openssl will create a new ClientHello with increased sequence. It's ok, but waste a lots of duplicated ClientHello +// packets, so we fail the test, requires the epoch+sequence never dup, even for ARQ. +func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ServerHello(t *testing.T) { + var r0, r1 error + err := func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnServerHello, nnMaxDrop := 0, 1 + var lastClientHello, lastServerHello *DTLSRecord + + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || chunk.chunk != ChunkTypeDTLS || chunk.content != DTLSContentTypeHandshake || + (chunk.handshake != DTLSHandshakeTypeClientHello && chunk.handshake != DTLSHandshakeTypeServerHello) { + return true + } + + record, err := NewDTLSRecord(c.UserData()) + if err != nil { + return true + } + + if chunk.handshake == DTLSHandshakeTypeClientHello { + if lastClientHello != nil && record.Equals(lastClientHello) { + r0 = errors.Errorf("dup record %v", record) + } + lastClientHello = record + return true + } + + if lastServerHello != nil && record.Equals(lastServerHello) { + r1 = errors.Errorf("dup record %v", record) + } + lastServerHello = record + + nnServerHello++ + ok = (nnServerHello > nnMaxDrop) + logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnServerHello, chunk, record, ok, len(c.UserData())) + return + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }() + if err := filterTestError(err, r0, r1); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS server, srs-bench is DTLS client which is active mode. +// No.1 srs-bench: ClientHello +// No.2 srs-server: ServerHello, Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// [Drop] No.3 srs-bench: Certificate(Epoch=0, Sequence=0), ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// [ARQ] No.4 srs-bench: Certificate(Epoch=0, Sequence=5), ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// No.5 srs-server: ChangeCipherSpec, Finished +// +// @remark The pion is active, so it can be consider a benchmark for DTLS server. +func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_Certificate(t *testing.T) { + var r0 error + err := func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-active-arq-certificate-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupActive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnCertificate, nnMaxDrop := 0, 1 + var lastCertificate *DTLSRecord + + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || chunk.chunk != ChunkTypeDTLS || chunk.content != DTLSContentTypeHandshake || chunk.handshake != DTLSHandshakeTypeCertificate { + return true + } + + record, err := NewDTLSRecord(c.UserData()) + if err != nil { + return true + } + + if lastCertificate != nil && lastCertificate.Equals(record) { + r0 = errors.Errorf("dup record %v", record) + } + lastCertificate = record + + nnCertificate++ + ok = (nnCertificate > nnMaxDrop) + logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnCertificate, chunk, record, ok, len(c.UserData())) + return + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }() + if err := filterTestError(err, r0); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client, srs-bench is DTLS server which is passive mode. +// No.1 srs-server: ClientHello +// No.2 srs-bench: ServerHello, Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// [Drop] No.3 srs-server: Certificate(Epoch=0, Sequence=0), ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// [ARQ] No.4 srs-server: Certificate(Epoch=0, Sequence=5), ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// No.5 srs-bench: ChangeCipherSpec, Finished +// +// @remark If retransmit the Certificate, with the same epoch+sequence, peer will drop the message. It's ok right now, but +// wast some packets, so we check the epoch+sequence which should never dup, even for ARQ. +func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_Certificate(t *testing.T) { + var r0 error + err := func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-arq-certificate-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnCertificate, nnMaxDrop := 0, 1 + var lastCertificate *DTLSRecord + + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || chunk.chunk != ChunkTypeDTLS || chunk.content != DTLSContentTypeHandshake || chunk.handshake != DTLSHandshakeTypeCertificate { + return true + } + + record, err := NewDTLSRecord(c.UserData()) + if err != nil { + return true + } + + if lastCertificate != nil && lastCertificate.Equals(record) { + r0 = errors.Errorf("dup record %v", record) + } + lastCertificate = record + + nnCertificate++ + ok = (nnCertificate > nnMaxDrop) + logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnCertificate, chunk, record, ok, len(c.UserData())) + return + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }() + if err := filterTestError(err, r0); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS server, srs-bench is DTLS client which is active mode. +// No.1 srs-bench: ClientHello +// No.2 srs-server: ServerHello, Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// No.3 srs-bench: Certificate(Epoch=0, Sequence=0), ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// [Drop] No.5 srs-server: ChangeCipherSpec, Finished +// [ARQ] No.6 srs-bench: Certificate(Epoch=0, Sequence=5), ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// [ARQ] No.7 srs-server: ChangeCipherSpec, Finished +// +// @remark The pion is active, so it can be consider a benchmark for DTLS server. +func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *testing.T) { + var r0, r1 error + err := func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-active-arq-certificate-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupActive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnCertificate, nnMaxDrop := 0, 1 + var lastChangeCipherSepc, lastCertifidate *DTLSRecord + + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || (!chunk.IsChangeCipherSpec() && !chunk.IsCertificate()) { + return true + } + + record, err := NewDTLSRecord(c.UserData()) + if err != nil { + return true + } + + if chunk.IsCertificate() { + if lastCertifidate != nil && record.Equals(lastCertifidate) { + r0 = errors.Errorf("dup record %v", record) + } + lastCertifidate = record + return true + } + + if lastChangeCipherSepc != nil && lastChangeCipherSepc.Equals(record) { + r1 = errors.Errorf("dup record %v", record) + } + lastChangeCipherSepc = record + + nnCertificate++ + ok = (nnCertificate > nnMaxDrop) + logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnCertificate, chunk, record, ok, len(c.UserData())) + return + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }() + if err := filterTestError(err, r0, r1); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client, srs-bench is DTLS server which is passive mode. +// No.1 srs-server: ClientHello +// No.2 srs-bench: ServerHello, Certificate, ServerKeyExchange, CertificateRequest, ServerHelloDone +// No.3 srs-server: Certificate(Epoch=0, Sequence=0), ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// [Drop] No.5 srs-bench: ChangeCipherSpec, Finished +// [ARQ] No.6 srs-server: Certificate(Epoch=0, Sequence=5), ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished +// [ARQ] No.7 srs-bench: ChangeCipherSpec, Finished +// +// @remark If retransmit the Certificate, with the same epoch+sequence, peer will drop the message, and never generate the +// ChangeCipherSpec, which will cause DTLS fail. +func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *testing.T) { + var r0, r1 error + err := func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-arq-certificate-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnCertificate, nnMaxDrop := 0, 1 + var lastChangeCipherSepc, lastCertifidate *DTLSRecord + + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed || (!chunk.IsChangeCipherSpec() && !chunk.IsCertificate()) { + return true + } + + record, err := NewDTLSRecord(c.UserData()) + if err != nil { + return true + } + + if chunk.IsCertificate() { + if lastCertifidate != nil && record.Equals(lastCertifidate) { + r0 = errors.Errorf("dup record %v", record) + } + lastCertifidate = record + return true + } + + if lastChangeCipherSepc != nil && lastChangeCipherSepc.Equals(record) { + r1 = errors.Errorf("dup record %v", record) + } + lastChangeCipherSepc = record + + nnCertificate++ + ok = (nnCertificate > nnMaxDrop) + logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnCertificate, chunk, record, ok, len(c.UserData())) + return + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }() + if err := filterTestError(err, r0, r1); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client(client), srs-bench is DTLS server which is passive mode. +// Drop all DTLS packets when got ClientHello, to test the server ARQ thread cleanup. +func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ClientHello(t *testing.T) { + if err := filterTestError(func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + vnetClientIP, dtlsDropPackets := *srsVnetClientIP, *srsDTLSDropPackets + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + nnDrop, dropAll := 0, false + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed { + return true + } + + if chunk.IsHandshake() { + if chunk.IsClientHello() { + dropAll = true + } + + if !dropAll { + return true + } + + if nnDrop++; nnDrop >= dtlsDropPackets { + cancel() // Done, server transmit 5 Client Hello. + } + + logger.Tf(ctx, "N=%v, Drop chunk %v %v bytes", nnDrop, chunk, len(c.UserData())) + return false + } + + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }()); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client(client), srs-bench is DTLS server which is passive mode. +// Drop all DTLS packets when got ServerHello, to test the server ARQ thread cleanup. +func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ServerHello(t *testing.T) { + if err := filterTestError(func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + vnetClientIP, dtlsDropPackets := *srsVnetClientIP, *srsDTLSDropPackets + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + nnDrop, dropAll := 0, false + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed { + return true + } + + if chunk.IsHandshake() { + if chunk.IsServerHello() { + dropAll = true + } + + if !dropAll { + return true + } + + if nnDrop++; nnDrop >= dtlsDropPackets { + cancel() // Done, server transmit 5 Client Hello. + } + + logger.Tf(ctx, "N=%v, Drop chunk %v %v bytes", nnDrop, chunk, len(c.UserData())) + return false + } + + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }()); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client(client), srs-bench is DTLS server which is passive mode. +// Drop all DTLS packets when got Certificate, to test the server ARQ thread cleanup. +func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_Certificate(t *testing.T) { + if err := filterTestError(func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + vnetClientIP, dtlsDropPackets := *srsVnetClientIP, *srsDTLSDropPackets + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + nnDrop, dropAll := 0, false + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed { + return true + } + + if chunk.IsHandshake() { + if chunk.IsCertificate() { + dropAll = true + } + + if !dropAll { + return true + } + + if nnDrop++; nnDrop >= dtlsDropPackets { + cancel() // Done, server transmit 5 Client Hello. + } + + logger.Tf(ctx, "N=%v, Drop chunk %v %v bytes", nnDrop, chunk, len(c.UserData())) + return false + } + + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }()); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client(client), srs-bench is DTLS server which is passive mode. +// Drop all DTLS packets when got ChangeCipherSpec, to test the server ARQ thread cleanup. +func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ChangeCipherSpec(t *testing.T) { + if err := filterTestError(func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + vnetClientIP, dtlsDropPackets := *srsVnetClientIP, *srsDTLSDropPackets + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + nnDrop, dropAll := 0, false + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed { + return true + } + + if chunk.IsHandshake() || chunk.IsChangeCipherSpec() { + if chunk.IsChangeCipherSpec() { + dropAll = true + } + + if !dropAll { + return true + } + + if nnDrop++; nnDrop >= dtlsDropPackets { + cancel() // Done, server transmit 5 Client Hello. + } + + logger.Tf(ctx, "N=%v, Drop chunk %v %v bytes", nnDrop, chunk, len(c.UserData())) + return false + } + + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }()); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client(client), srs-bench is DTLS server which is passive mode. +// For very bad network, we drop 4 ClientHello consume about 750ms, then drop 4 Certificate +// which also consume about 750ms, but finally should be done successfully. +func TestRtcDTLS_ClientPassive_ARQ_VeryBadNetwork(t *testing.T) { + if err := filterTestError(func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP, dtlsDropPackets := *srsPublishOKPackets, *srsVnetClientIP, *srsDTLSDropPackets + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnDropClientHello, nnDropCertificate := 0, 0 + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed { + return true + } + + if chunk.IsHandshake() { + if !chunk.IsClientHello() && !chunk.IsCertificate() { + return true + } + + if chunk.IsClientHello() { + if nnDropClientHello >= 4 { + return true + } + nnDropClientHello++ + } + + if chunk.IsCertificate() { + if nnDropCertificate >= dtlsDropPackets { + return true + } + nnDropCertificate++ + } + + logger.Tf(ctx, "N=%v/%v, Drop chunk %v %v bytes", nnDropClientHello, nnDropCertificate, chunk, len(c.UserData())) + return false + } + + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }()); err != nil { + t.Errorf("err %+v", err) + } +} + +// The srs-server is DTLS client(client), srs-bench is DTLS server which is passive mode. +// If we retransmit 2 ClientHello packets, consumed 150ms, server might wait at 200ms. +// Then we retransmit the Certificate, server reset the timer and retransmit it in 50ms, not 200ms. +func TestRtcDTLS_ClientPassive_ARQ_Certificate_After_ClientHello(t *testing.T) { + var r0 error + err := func() error { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP + + // Create top level test object. + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + defer api.Close() + + streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) + p := NewTestPublisher(api, func(p *TestPublisher) { + p.streamSuffix = streamSuffix + p.onOffer = testUtilSetupPassive + }) + defer p.Close() + + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + var nn int64 + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if nn++; nn >= int64(publishOK) { + cancel() // Send enough packets, done. + } + logger.Tf(ctx, "publish write %v packets", nn) + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + }, func(api *TestWebRTCAPI) { + nnDropClientHello, nnDropCertificate := 0, 0 + var firstCertificate time.Time + api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { + chunk, parsed := NewChunkMessageType(c) + if !parsed { + return true + } + + if chunk.IsHandshake() { + if !chunk.IsClientHello() && !chunk.IsCertificate() { + return true + } + + if chunk.IsClientHello() { + if nnDropClientHello > 3 { + return true + } + nnDropClientHello++ + } + + if chunk.IsCertificate() { + if nnDropCertificate == 0 { + firstCertificate = time.Now() + } else if nnDropCertificate == 1 { + if duration := time.Now().Sub(firstCertificate); duration > 150*time.Millisecond { + r0 = fmt.Errorf("ARQ between ClientHello and Certificate too large %v", duration) + } else { + logger.Tf(ctx, "ARQ between ClientHello and Certificate is %v", duration) + } + cancel() + } + nnDropCertificate++ + } + + logger.Tf(ctx, "N=%v/%v, Drop chunk %v %v bytes", nnDropClientHello, nnDropCertificate, chunk, len(c.UserData())) + return false + } + + return true + }) + }); err != nil { + return err + } + + return p.Run(ctx, cancel) + }() + if err := filterTestError(err, r0); err != nil { + t.Errorf("err %+v", err) } } diff --git a/trunk/3rdparty/srs-bench/srs/stat.go b/trunk/3rdparty/srs-bench/srs/stat.go index 35cbe3737..3ca7ed79a 100644 --- a/trunk/3rdparty/srs-bench/srs/stat.go +++ b/trunk/3rdparty/srs-bench/srs/stat.go @@ -1,3 +1,23 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package srs import ( diff --git a/trunk/3rdparty/srs-bench/srs/util.go b/trunk/3rdparty/srs-bench/srs/util.go index 4e6b2f783..8c5a5434d 100644 --- a/trunk/3rdparty/srs-bench/srs/util.go +++ b/trunk/3rdparty/srs-bench/srs/util.go @@ -1,3 +1,23 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package srs import ( @@ -7,10 +27,14 @@ import ( "fmt" "github.com/ossrs/go-oryx-lib/errors" "github.com/ossrs/go-oryx-lib/logger" + "github.com/pion/transport/vnet" + "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/pkg/media/h264reader" "io/ioutil" + "net" "net/http" "net/url" + "strconv" "strings" "time" ) @@ -140,3 +164,305 @@ func (v *wallClock) Tick(d time.Duration) time.Duration { } return 0 } + +// Set to active, as DTLS client, to start ClientHello. +func testUtilSetupActive(s *webrtc.SessionDescription) error { + if strings.Contains(s.SDP, "setup:passive") { + return errors.New("set to active") + } + + s.SDP = strings.ReplaceAll(s.SDP, "setup:actpass", "setup:active") + return nil +} + +// Set to passive, as DTLS client, to start ClientHello. +func testUtilSetupPassive(s *webrtc.SessionDescription) error { + if strings.Contains(s.SDP, "setup:active") { + return errors.New("set to passive") + } + + s.SDP = strings.ReplaceAll(s.SDP, "setup:actpass", "setup:passive") + return nil +} + +// Parse address from SDP. +// candidate:0 1 udp 2130706431 192.168.3.8 8000 typ host generation 0 +func parseAddressOfCandidate(answerSDP string) (*net.UDPAddr, error) { + answer := webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: answerSDP} + answerObject, err := answer.Unmarshal() + if err != nil { + return nil, errors.Wrapf(err, "unmarshal answer %v", answerSDP) + } + + if len(answerObject.MediaDescriptions) == 0 { + return nil, errors.New("no media") + } + + candidate, ok := answerObject.MediaDescriptions[0].Attribute("candidate") + if !ok { + return nil, errors.New("no candidate") + } + + // candidate:0 1 udp 2130706431 192.168.3.8 8000 typ host generation 0 + attrs := strings.Split(candidate, " ") + if len(attrs) <= 6 { + return nil, errors.Errorf("no address in %v", candidate) + } + + // Parse ip and port from answer. + ip := attrs[4] + port, err := strconv.Atoi(attrs[5]) + if err != nil { + return nil, errors.Wrapf(err, "invalid port %v", candidate) + } + + address := fmt.Sprintf("%v:%v", ip, port) + addr, err := net.ResolveUDPAddr("udp4", address) + if err != nil { + return nil, errors.Wrapf(err, "parse %v", address) + } + + return addr, nil +} + +// Filter the test error, ignore context.Canceled +func filterTestError(errs ...error) error { + var filteredErrors []error + + for _, err := range errs { + if err == nil || errors.Cause(err) == context.Canceled { + continue + } + filteredErrors = append(filteredErrors, err) + } + + if len(filteredErrors) == 0 { + return nil + } + if len(filteredErrors) == 1 { + return filteredErrors[0] + } + + var descs []string + for i, err := range filteredErrors[1:] { + descs = append(descs, fmt.Sprintf("err #%d, %+v", i, err)) + } + return errors.Wrapf(filteredErrors[0], "with %v", strings.Join(descs, ",")) +} + +// For STUN packet, 0x00 is binding request, 0x01 is binding success response. +// @see srs_is_stun of https://github.com/ossrs/srs +func srsIsStun(b []byte) bool { + return len(b) > 0 && (b[0] == 0 || b[0] == 1) +} + +// change_cipher_spec(20), alert(21), handshake(22), application_data(23) +// @see https://tools.ietf.org/html/rfc2246#section-6.2.1 +// @see srs_is_dtls of https://github.com/ossrs/srs +func srsIsDTLS(b []byte) bool { + return (len(b) >= 13 && (b[0] > 19 && b[0] < 64)) +} + +// For RTP or RTCP, the V=2 which is in the high 2bits, 0xC0 (1100 0000) +// @see srs_is_rtp_or_rtcp of https://github.com/ossrs/srs +func srsIsRTPOrRTCP(b []byte) bool { + return (len(b) >= 12 && (b[0]&0xC0) == 0x80) +} + +// For RTCP, PT is [128, 223] (or without marker [0, 95]). +// Literally, RTCP starts from 64 not 0, so PT is [192, 223] (or without marker [64, 95]). +// @note For RTP, the PT is [96, 127], or [224, 255] with marker. +// @see srs_is_rtcp of https://github.com/ossrs/srs +func srsIsRTCP(b []byte) bool { + return (len(b) >= 12) && (b[0]&0x80) != 0 && (b[1] >= 192 && b[1] <= 223) +} + +type ChunkType int + +const ( + ChunkTypeICE ChunkType = iota + 1 + ChunkTypeDTLS + ChunkTypeRTP + ChunkTypeRTCP +) + +func (v ChunkType) String() string { + switch v { + case ChunkTypeICE: + return "ICE" + case ChunkTypeDTLS: + return "DTLS" + case ChunkTypeRTP: + return "RTP" + case ChunkTypeRTCP: + return "RTCP" + default: + return "Unknown" + } +} + +type DTLSContentType int + +const ( + DTLSContentTypeHandshake DTLSContentType = 22 + DTLSContentTypeChangeCipherSpec DTLSContentType = 20 + DTLSContentTypeAlert DTLSContentType = 21 +) + +func (v DTLSContentType) String() string { + switch v { + case DTLSContentTypeHandshake: + return "Handshake" + case DTLSContentTypeChangeCipherSpec: + return "ChangeCipherSpec" + default: + return "Unknown" + } +} + +type DTLSHandshakeType int + +const ( + DTLSHandshakeTypeClientHello DTLSHandshakeType = 1 + DTLSHandshakeTypeServerHello DTLSHandshakeType = 2 + DTLSHandshakeTypeCertificate DTLSHandshakeType = 11 + DTLSHandshakeTypeServerKeyExchange DTLSHandshakeType = 12 + DTLSHandshakeTypeCertificateRequest DTLSHandshakeType = 13 + DTLSHandshakeTypeServerDone DTLSHandshakeType = 14 + DTLSHandshakeTypeCertificateVerify DTLSHandshakeType = 15 + DTLSHandshakeTypeClientKeyExchange DTLSHandshakeType = 16 + DTLSHandshakeTypeFinished DTLSHandshakeType = 20 +) + +func (v DTLSHandshakeType) String() string { + switch v { + case DTLSHandshakeTypeClientHello: + return "ClientHello" + case DTLSHandshakeTypeServerHello: + return "ServerHello" + case DTLSHandshakeTypeCertificate: + return "Certificate" + case DTLSHandshakeTypeServerKeyExchange: + return "ServerKeyExchange" + case DTLSHandshakeTypeCertificateRequest: + return "CertificateRequest" + case DTLSHandshakeTypeServerDone: + return "ServerDone" + case DTLSHandshakeTypeCertificateVerify: + return "CertificateVerify" + case DTLSHandshakeTypeClientKeyExchange: + return "ClientKeyExchange" + case DTLSHandshakeTypeFinished: + return "Finished" + default: + return "Unknown" + } +} + +type ChunkMessageType struct { + chunk ChunkType + content DTLSContentType + handshake DTLSHandshakeType +} + +func (v *ChunkMessageType) String() string { + if v.chunk == ChunkTypeDTLS { + return fmt.Sprintf("%v-%v-%v", v.chunk, v.content, v.handshake) + } + return fmt.Sprintf("%v", v.chunk) +} + +func NewChunkMessageType(c vnet.Chunk) (*ChunkMessageType, bool) { + b := c.UserData() + + if len(b) == 0 { + return nil, false + } + + v := &ChunkMessageType{} + + if srsIsRTPOrRTCP(b) { + if srsIsRTCP(b) { + v.chunk = ChunkTypeRTCP + } else { + v.chunk = ChunkTypeRTP + } + return v, true + } + + if srsIsStun(b) { + v.chunk = ChunkTypeICE + return v, true + } + + if !srsIsDTLS(b) { + return nil, false + } + + v.chunk, v.content = ChunkTypeDTLS, DTLSContentType(b[0]) + if v.content != DTLSContentTypeHandshake { + return v, true + } + + if len(b) < 14 { + return v, false + } + v.handshake = DTLSHandshakeType(b[13]) + return v, true +} + +func (v *ChunkMessageType) IsHandshake() bool { + return v.chunk == ChunkTypeDTLS && v.content == DTLSContentTypeHandshake +} + +func (v *ChunkMessageType) IsClientHello() bool { + return v.chunk == ChunkTypeDTLS && v.content == DTLSContentTypeHandshake && v.handshake == DTLSHandshakeTypeClientHello +} + +func (v *ChunkMessageType) IsServerHello() bool { + return v.chunk == ChunkTypeDTLS && v.content == DTLSContentTypeHandshake && v.handshake == DTLSHandshakeTypeServerHello +} + +func (v *ChunkMessageType) IsCertificate() bool { + return v.chunk == ChunkTypeDTLS && v.content == DTLSContentTypeHandshake && v.handshake == DTLSHandshakeTypeCertificate +} + +func (v *ChunkMessageType) IsChangeCipherSpec() bool { + return v.chunk == ChunkTypeDTLS && v.content == DTLSContentTypeChangeCipherSpec +} + +type DTLSRecord struct { + ContentType DTLSContentType + Version uint16 + Epoch uint16 + SequenceNumber uint64 + Length uint16 + Data []byte +} + +func NewDTLSRecord(b []byte) (*DTLSRecord, error) { + v := &DTLSRecord{} + return v, v.Unmarshal(b) +} + +func (v *DTLSRecord) String() string { + return fmt.Sprintf("epoch=%v, sequence=%v", v.Epoch, v.SequenceNumber) +} + +func (v *DTLSRecord) Equals(p *DTLSRecord) bool { + return v.Epoch == p.Epoch && v.SequenceNumber == p.SequenceNumber +} + +func (v *DTLSRecord) Unmarshal(b []byte) error { + if len(b) < 13 { + return errors.Errorf("requires 13B only %v", len(b)) + } + + v.ContentType = DTLSContentType(uint8(b[0])) + v.Version = uint16(b[1])<<8 | uint16(b[2]) + v.Epoch = uint16(b[3])<<8 | uint16(b[4]) + v.SequenceNumber = uint64(b[5])<<40 | uint64(b[6])<<32 | uint64(b[7])<<24 | uint64(b[8])<<16 | uint64(b[9])<<8 | uint64(b[10]) + v.Length = uint16(b[11])<<8 | uint16(b[12]) + v.Data = b[13:] + return nil +} diff --git a/trunk/3rdparty/srs-bench/vnet/example_udpproxy_test.go b/trunk/3rdparty/srs-bench/vnet/example_udpproxy_test.go new file mode 100644 index 000000000..d54e71653 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vnet/example_udpproxy_test.go @@ -0,0 +1,278 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package vnet_test + +import ( + "net" + + vnet_proxy "github.com/ossrs/srs-bench/vnet" + "github.com/pion/logging" + "github.com/pion/transport/vnet" +) + +// Proxy many vnet endpoint to one real server endpoint. +// For example: +// vnet(10.0.0.11:5787) => proxy => 192.168.1.10:8000 +// vnet(10.0.0.11:5788) => proxy => 192.168.1.10:8000 +// vnet(10.0.0.11:5789) => proxy => 192.168.1.10:8000 +func ExampleUDPProxyManyToOne() { // nolint:govet + var clientNetwork *vnet.Net + + var serverAddr *net.UDPAddr + if addr, err := net.ResolveUDPAddr("udp4", "192.168.1.10:8000"); err != nil { + // handle error + } else { + serverAddr = addr + } + + // Setup the network and proxy. + if true { + // Create vnet WAN with one endpoint, please read from + // https://github.com/pion/transport/tree/master/vnet#example-wan-with-one-endpoint-vnet + router, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + if err != nil { + // handle error + } + + // Create a network and add to router, for example, for client. + clientNetwork = vnet.NewNet(&vnet.NetConfig{ + StaticIP: "10.0.0.11", + }) + if err = router.AddNet(clientNetwork); err != nil { + // handle error + } + + // Start the router. + if err = router.Start(); err != nil { + // handle error + } + defer router.Stop() // nolint:errcheck + + // Create a proxy, bind to the router. + proxy, err := vnet_proxy.NewProxy(router) + if err != nil { + // handle error + } + defer proxy.Close() // nolint:errcheck + + // Start to proxy some addresses, clientNetwork is a hit for proxy, + // that the client in vnet is from this network. + if err := proxy.Proxy(clientNetwork, serverAddr); err != nil { + // handle error + } + } + + // Now, all packets from client, will be proxy to real server, vice versa. + client0, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5787") + if err != nil { + // handle error + } + _, _ = client0.WriteTo([]byte("Hello"), serverAddr) + + client1, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5788") + if err != nil { + // handle error + } + _, _ = client1.WriteTo([]byte("Hello"), serverAddr) + + client2, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5789") + if err != nil { + // handle error + } + _, _ = client2.WriteTo([]byte("Hello"), serverAddr) +} + +// Proxy many vnet endpoint to one real server endpoint. +// For example: +// vnet(10.0.0.11:5787) => proxy => 192.168.1.10:8000 +// vnet(10.0.0.11:5788) => proxy => 192.168.1.10:8000 +func ExampleUDPProxyMultileTimes() { // nolint:govet + var clientNetwork *vnet.Net + + var serverAddr *net.UDPAddr + if addr, err := net.ResolveUDPAddr("udp4", "192.168.1.10:8000"); err != nil { + // handle error + } else { + serverAddr = addr + } + + // Setup the network and proxy. + var proxy *vnet_proxy.UDPProxy + if true { + // Create vnet WAN with one endpoint, please read from + // https://github.com/pion/transport/tree/master/vnet#example-wan-with-one-endpoint-vnet + router, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + if err != nil { + // handle error + } + + // Create a network and add to router, for example, for client. + clientNetwork = vnet.NewNet(&vnet.NetConfig{ + StaticIP: "10.0.0.11", + }) + if err = router.AddNet(clientNetwork); err != nil { + // handle error + } + + // Start the router. + if err = router.Start(); err != nil { + // handle error + } + defer router.Stop() // nolint:errcheck + + // Create a proxy, bind to the router. + proxy, err = vnet_proxy.NewProxy(router) + if err != nil { + // handle error + } + defer proxy.Close() // nolint:errcheck + } + + if true { + // Start to proxy some addresses, clientNetwork is a hit for proxy, + // that the client in vnet is from this network. + if err := proxy.Proxy(clientNetwork, serverAddr); err != nil { + // handle error + } + + // Now, all packets from client, will be proxy to real server, vice versa. + client0, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5787") + if err != nil { + // handle error + } + _, _ = client0.WriteTo([]byte("Hello"), serverAddr) + } + + if true { + // It's ok to proxy multiple times, for example, the publisher and player + // might need to proxy when got answer. + if err := proxy.Proxy(clientNetwork, serverAddr); err != nil { + // handle error + } + + client1, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5788") + if err != nil { + // handle error + } + _, _ = client1.WriteTo([]byte("Hello"), serverAddr) + } +} + +// Proxy one vnet endpoint to one real server endpoint. +// For example: +// vnet(10.0.0.11:5787) => proxy0 => 192.168.1.10:8000 +// vnet(10.0.0.11:5788) => proxy1 => 192.168.1.10:8001 +// vnet(10.0.0.11:5789) => proxy2 => 192.168.1.10:8002 +func ExampleUDPProxyOneToOne() { // nolint:govet + var clientNetwork *vnet.Net + + var serverAddr0 *net.UDPAddr + if addr, err := net.ResolveUDPAddr("udp4", "192.168.1.10:8000"); err != nil { + // handle error + } else { + serverAddr0 = addr + } + + var serverAddr1 *net.UDPAddr + if addr, err := net.ResolveUDPAddr("udp4", "192.168.1.10:8001"); err != nil { + // handle error + } else { + serverAddr1 = addr + } + + var serverAddr2 *net.UDPAddr + if addr, err := net.ResolveUDPAddr("udp4", "192.168.1.10:8002"); err != nil { + // handle error + } else { + serverAddr2 = addr + } + + // Setup the network and proxy. + if true { + // Create vnet WAN with one endpoint, please read from + // https://github.com/pion/transport/tree/master/vnet#example-wan-with-one-endpoint-vnet + router, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + if err != nil { + // handle error + } + + // Create a network and add to router, for example, for client. + clientNetwork = vnet.NewNet(&vnet.NetConfig{ + StaticIP: "10.0.0.11", + }) + if err = router.AddNet(clientNetwork); err != nil { + // handle error + } + + // Start the router. + if err = router.Start(); err != nil { + // handle error + } + defer router.Stop() // nolint:errcheck + + // Create a proxy, bind to the router. + proxy, err := vnet_proxy.NewProxy(router) + if err != nil { + // handle error + } + defer proxy.Close() // nolint:errcheck + + // Start to proxy some addresses, clientNetwork is a hit for proxy, + // that the client in vnet is from this network. + if err := proxy.Proxy(clientNetwork, serverAddr0); err != nil { + // handle error + } + if err := proxy.Proxy(clientNetwork, serverAddr1); err != nil { + // handle error + } + if err := proxy.Proxy(clientNetwork, serverAddr2); err != nil { + // handle error + } + } + + // Now, all packets from client, will be proxy to real server, vice versa. + client0, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5787") + if err != nil { + // handle error + } + _, _ = client0.WriteTo([]byte("Hello"), serverAddr0) + + client1, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5788") + if err != nil { + // handle error + } + _, _ = client1.WriteTo([]byte("Hello"), serverAddr1) + + client2, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5789") + if err != nil { + // handle error + } + _, _ = client2.WriteTo([]byte("Hello"), serverAddr2) +} diff --git a/trunk/3rdparty/srs-bench/vnet/udpproxy.go b/trunk/3rdparty/srs-bench/vnet/udpproxy.go new file mode 100644 index 000000000..c21fe4603 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vnet/udpproxy.go @@ -0,0 +1,222 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package vnet + +import ( + "net" + "sync" + "time" + + "github.com/pion/transport/vnet" +) + +// A UDP proxy between real server(net.UDPConn) and vnet.UDPConn. +// +// High level design: +// .............................................. +// : Virtual Network (vnet) : +// : : +// +-------+ * 1 +----+ +--------+ : +// | :App |------------>|:Net|--o<-----|:Router | ............................. +// +-------+ +----+ | | : UDPProxy : +// : | | +----+ +---------+ +---------+ +--------+ +// : | |--->o--|:Net|-->o-| vnet. |-->o-| net. |--->-| :Real | +// : | | +----+ | UDPConn | | UDPConn | | Server | +// : | | : +---------+ +---------+ +--------+ +// : | | ............................: +// : +--------+ : +// ............................................... +// +// The whole big picture: +// ...................................... +// : Virtual Network (vnet) : +// : : +// +-------+ * 1 +----+ +--------+ : +// | :App |------------>|:Net|--o<-----|:Router | ............................. +// +-------+ +----+ | | : UDPProxy : +// +-----------+ * 1 +----+ | | +----+ +---------+ +---------+ +--------+ +// |:STUNServer|-------->|:Net|--o<-----| |--->o--|:Net|-->o-| vnet. |-->o-| net. |--->-| :Real | +// +-----------+ +----+ | | +----+ | UDPConn | | UDPConn | | Server | +// +-----------+ * 1 +----+ | | : +---------+ +---------+ +--------+ +// |:TURNServer|-------->|:Net|--o<-----| | ............................: +// +-----------+ +----+ [1] | | : +// : 1 | | 1 <> : +// : +---<>| |<>----+ [2] : +// : | +--------+ | : +// To form | *| v 0..1 : +// a subnet tree | o [3] +-----+ : +// : | ^ |:NAT | : +// : | | +-----+ : +// : +-------+ : +// ...................................... +type UDPProxy struct { + // The router bind to. + router *vnet.Router + + // Each vnet source, bind to a real socket to server. + // key is real server addr, which is net.Addr + // value is *aUDPProxyWorker + workers sync.Map + + // For each endpoint, we never know when to start and stop proxy, + // so we stop the endpoint when timeout. + timeout time.Duration + + // For utest, to mock the target real server. + // Optional, use the address of received client packet. + mockRealServerAddr *net.UDPAddr +} + +// NewProxy create a proxy, the router for this proxy belongs/bind to. If need to proxy for +// please create a new proxy for each router. For all addresses we proxy, we will create a +// vnet.Net in this router and proxy all packets. +func NewProxy(router *vnet.Router) (*UDPProxy, error) { + v := &UDPProxy{router: router, timeout: 2 * time.Minute} + return v, nil +} + +// Close the proxy, stop all workers. +func (v *UDPProxy) Close() error { + // nolint:godox // TODO: FIXME: Do cleanup. + return nil +} + +// Proxy starts a worker for server, ignore if already started. +func (v *UDPProxy) Proxy(client *vnet.Net, server *net.UDPAddr) error { + // Note that even if the worker exists, it's also ok to create a same worker, + // because the router will use the last one, and the real server will see a address + // change event after we switch to the next worker. + if _, ok := v.workers.Load(server.String()); ok { + // nolint:godox // TODO: Need to restart the stopped worker? + return nil + } + + // Not exists, create a new one. + worker := &aUDPProxyWorker{ + router: v.router, mockRealServerAddr: v.mockRealServerAddr, + } + v.workers.Store(server.String(), worker) + + return worker.Proxy(client, server) +} + +// A proxy worker for a specified proxy server. +type aUDPProxyWorker struct { + router *vnet.Router + mockRealServerAddr *net.UDPAddr + + // Each vnet source, bind to a real socket to server. + // key is vnet client addr, which is net.Addr + // value is *net.UDPConn + endpoints sync.Map +} + +func (v *aUDPProxyWorker) Proxy(client *vnet.Net, serverAddr *net.UDPAddr) error { // nolint:gocognit + // Create vnet for real server by serverAddr. + nw := vnet.NewNet(&vnet.NetConfig{ + StaticIP: serverAddr.IP.String(), + }) + if err := v.router.AddNet(nw); err != nil { + return err + } + + // We must create a "same" vnet.UDPConn as the net.UDPConn, + // which has the same ip:port, to copy packets between them. + vnetSocket, err := nw.ListenUDP("udp4", serverAddr) + if err != nil { + return err + } + + // Start a proxy goroutine. + var findEndpointBy func(addr net.Addr) (*net.UDPConn, error) + // nolint:godox // TODO: FIXME: Do cleanup. + go func() { + buf := make([]byte, 1500) + + for { + n, addr, err := vnetSocket.ReadFrom(buf) + if err != nil { + return + } + + if n <= 0 || addr == nil { + continue // Drop packet + } + + realSocket, err := findEndpointBy(addr) + if err != nil { + continue // Drop packet. + } + + if _, err := realSocket.Write(buf[:n]); err != nil { + return + } + } + }() + + // Got new vnet client, start a new endpoint. + findEndpointBy = func(addr net.Addr) (*net.UDPConn, error) { + // Exists binding. + if value, ok := v.endpoints.Load(addr.String()); ok { + // Exists endpoint, reuse it. + return value.(*net.UDPConn), nil + } + + // The real server we proxy to, for utest to mock it. + realAddr := serverAddr + if v.mockRealServerAddr != nil { + realAddr = v.mockRealServerAddr + } + + // Got new vnet client, create new endpoint. + realSocket, err := net.DialUDP("udp4", nil, realAddr) + if err != nil { + return nil, err + } + + // Bind address. + v.endpoints.Store(addr.String(), realSocket) + + // Got packet from real serverAddr, we should proxy it to vnet. + // nolint:godox // TODO: FIXME: Do cleanup. + go func(vnetClientAddr net.Addr) { + buf := make([]byte, 1500) + for { + n, _, err := realSocket.ReadFrom(buf) + if err != nil { + return + } + + if n <= 0 { + continue // Drop packet + } + + if _, err := vnetSocket.WriteTo(buf[:n], vnetClientAddr); err != nil { + return + } + } + }(addr) + + return realSocket, nil + } + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vnet/udpproxy_direct.go b/trunk/3rdparty/srs-bench/vnet/udpproxy_direct.go new file mode 100644 index 000000000..6d49494ed --- /dev/null +++ b/trunk/3rdparty/srs-bench/vnet/udpproxy_direct.go @@ -0,0 +1,61 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package vnet + +import ( + "net" +) + +func (v *UDPProxy) Deliver(sourceAddr, destAddr net.Addr, b []byte) (nn int, err error) { + v.workers.Range(func(key, value interface{}) bool { + if nn, err := value.(*aUDPProxyWorker).Deliver(sourceAddr, destAddr, b); err != nil { + return false // Fail, abort. + } else if nn == len(b) { + return false // Done. + } + + return true // Deliver by next worker. + }) + return +} + +func (v *aUDPProxyWorker) Deliver(sourceAddr, destAddr net.Addr, b []byte) (nn int, err error) { + addr, ok := sourceAddr.(*net.UDPAddr) + if !ok { + return 0, nil + } + + // TODO: Support deliver packet from real server to vnet. + // If packet is from vent, proxy to real server. + var realSocket *net.UDPConn + if value, ok := v.endpoints.Load(addr.String()); !ok { + return 0, nil + } else { + realSocket = value.(*net.UDPConn) + } + + // Send to real server. + if _, err := realSocket.Write(b); err != nil { + return 0, err + } + + return len(b), nil +} diff --git a/trunk/3rdparty/srs-bench/vnet/udpproxy_direct_test.go b/trunk/3rdparty/srs-bench/vnet/udpproxy_direct_test.go new file mode 100644 index 000000000..b347c682c --- /dev/null +++ b/trunk/3rdparty/srs-bench/vnet/udpproxy_direct_test.go @@ -0,0 +1,184 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package vnet + +import ( + "context" + "fmt" + "github.com/pion/logging" + "github.com/pion/transport/vnet" + "net" + "sync" + "testing" + "time" +) + +// vnet client: +// 10.0.0.11:5787 +// proxy to real server: +// 192.168.1.10:8000 +func TestUDPProxyDirectDeliver(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + var r0, r1, r2 error + defer func() { + if r0 != nil || r1 != nil || r2 != nil { + t.Errorf("fail for ctx=%v, r0=%v, r1=%v, r2=%v", ctx.Err(), r0, r1, r2) + } + }() + + var wg sync.WaitGroup + defer wg.Wait() + + // Timeout, fail + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + select { + case <-ctx.Done(): + case <-time.After(time.Duration(*testTimeout) * time.Millisecond): + r2 = fmt.Errorf("timeout") + } + }() + + // For utest, we always proxy vnet packets to the random port we listen to. + mockServer := NewMockUDPEchoServer() + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + if err := mockServer.doMockUDPServer(ctx); err != nil { + r0 = err + } + }() + + // Create a vent and proxy. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + // When real server is ready, start the vnet test. + select { + case <-ctx.Done(): + return + case <-mockServer.realServerReady.Done(): + } + + doVnetProxy := func() error { + router, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + if err != nil { + return err + } + + clientNetwork := vnet.NewNet(&vnet.NetConfig{ + StaticIP: "10.0.0.11", + }) + if err = router.AddNet(clientNetwork); err != nil { + return err + } + + if err := router.Start(); err != nil { + return err + } + defer router.Stop() + + proxy, err := NewProxy(router) + if err != nil { + return err + } + defer proxy.Close() + + // For utest, mock the target real server. + proxy.mockRealServerAddr = mockServer.realServerAddr + + // The real server address to proxy to. + // Note that for utest, we will proxy to a local address. + serverAddr, err := net.ResolveUDPAddr("udp4", "192.168.1.10:8000") + if err != nil { + return err + } + + if err := proxy.Proxy(clientNetwork, serverAddr); err != nil { + return err + } + + // Now, all packets from client, will be proxy to real server, vice versa. + client, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5787") + if err != nil { + return err + } + + // When system quit, interrupt client. + selfKill, selfKillCancel := context.WithCancel(context.Background()) + go func() { + <-ctx.Done() + selfKillCancel() + client.Close() + }() + + // Write by vnet client. + if _, err := client.WriteTo([]byte("Hello"), serverAddr); err != nil { + return err + } + + buf := make([]byte, 1500) + if n, addr, err := client.ReadFrom(buf); err != nil { + if selfKill.Err() == context.Canceled { + return nil + } + return err + } else if n != 5 || addr == nil { + return fmt.Errorf("n=%v, addr=%v", n, addr) + } else if string(buf[:n]) != "Hello" { + return fmt.Errorf("data %v", buf[:n]) + } + + // Directly write, simulate the ARQ packet. + // We should got the echo packet also. + if _, err := proxy.Deliver(client.LocalAddr(), serverAddr, []byte("Hello")); err != nil { + return err + } + + if n, addr, err := client.ReadFrom(buf); err != nil { + if selfKill.Err() == context.Canceled { + return nil + } + return err + } else if n != 5 || addr == nil { + return fmt.Errorf("n=%v, addr=%v", n, addr) + } else if string(buf[:n]) != "Hello" { + return fmt.Errorf("data %v", buf[:n]) + } + + return err + } + + if err := doVnetProxy(); err != nil { + r1 = err + } + }() +} diff --git a/trunk/3rdparty/srs-bench/vnet/udpproxy_test.go b/trunk/3rdparty/srs-bench/vnet/udpproxy_test.go new file mode 100644 index 000000000..c0c1c4a2b --- /dev/null +++ b/trunk/3rdparty/srs-bench/vnet/udpproxy_test.go @@ -0,0 +1,615 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package vnet + +import ( + "context" + "errors" + "flag" + "fmt" + "net" + "os" + "sync" + "testing" + "time" + + "github.com/pion/logging" + "github.com/pion/transport/vnet" +) + +type MockUDPEchoServer struct { + realServerAddr *net.UDPAddr + realServerReady context.Context + realServerReadyCancel context.CancelFunc +} + +func NewMockUDPEchoServer() *MockUDPEchoServer { + v := &MockUDPEchoServer{} + v.realServerReady, v.realServerReadyCancel = context.WithCancel(context.Background()) + return v +} + +func (v *MockUDPEchoServer) doMockUDPServer(ctx context.Context) error { + // Listen to a random port. + laddr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:0") + if err != nil { + return err + } + + conn, err := net.ListenUDP("udp4", laddr) + if err != nil { + return err + } + + v.realServerAddr = conn.LocalAddr().(*net.UDPAddr) + v.realServerReadyCancel() + + // When system quit, interrupt client. + selfKill, selfKillCancel := context.WithCancel(context.Background()) + go func() { + <-ctx.Done() + selfKillCancel() + _ = conn.Close() + }() + + // Note that if they has the same ID, the address should not changed. + addrs := make(map[string]net.Addr) + + // Start an echo UDP server. + buf := make([]byte, 1500) + for ctx.Err() == nil { + n, addr, err := conn.ReadFrom(buf) + if err != nil { + if errors.Is(selfKill.Err(), context.Canceled) { + return nil + } + return err + } else if n == 0 || addr == nil { + return fmt.Errorf("n=%v, addr=%v", n, addr) // nolint:goerr113 + } else if nn, err := conn.WriteTo(buf[:n], addr); err != nil { + return err + } else if nn != n { + return fmt.Errorf("nn=%v, n=%v", nn, n) // nolint:goerr113 + } + + // Check the address, shold not change, use content as ID. + clientID := string(buf[:n]) + if oldAddr, ok := addrs[clientID]; ok && oldAddr.String() != addr.String() { + return fmt.Errorf("address change %v to %v", oldAddr.String(), addr.String()) // nolint:goerr113 + } + addrs[clientID] = addr + } + + return nil +} + +var testTimeout = flag.Int("timeout", 5000, "For each case, the timeout in ms") // nolint:gochecknoglobals + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +// vnet client: +// 10.0.0.11:5787 +// proxy to real server: +// 192.168.1.10:8000 +func TestUDPProxyOne2One(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + var r0, r1, r2 error + defer func() { + if r0 != nil || r1 != nil || r2 != nil { + t.Errorf("fail for ctx=%v, r0=%v, r1=%v, r2=%v", ctx.Err(), r0, r1, r2) + } + }() + + var wg sync.WaitGroup + defer wg.Wait() + + // Timeout, fail + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + select { + case <-ctx.Done(): + case <-time.After(time.Duration(*testTimeout) * time.Millisecond): + r2 = fmt.Errorf("timeout") // nolint:goerr113 + } + }() + + // For utest, we always proxy vnet packets to the random port we listen to. + mockServer := NewMockUDPEchoServer() + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + if err := mockServer.doMockUDPServer(ctx); err != nil { + r0 = err + } + }() + + // Create a vent and proxy. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + // When real server is ready, start the vnet test. + select { + case <-ctx.Done(): + return + case <-mockServer.realServerReady.Done(): + } + + doVnetProxy := func() error { + router, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + if err != nil { + return err + } + + clientNetwork := vnet.NewNet(&vnet.NetConfig{ + StaticIP: "10.0.0.11", + }) + if err = router.AddNet(clientNetwork); err != nil { + return err + } + + if err = router.Start(); err != nil { + return err + } + defer router.Stop() // nolint:errcheck + + proxy, err := NewProxy(router) + if err != nil { + return err + } + defer proxy.Close() // nolint:errcheck + + // For utest, mock the target real server. + proxy.mockRealServerAddr = mockServer.realServerAddr + + // The real server address to proxy to. + // Note that for utest, we will proxy to a local address. + serverAddr, err := net.ResolveUDPAddr("udp4", "192.168.1.10:8000") + if err != nil { + return err + } + + if err = proxy.Proxy(clientNetwork, serverAddr); err != nil { + return err + } + + // Now, all packets from client, will be proxy to real server, vice versa. + client, err := clientNetwork.ListenPacket("udp4", "10.0.0.11:5787") + if err != nil { + return err + } + + // When system quit, interrupt client. + selfKill, selfKillCancel := context.WithCancel(context.Background()) + go func() { + <-ctx.Done() + selfKillCancel() + _ = client.Close() // nolint:errcheck + }() + + for i := 0; i < 10; i++ { + if _, err = client.WriteTo([]byte("Hello"), serverAddr); err != nil { + return err + } + + var n int + var addr net.Addr + buf := make([]byte, 1500) + if n, addr, err = client.ReadFrom(buf); err != nil { // nolint:gocritic + if errors.Is(selfKill.Err(), context.Canceled) { + return nil + } + return err + } else if n != 5 || addr == nil { + return fmt.Errorf("n=%v, addr=%v", n, addr) // nolint:goerr113 + } else if string(buf[:n]) != "Hello" { + return fmt.Errorf("data %v", buf[:n]) // nolint:goerr113 + } + + // Wait for awhile for each UDP packet, to simulate real network. + select { + case <-ctx.Done(): + return nil + case <-time.After(30 * time.Millisecond): + } + } + + return err + } + + if err := doVnetProxy(); err != nil { + r1 = err + } + }() +} + +// vnet client: +// 10.0.0.11:5787 +// 10.0.0.11:5788 +// proxy to real server: +// 192.168.1.10:8000 +func TestUDPProxyTwo2One(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + var r0, r1, r2, r3 error + defer func() { + if r0 != nil || r1 != nil || r2 != nil || r3 != nil { + t.Errorf("fail for ctx=%v, r0=%v, r1=%v, r2=%v, r3=%v", ctx.Err(), r0, r1, r2, r3) + } + }() + + var wg sync.WaitGroup + defer wg.Wait() + + // Timeout, fail + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + select { + case <-ctx.Done(): + case <-time.After(time.Duration(*testTimeout) * time.Millisecond): + r2 = fmt.Errorf("timeout") // nolint:goerr113 + } + }() + + // For utest, we always proxy vnet packets to the random port we listen to. + mockServer := NewMockUDPEchoServer() + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + if err := mockServer.doMockUDPServer(ctx); err != nil { + r0 = err + } + }() + + // Create a vent and proxy. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + // When real server is ready, start the vnet test. + select { + case <-ctx.Done(): + return + case <-mockServer.realServerReady.Done(): + } + + doVnetProxy := func() error { + router, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + if err != nil { + return err + } + + clientNetwork := vnet.NewNet(&vnet.NetConfig{ + StaticIP: "10.0.0.11", + }) + if err = router.AddNet(clientNetwork); err != nil { + return err + } + + if err = router.Start(); err != nil { + return err + } + defer router.Stop() // nolint:errcheck + + proxy, err := NewProxy(router) + if err != nil { + return err + } + defer proxy.Close() // nolint:errcheck + + // For utest, mock the target real server. + proxy.mockRealServerAddr = mockServer.realServerAddr + + // The real server address to proxy to. + // Note that for utest, we will proxy to a local address. + serverAddr, err := net.ResolveUDPAddr("udp4", "192.168.1.10:8000") + if err != nil { + return err + } + + if err = proxy.Proxy(clientNetwork, serverAddr); err != nil { + return err + } + + handClient := func(address, echoData string) error { + // Now, all packets from client, will be proxy to real server, vice versa. + client, err := clientNetwork.ListenPacket("udp4", address) // nolint:govet + if err != nil { + return err + } + + // When system quit, interrupt client. + selfKill, selfKillCancel := context.WithCancel(context.Background()) + go func() { + <-ctx.Done() + selfKillCancel() + _ = client.Close() + }() + + for i := 0; i < 10; i++ { + if _, err := client.WriteTo([]byte(echoData), serverAddr); err != nil { // nolint:govet + return err + } + + var n int + var addr net.Addr + buf := make([]byte, 1400) + if n, addr, err = client.ReadFrom(buf); err != nil { // nolint:gocritic + if errors.Is(selfKill.Err(), context.Canceled) { + return nil + } + return err + } else if n != len(echoData) || addr == nil { + return fmt.Errorf("n=%v, addr=%v", n, addr) // nolint:goerr113 + } else if string(buf[:n]) != echoData { + return fmt.Errorf("check data %v", buf[:n]) // nolint:goerr113 + } + + // Wait for awhile for each UDP packet, to simulate real network. + select { + case <-ctx.Done(): + return nil + case <-time.After(30 * time.Millisecond): + } + } + + return nil + } + + client0, client0Cancel := context.WithCancel(context.Background()) + go func() { + defer client0Cancel() + address := "10.0.0.11:5787" + if err := handClient(address, "Hello"); err != nil { // nolint:govet + r3 = fmt.Errorf("client %v err %v", address, err) // nolint:goerr113 + } + }() + + client1, client1Cancel := context.WithCancel(context.Background()) + go func() { + defer client1Cancel() + address := "10.0.0.11:5788" + if err := handClient(address, "World"); err != nil { // nolint:govet + r3 = fmt.Errorf("client %v err %v", address, err) // nolint:goerr113 + } + }() + + select { + case <-ctx.Done(): + case <-client0.Done(): + case <-client1.Done(): + } + + return err + } + + if err := doVnetProxy(); err != nil { + r1 = err + } + }() +} + +// vnet client: +// 10.0.0.11:5787 +// proxy to real server: +// 192.168.1.10:8000 +// +// vnet client: +// 10.0.0.11:5788 +// proxy to real server: +// 192.168.1.10:8000 +func TestUDPProxyProxyTwice(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + var r0, r1, r2, r3 error + defer func() { + if r0 != nil || r1 != nil || r2 != nil || r3 != nil { + t.Errorf("fail for ctx=%v, r0=%v, r1=%v, r2=%v, r3=%v", ctx.Err(), r0, r1, r2, r3) + } + }() + + var wg sync.WaitGroup + defer wg.Wait() + + // Timeout, fail + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + select { + case <-ctx.Done(): + case <-time.After(time.Duration(*testTimeout) * time.Millisecond): + r2 = fmt.Errorf("timeout") // nolint:goerr113 + } + }() + + // For utest, we always proxy vnet packets to the random port we listen to. + mockServer := NewMockUDPEchoServer() + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + if err := mockServer.doMockUDPServer(ctx); err != nil { + r0 = err + } + }() + + // Create a vent and proxy. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + // When real server is ready, start the vnet test. + select { + case <-ctx.Done(): + return + case <-mockServer.realServerReady.Done(): + } + + doVnetProxy := func() error { + router, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + if err != nil { + return err + } + + clientNetwork := vnet.NewNet(&vnet.NetConfig{ + StaticIP: "10.0.0.11", + }) + if err = router.AddNet(clientNetwork); err != nil { + return err + } + + if err = router.Start(); err != nil { + return err + } + defer router.Stop() // nolint:errcheck + + proxy, err := NewProxy(router) + if err != nil { + return err + } + defer proxy.Close() // nolint:errcheck + + // For utest, mock the target real server. + proxy.mockRealServerAddr = mockServer.realServerAddr + + // The real server address to proxy to. + // Note that for utest, we will proxy to a local address. + serverAddr, err := net.ResolveUDPAddr("udp4", "192.168.1.10:8000") + if err != nil { + return err + } + + handClient := func(address, echoData string) error { + // We proxy multiple times, for example, in publisher and player, both call + // the proxy when got answer. + if err := proxy.Proxy(clientNetwork, serverAddr); err != nil { // nolint:govet + return err + } + + // Now, all packets from client, will be proxy to real server, vice versa. + client, err := clientNetwork.ListenPacket("udp4", address) // nolint:govet + if err != nil { + return err + } + + // When system quit, interrupt client. + selfKill, selfKillCancel := context.WithCancel(context.Background()) + go func() { + <-ctx.Done() + selfKillCancel() + _ = client.Close() // nolint:errcheck + }() + + for i := 0; i < 10; i++ { + if _, err = client.WriteTo([]byte(echoData), serverAddr); err != nil { + return err + } + + buf := make([]byte, 1500) + if n, addr, err := client.ReadFrom(buf); err != nil { // nolint:gocritic,govet + if errors.Is(selfKill.Err(), context.Canceled) { + return nil + } + return err + } else if n != len(echoData) || addr == nil { + return fmt.Errorf("n=%v, addr=%v", n, addr) // nolint:goerr113 + } else if string(buf[:n]) != echoData { + return fmt.Errorf("verify data %v", buf[:n]) // nolint:goerr113 + } + + // Wait for awhile for each UDP packet, to simulate real network. + select { + case <-ctx.Done(): + return nil + case <-time.After(30 * time.Millisecond): + } + } + + return nil + } + + client0, client0Cancel := context.WithCancel(context.Background()) + go func() { + defer client0Cancel() + address := "10.0.0.11:5787" + if err = handClient(address, "Hello"); err != nil { + r3 = fmt.Errorf("client %v err %v", address, err) // nolint:goerr113 + } + }() + + client1, client1Cancel := context.WithCancel(context.Background()) + go func() { + defer client1Cancel() + + // Slower than client0, 60ms. + // To simulate the real player or publisher, might not start at the same time. + select { + case <-ctx.Done(): + return + case <-time.After(150 * time.Millisecond): + } + + address := "10.0.0.11:5788" + if err = handClient(address, "World"); err != nil { + r3 = fmt.Errorf("client %v err %v", address, err) // nolint:goerr113 + } + }() + + select { + case <-ctx.Done(): + case <-client0.Done(): + case <-client1.Done(): + } + + return err + } + + if err := doVnetProxy(); err != nil { + r1 = err + } + }() +} From 030b94e7179981f7d9fd10767a012d3ef0d2419c Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 10 Mar 2021 07:03:57 +0800 Subject: [PATCH 016/563] Test: Add missing files for srs-bench --- trunk/.gitignore | 2 +- trunk/3rdparty/srs-bench/srs/ingester.go | 285 ++++++++ trunk/3rdparty/srs-bench/srs/interceptor.go | 175 +++++ trunk/3rdparty/srs-bench/srs/util_test.go | 721 ++++++++++++++++++++ 4 files changed, 1182 insertions(+), 1 deletion(-) create mode 100644 trunk/3rdparty/srs-bench/srs/ingester.go create mode 100644 trunk/3rdparty/srs-bench/srs/interceptor.go create mode 100644 trunk/3rdparty/srs-bench/srs/util_test.go diff --git a/trunk/.gitignore b/trunk/.gitignore index 1cbd7b7e6..44e1d851d 100644 --- a/trunk/.gitignore +++ b/trunk/.gitignore @@ -34,7 +34,7 @@ /research/speex/ /test/ .DS_Store -srs +./srs *.dSYM/ *.gcov *.ts diff --git a/trunk/3rdparty/srs-bench/srs/ingester.go b/trunk/3rdparty/srs-bench/srs/ingester.go new file mode 100644 index 000000000..1e3161a89 --- /dev/null +++ b/trunk/3rdparty/srs-bench/srs/ingester.go @@ -0,0 +1,285 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package srs + +import ( + "context" + "github.com/ossrs/go-oryx-lib/errors" + "github.com/ossrs/go-oryx-lib/logger" + "github.com/pion/interceptor" + "github.com/pion/rtp" + "github.com/pion/sdp/v3" + "github.com/pion/webrtc/v3" + "github.com/pion/webrtc/v3/pkg/media" + "github.com/pion/webrtc/v3/pkg/media/h264reader" + "github.com/pion/webrtc/v3/pkg/media/oggreader" + "io" + "os" + "strings" + "time" +) + +type videoIngester struct { + sourceVideo string + fps int + markerInterceptor *RTPInterceptor + sVideoTrack *webrtc.TrackLocalStaticSample + sVideoSender *webrtc.RTPSender +} + +func NewVideoIngester(sourceVideo string) *videoIngester { + return &videoIngester{markerInterceptor: &RTPInterceptor{}, sourceVideo: sourceVideo} +} + +func (v *videoIngester) Close() error { + if v.sVideoSender != nil { + v.sVideoSender.Stop() + v.sVideoSender = nil + } + return nil +} + +func (v *videoIngester) AddTrack(pc *webrtc.PeerConnection, fps int) error { + v.fps = fps + + mimeType, trackID := "video/H264", "video" + if strings.HasSuffix(v.sourceVideo, ".ivf") { + mimeType = "video/VP8" + } + + var err error + v.sVideoTrack, err = webrtc.NewTrackLocalStaticSample( + webrtc.RTPCodecCapability{MimeType: mimeType, ClockRate: 90000}, trackID, "pion", + ) + if err != nil { + return errors.Wrapf(err, "Create video track") + } + + v.sVideoSender, err = pc.AddTrack(v.sVideoTrack) + if err != nil { + return errors.Wrapf(err, "Add video track") + } + return err +} + +func (v *videoIngester) Ingest(ctx context.Context) error { + source, sender, track, fps := v.sourceVideo, v.sVideoSender, v.sVideoTrack, v.fps + + f, err := os.Open(source) + if err != nil { + return errors.Wrapf(err, "Open file %v", source) + } + defer f.Close() + + // TODO: FIXME: Support ivf for vp8. + h264, err := h264reader.NewReader(f) + if err != nil { + return errors.Wrapf(err, "Open h264 %v", source) + } + + enc := sender.GetParameters().Encodings[0] + codec := sender.GetParameters().Codecs[0] + headers := sender.GetParameters().HeaderExtensions + logger.Tf(ctx, "Video %v, tbn=%v, fps=%v, ssrc=%v, pt=%v, header=%v", + codec.MimeType, codec.ClockRate, fps, enc.SSRC, codec.PayloadType, headers) + + clock := newWallClock() + sampleDuration := time.Duration(uint64(time.Millisecond) * 1000 / uint64(fps)) + for ctx.Err() == nil { + var sps, pps *h264reader.NAL + var oFrames []*h264reader.NAL + for ctx.Err() == nil { + frame, err := h264.NextNAL() + if err == io.EOF { + return io.EOF + } + if err != nil { + return errors.Wrapf(err, "Read h264") + } + + oFrames = append(oFrames, frame) + logger.If(ctx, "NALU %v PictureOrderCount=%v, ForbiddenZeroBit=%v, RefIdc=%v, %v bytes", + frame.UnitType.String(), frame.PictureOrderCount, frame.ForbiddenZeroBit, frame.RefIdc, len(frame.Data)) + + if frame.UnitType == h264reader.NalUnitTypeSPS { + sps = frame + } else if frame.UnitType == h264reader.NalUnitTypePPS { + pps = frame + } else { + break + } + } + + var frames []*h264reader.NAL + // Package SPS/PPS to STAP-A + if sps != nil && pps != nil { + stapA := packageAsSTAPA(sps, pps) + frames = append(frames, stapA) + } + // Append other original frames. + for _, frame := range oFrames { + if frame.UnitType != h264reader.NalUnitTypeSPS && frame.UnitType != h264reader.NalUnitTypePPS { + frames = append(frames, frame) + } + } + + // Covert frames to sample(buffers). + for i, frame := range frames { + sample := media.Sample{Data: frame.Data, Duration: sampleDuration} + // Use the sample timestamp for frames. + if i != len(frames)-1 { + sample.Duration = 0 + } + + // For STAP-A, set marker to false, to make Chrome happy. + if ri := v.markerInterceptor; ri.rtpWriter == nil { + ri.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + // TODO: Should we decode to check whether SPS/PPS? + if len(payload) > 0 && payload[0]&0x1f == 24 { + header.Marker = false // 24, STAP-A + } + return ri.nextRTPWriter.Write(header, payload, attributes) + } + } + + if err = track.WriteSample(sample); err != nil { + return errors.Wrapf(err, "Write sample") + } + } + + if d := clock.Tick(sampleDuration); d > 0 { + time.Sleep(d) + } + } + + return ctx.Err() +} + +type audioIngester struct { + sourceAudio string + audioLevelInterceptor *RTPInterceptor + sAudioTrack *webrtc.TrackLocalStaticSample + sAudioSender *webrtc.RTPSender +} + +func NewAudioIngester(sourceAudio string) *audioIngester { + return &audioIngester{audioLevelInterceptor: &RTPInterceptor{}, sourceAudio: sourceAudio} +} + +func (v *audioIngester) Close() error { + if v.sAudioSender != nil { + v.sAudioSender.Stop() + v.sAudioSender = nil + } + return nil +} + +func (v *audioIngester) AddTrack(pc *webrtc.PeerConnection) error { + var err error + + mimeType, trackID := "audio/opus", "audio" + v.sAudioTrack, err = webrtc.NewTrackLocalStaticSample( + webrtc.RTPCodecCapability{MimeType: mimeType, ClockRate: 48000, Channels: 2}, trackID, "pion", + ) + if err != nil { + return errors.Wrapf(err, "Create audio track") + } + + v.sAudioSender, err = pc.AddTrack(v.sAudioTrack) + if err != nil { + return errors.Wrapf(err, "Add audio track") + } + + return nil +} + +func (v *audioIngester) Ingest(ctx context.Context) error { + source, sender, track := v.sourceAudio, v.sAudioSender, v.sAudioTrack + + f, err := os.Open(source) + if err != nil { + return errors.Wrapf(err, "Open file %v", source) + } + defer f.Close() + + ogg, _, err := oggreader.NewWith(f) + if err != nil { + return errors.Wrapf(err, "Open ogg %v", source) + } + + enc := sender.GetParameters().Encodings[0] + codec := sender.GetParameters().Codecs[0] + headers := sender.GetParameters().HeaderExtensions + logger.Tf(ctx, "Audio %v, tbn=%v, channels=%v, ssrc=%v, pt=%v, header=%v", + codec.MimeType, codec.ClockRate, codec.Channels, enc.SSRC, codec.PayloadType, headers) + + // Whether should encode the audio-level in RTP header. + var audioLevel *webrtc.RTPHeaderExtensionParameter + for _, h := range headers { + if h.URI == sdp.AudioLevelURI { + audioLevel = &h + } + } + + clock := newWallClock() + var lastGranule uint64 + + for ctx.Err() == nil { + pageData, pageHeader, err := ogg.ParseNextPage() + if err == io.EOF { + return io.EOF + } + if err != nil { + return errors.Wrapf(err, "Read ogg") + } + + // The amount of samples is the difference between the last and current timestamp + sampleCount := uint64(pageHeader.GranulePosition - lastGranule) + lastGranule = pageHeader.GranulePosition + sampleDuration := time.Duration(uint64(time.Millisecond) * 1000 * sampleCount / uint64(codec.ClockRate)) + + // For audio-level, set the extensions if negotiated. + if ri := v.audioLevelInterceptor; ri.rtpWriter == nil { + ri.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if audioLevel != nil { + audioLevelPayload, err := new(rtp.AudioLevelExtension).Marshal() + if err != nil { + return 0, err + } + + header.SetExtension(uint8(audioLevel.ID), audioLevelPayload) + } + + return ri.nextRTPWriter.Write(header, payload, attributes) + } + } + + if err = track.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); err != nil { + return errors.Wrapf(err, "Write sample") + } + + if d := clock.Tick(sampleDuration); d > 0 { + time.Sleep(d) + } + } + + return ctx.Err() +} diff --git a/trunk/3rdparty/srs-bench/srs/interceptor.go b/trunk/3rdparty/srs-bench/srs/interceptor.go new file mode 100644 index 000000000..d853aaf7d --- /dev/null +++ b/trunk/3rdparty/srs-bench/srs/interceptor.go @@ -0,0 +1,175 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package srs + +import ( + "github.com/pion/interceptor" + "github.com/pion/rtcp" + "github.com/pion/rtp" +) + +type RTPInterceptorOptionFunc func(i *RTPInterceptor) + +// Common RTP packet interceptor for benchmark. +// @remark Should never merge with RTCPInterceptor, because they has the same Write interface. +type RTPInterceptor struct { + localInfo *interceptor.StreamInfo + remoteInfo *interceptor.StreamInfo + // If rtpReader is nil, use the default next one to read. + rtpReader interceptor.RTPReaderFunc + nextRTPReader interceptor.RTPReader + // If rtpWriter is nil, use the default next one to write. + rtpWriter interceptor.RTPWriterFunc + nextRTPWriter interceptor.RTPWriter + BypassInterceptor +} + +func NewRTPInterceptor(options ...RTPInterceptorOptionFunc) *RTPInterceptor { + v := &RTPInterceptor{} + for _, opt := range options { + opt(v) + } + return v +} + +func (v *RTPInterceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { + if v.localInfo != nil { + return writer // Only handle one stream. + } + + v.localInfo = info + v.nextRTPWriter = writer + return v // Handle all RTP +} + +func (v *RTPInterceptor) Write(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + if v.rtpWriter != nil { + return v.rtpWriter(header, payload, attributes) + } + return v.nextRTPWriter.Write(header, payload, attributes) +} + +func (v *RTPInterceptor) UnbindLocalStream(info *interceptor.StreamInfo) { + if v.localInfo == nil || v.localInfo.ID != info.ID { + return + } + v.localInfo = nil // Reset the interceptor. +} + +func (v *RTPInterceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { + if v.remoteInfo != nil { + return reader // Only handle one stream. + } + + v.nextRTPReader = reader + return v // Handle all RTP +} + +func (v *RTPInterceptor) Read(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { + if v.rtpReader != nil { + return v.rtpReader(b, a) + } + return v.nextRTPReader.Read(b, a) +} + +func (v *RTPInterceptor) UnbindRemoteStream(info *interceptor.StreamInfo) { + if v.remoteInfo == nil || v.remoteInfo.ID != info.ID { + return + } + v.remoteInfo = nil +} + +type RTCPInterceptorOptionFunc func(i *RTCPInterceptor) + +// Common RTCP packet interceptor for benchmark. +// @remark Should never merge with RTPInterceptor, because they has the same Write interface. +type RTCPInterceptor struct { + // If rtcpReader is nil, use the default next one to read. + rtcpReader interceptor.RTCPReaderFunc + nextRTCPReader interceptor.RTCPReader + // If rtcpWriter is nil, use the default next one to write. + rtcpWriter interceptor.RTCPWriterFunc + nextRTCPWriter interceptor.RTCPWriter + BypassInterceptor +} + +func NewRTCPInterceptor(options ...RTCPInterceptorOptionFunc) *RTCPInterceptor { + v := &RTCPInterceptor{} + for _, opt := range options { + opt(v) + } + return v +} + +func (v *RTCPInterceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader { + v.nextRTCPReader = reader + return v // Handle all RTCP +} + +func (v *RTCPInterceptor) Read(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { + if v.rtcpReader != nil { + return v.rtcpReader(b, a) + } + return v.nextRTCPReader.Read(b, a) +} + +func (v *RTCPInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { + v.nextRTCPWriter = writer + return v // Handle all RTCP +} + +func (v *RTCPInterceptor) Write(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { + if v.rtcpWriter != nil { + return v.rtcpWriter(pkts, attributes) + } + return v.nextRTCPWriter.Write(pkts, attributes) +} + +// Do nothing. +type BypassInterceptor struct { + interceptor.Interceptor +} + +func (v *BypassInterceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader { + return reader +} + +func (v *BypassInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { + return writer +} + +func (v *BypassInterceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { + return writer +} + +func (v *BypassInterceptor) UnbindLocalStream(info *interceptor.StreamInfo) { +} + +func (v *BypassInterceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { + return reader +} + +func (v *BypassInterceptor) UnbindRemoteStream(info *interceptor.StreamInfo) { +} + +func (v *BypassInterceptor) Close() error { + return nil +} diff --git a/trunk/3rdparty/srs-bench/srs/util_test.go b/trunk/3rdparty/srs-bench/srs/util_test.go new file mode 100644 index 000000000..0c358f8e7 --- /dev/null +++ b/trunk/3rdparty/srs-bench/srs/util_test.go @@ -0,0 +1,721 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 srs-bench(ossrs) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package srs + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "github.com/ossrs/go-oryx-lib/errors" + "github.com/ossrs/go-oryx-lib/logger" + vnet_proxy "github.com/ossrs/srs-bench/vnet" + "github.com/pion/interceptor" + "github.com/pion/logging" + "github.com/pion/rtcp" + "github.com/pion/transport/vnet" + "github.com/pion/webrtc/v3" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "strings" + "sync" + "testing" + "time" +) + +var srsSchema = "http" +var srsHttps = flag.Bool("srs-https", false, "Whther connect to HTTPS-API") +var srsServer = flag.String("srs-server", "127.0.0.1", "The RTC server to connect to") +var srsStream = flag.String("srs-stream", "/rtc/regression", "The RTC stream to play") +var srsLog = flag.Bool("srs-log", false, "Whether enable the detail log") +var srsTimeout = flag.Int("srs-timeout", 5000, "For each case, the timeout in ms") +var srsPlayPLI = flag.Int("srs-play-pli", 5000, "The PLI interval in seconds for player.") +var srsPlayOKPackets = flag.Int("srs-play-ok-packets", 10, "If got N packets, it's ok, or fail") +var srsPublishOKPackets = flag.Int("srs-publish-ok-packets", 10, "If send N packets, it's ok, or fail") +var srsPublishAudio = flag.String("srs-publish-audio", "avatar.ogg", "The audio file for publisher.") +var srsPublishVideo = flag.String("srs-publish-video", "avatar.h264", "The video file for publisher.") +var srsPublishVideoFps = flag.Int("srs-publish-video-fps", 25, "The video fps for publisher.") +var srsVnetClientIP = flag.String("srs-vnet-client-ip", "192.168.168.168", "The client ip in pion/vnet.") +var srsDTLSDropPackets = flag.Int("srs-dtls-drop-packets", 5, "If dropped N packets, it's ok, or fail") + +func prepareTest() error { + var err error + + // Should parse it first. + flag.Parse() + + // The stream should starts with /, for example, /rtc/regression + if !strings.HasPrefix(*srsStream, "/") { + *srsStream = "/" + *srsStream + } + + // Generate srs protocol from whether use HTTPS. + if *srsHttps { + srsSchema = "https" + } + + // Check file. + tryOpenFile := func(filename string) (string, error) { + if filename == "" { + return filename, nil + } + + f, err := os.Open(filename) + if err != nil { + nfilename := path.Join("../", filename) + if fi, r0 := os.Stat(nfilename); r0 != nil && !fi.IsDir() { + return filename, errors.Wrapf(err, "No video file at %v or %v", filename, nfilename) + } + + return nfilename, nil + } + defer f.Close() + + return filename, nil + } + + if *srsPublishVideo, err = tryOpenFile(*srsPublishVideo); err != nil { + return err + } + + if *srsPublishAudio, err = tryOpenFile(*srsPublishAudio); err != nil { + return err + } + + return nil +} + +func TestMain(m *testing.M) { + if err := prepareTest(); err != nil { + logger.Ef(nil, "Prepare test fail, err %+v", err) + os.Exit(-1) + } + + // Disable the logger during all tests. + if *srsLog == false { + olw := logger.Switch(ioutil.Discard) + defer func() { + logger.Switch(olw) + }() + } + + os.Exit(m.Run()) +} + +type TestWebRTCAPIOptionFunc func(api *TestWebRTCAPI) + +type TestWebRTCAPI struct { + // The options to setup the api. + options []TestWebRTCAPIOptionFunc + // The api and settings. + api *webrtc.API + mediaEngine *webrtc.MediaEngine + registry *interceptor.Registry + settingEngine *webrtc.SettingEngine + // The vnet router, can be shared by different apis, but we do not share it. + router *vnet.Router + // The network for api. + network *vnet.Net + // The vnet UDP proxy bind to the router. + proxy *vnet_proxy.UDPProxy +} + +func NewTestWebRTCAPI(options ...TestWebRTCAPIOptionFunc) (*TestWebRTCAPI, error) { + v := &TestWebRTCAPI{} + + v.mediaEngine = &webrtc.MediaEngine{} + if err := v.mediaEngine.RegisterDefaultCodecs(); err != nil { + return nil, err + } + + v.registry = &interceptor.Registry{} + if err := webrtc.RegisterDefaultInterceptors(v.mediaEngine, v.registry); err != nil { + return nil, err + } + + for _, setup := range options { + setup(v) + } + + v.settingEngine = &webrtc.SettingEngine{} + + return v, nil +} + +func (v *TestWebRTCAPI) Close() error { + if v.proxy != nil { + v.proxy.Close() + v.proxy = nil + } + + if v.router != nil { + v.router.Stop() + v.router = nil + } + + return nil +} + +func (v *TestWebRTCAPI) Setup(vnetClientIP string, options ...TestWebRTCAPIOptionFunc) error { + // Setting engine for https://github.com/pion/transport/tree/master/vnet + setupVnet := func(vnetClientIP string) (err error) { + // We create a private router for a api, however, it's possible to share the + // same router between apis. + if v.router, err = vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", // Accept all ip, no sub router. + LoggerFactory: logging.NewDefaultLoggerFactory(), + }); err != nil { + return errors.Wrapf(err, "create router for api") + } + + // Each api should bind to a network, however, it's possible to share it + // for different apis. + v.network = vnet.NewNet(&vnet.NetConfig{ + StaticIP: vnetClientIP, + }) + + if err = v.router.AddNet(v.network); err != nil { + return errors.Wrapf(err, "create network for api") + } + + v.settingEngine.SetVNet(v.network) + + // Create a proxy bind to the router. + if v.proxy, err = vnet_proxy.NewProxy(v.router); err != nil { + return errors.Wrapf(err, "create proxy for router") + } + + return v.router.Start() + } + if err := setupVnet(vnetClientIP); err != nil { + return err + } + + for _, setup := range options { + setup(v) + } + + for _, setup := range v.options { + setup(v) + } + + v.api = webrtc.NewAPI( + webrtc.WithMediaEngine(v.mediaEngine), + webrtc.WithInterceptorRegistry(v.registry), + webrtc.WithSettingEngine(*v.settingEngine), + ) + + return nil +} + +func (v *TestWebRTCAPI) NewPeerConnection(configuration webrtc.Configuration) (*webrtc.PeerConnection, error) { + return v.api.NewPeerConnection(configuration) +} + +type TestPlayerOptionFunc func(p *TestPlayer) + +type TestPlayer struct { + pc *webrtc.PeerConnection + receivers []*webrtc.RTPReceiver + // root api object + api *TestWebRTCAPI + // Optional suffix for stream url. + streamSuffix string +} + +func NewTestPlayer(api *TestWebRTCAPI, options ...TestPlayerOptionFunc) *TestPlayer { + v := &TestPlayer{api: api} + + for _, opt := range options { + opt(v) + } + + return v +} + +func (v *TestPlayer) Close() error { + if v.pc != nil { + v.pc.Close() + v.pc = nil + } + + for _, receiver := range v.receivers { + receiver.Stop() + } + v.receivers = nil + + return nil +} + +func (v *TestPlayer) Run(ctx context.Context, cancel context.CancelFunc) error { + r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream) + if v.streamSuffix != "" { + r = fmt.Sprintf("%v-%v", r, v.streamSuffix) + } + pli := time.Duration(*srsPlayPLI) * time.Millisecond + logger.Tf(ctx, "Start play url=%v", r) + + pc, err := v.api.NewPeerConnection(webrtc.Configuration{}) + if err != nil { + return errors.Wrapf(err, "Create PC") + } + v.pc = pc + + pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{ + Direction: webrtc.RTPTransceiverDirectionRecvonly, + }) + pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{ + Direction: webrtc.RTPTransceiverDirectionRecvonly, + }) + + offer, err := pc.CreateOffer(nil) + if err != nil { + return errors.Wrapf(err, "Create Offer") + } + + if err := pc.SetLocalDescription(offer); err != nil { + return errors.Wrapf(err, "Set offer %v", offer) + } + + answer, err := apiRtcRequest(ctx, "/rtc/v1/play", r, offer.SDP) + if err != nil { + return errors.Wrapf(err, "Api request offer=%v", offer.SDP) + } + + // Start a proxy for real server and vnet. + if address, err := parseAddressOfCandidate(answer); err != nil { + return errors.Wrapf(err, "parse address of %v", answer) + } else if err := v.api.proxy.Proxy(v.api.network, address); err != nil { + return errors.Wrapf(err, "proxy %v to %v", v.api.network, address) + } + + if err := pc.SetRemoteDescription(webrtc.SessionDescription{ + Type: webrtc.SDPTypeAnswer, SDP: answer, + }); err != nil { + return errors.Wrapf(err, "Set answer %v", answer) + } + + handleTrack := func(ctx context.Context, track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) error { + // Send a PLI on an interval so that the publisher is pushing a keyframe + go func() { + if track.Kind() == webrtc.RTPCodecTypeAudio { + return + } + + for { + select { + case <-ctx.Done(): + return + case <-time.After(pli): + _ = pc.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ + MediaSSRC: uint32(track.SSRC()), + }}) + } + } + }() + + v.receivers = append(v.receivers, receiver) + + for ctx.Err() == nil { + _, _, err := track.ReadRTP() + if err != nil { + return errors.Wrapf(err, "Read RTP") + } + } + + return nil + } + + pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { + err = handleTrack(ctx, track, receiver) + if err != nil { + codec := track.Codec() + err = errors.Wrapf(err, "Handle track %v, pt=%v", codec.MimeType, codec.PayloadType) + cancel() + } + }) + + pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { + if state == webrtc.ICEConnectionStateFailed || state == webrtc.ICEConnectionStateClosed { + err = errors.Errorf("Close for ICE state %v", state) + cancel() + } + }) + + <-ctx.Done() + return err +} + +type TestPublisherOptionFunc func(p *TestPublisher) + +type TestPublisher struct { + onOffer func(s *webrtc.SessionDescription) error + onAnswer func(s *webrtc.SessionDescription) error + iceReadyCancel context.CancelFunc + // internal objects + aIngester *audioIngester + vIngester *videoIngester + pc *webrtc.PeerConnection + // root api object + api *TestWebRTCAPI + // Optional suffix for stream url. + streamSuffix string +} + +func NewTestPublisher(api *TestWebRTCAPI, options ...TestPublisherOptionFunc) *TestPublisher { + sourceVideo, sourceAudio := *srsPublishVideo, *srsPublishAudio + + v := &TestPublisher{api: api} + + for _, opt := range options { + opt(v) + } + + // Create ingesters. + if sourceAudio != "" { + v.aIngester = NewAudioIngester(sourceAudio) + } + if sourceVideo != "" { + v.vIngester = NewVideoIngester(sourceVideo) + } + + // Setup the interceptors for packets. + api.options = append(api.options, func(api *TestWebRTCAPI) { + // Filter for RTCP packets. + rtcpInterceptor := &RTCPInterceptor{} + rtcpInterceptor.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + return rtcpInterceptor.nextRTCPReader.Read(buf, attributes) + } + rtcpInterceptor.rtcpWriter = func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { + return rtcpInterceptor.nextRTCPWriter.Write(pkts, attributes) + } + api.registry.Add(rtcpInterceptor) + + // Filter for ingesters. + if sourceAudio != "" { + api.registry.Add(v.aIngester.audioLevelInterceptor) + } + if sourceVideo != "" { + api.registry.Add(v.vIngester.markerInterceptor) + } + }) + + return v +} + +func (v *TestPublisher) Close() error { + if v.vIngester != nil { + v.vIngester.Close() + } + + if v.aIngester != nil { + v.aIngester.Close() + } + + if v.pc != nil { + v.pc.Close() + } + + return nil +} + +func (v *TestPublisher) SetStreamSuffix(suffix string) *TestPublisher { + v.streamSuffix = suffix + return v +} + +func (v *TestPublisher) Run(ctx context.Context, cancel context.CancelFunc) error { + r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream) + if v.streamSuffix != "" { + r = fmt.Sprintf("%v-%v", r, v.streamSuffix) + } + sourceVideo, sourceAudio, fps := *srsPublishVideo, *srsPublishAudio, *srsPublishVideoFps + + logger.Tf(ctx, "Start publish url=%v, audio=%v, video=%v, fps=%v", + r, sourceAudio, sourceVideo, fps) + + pc, err := v.api.NewPeerConnection(webrtc.Configuration{}) + if err != nil { + return errors.Wrapf(err, "Create PC") + } + v.pc = pc + + if v.vIngester != nil { + if err := v.vIngester.AddTrack(pc, fps); err != nil { + return errors.Wrapf(err, "Add track") + } + defer v.vIngester.Close() + } + + if v.aIngester != nil { + if err := v.aIngester.AddTrack(pc); err != nil { + return errors.Wrapf(err, "Add track") + } + defer v.aIngester.Close() + } + + offer, err := pc.CreateOffer(nil) + if err != nil { + return errors.Wrapf(err, "Create Offer") + } + + if err := pc.SetLocalDescription(offer); err != nil { + return errors.Wrapf(err, "Set offer %v", offer) + } + + if v.onOffer != nil { + if err := v.onOffer(&offer); err != nil { + return errors.Wrapf(err, "sdp %v %v", offer.Type, offer.SDP) + } + } + + answerSDP, err := apiRtcRequest(ctx, "/rtc/v1/publish", r, offer.SDP) + if err != nil { + return errors.Wrapf(err, "Api request offer=%v", offer.SDP) + } + + // Start a proxy for real server and vnet. + if address, err := parseAddressOfCandidate(answerSDP); err != nil { + return errors.Wrapf(err, "parse address of %v", answerSDP) + } else if err := v.api.proxy.Proxy(v.api.network, address); err != nil { + return errors.Wrapf(err, "proxy %v to %v", v.api.network, address) + } + + answer := &webrtc.SessionDescription{ + Type: webrtc.SDPTypeAnswer, SDP: answerSDP, + } + if v.onAnswer != nil { + if err := v.onAnswer(answer); err != nil { + return errors.Wrapf(err, "on answerSDP") + } + } + + if err := pc.SetRemoteDescription(*answer); err != nil { + return errors.Wrapf(err, "Set answerSDP %v", answerSDP) + } + + logger.Tf(ctx, "State signaling=%v, ice=%v, conn=%v", pc.SignalingState(), pc.ICEConnectionState(), pc.ConnectionState()) + + // ICE state management. + pc.OnICEGatheringStateChange(func(state webrtc.ICEGathererState) { + logger.Tf(ctx, "ICE gather state %v", state) + }) + pc.OnICECandidate(func(candidate *webrtc.ICECandidate) { + logger.Tf(ctx, "ICE candidate %v %v:%v", candidate.Protocol, candidate.Address, candidate.Port) + + }) + pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { + logger.Tf(ctx, "ICE state %v", state) + }) + + pc.OnSignalingStateChange(func(state webrtc.SignalingState) { + logger.Tf(ctx, "Signaling state %v", state) + }) + + if v.aIngester != nil { + v.aIngester.sAudioSender.Transport().OnStateChange(func(state webrtc.DTLSTransportState) { + logger.Tf(ctx, "DTLS state %v", state) + }) + } + + pcDone, pcDoneCancel := context.WithCancel(context.Background()) + pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + logger.Tf(ctx, "PC state %v", state) + + if state == webrtc.PeerConnectionStateConnected { + pcDoneCancel() + if v.iceReadyCancel != nil { + v.iceReadyCancel() + } + } + + if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateClosed { + err = errors.Errorf("Close for PC state %v", state) + cancel() + } + }) + + // Wait for event from context or tracks. + var wg sync.WaitGroup + var finalErr error + + wg.Add(1) + go func() { + defer wg.Done() + defer logger.Tf(ctx, "ingest notify done") + + <-ctx.Done() + + if v.aIngester != nil && v.aIngester.sAudioSender != nil { + v.aIngester.sAudioSender.Stop() + } + + if v.vIngester != nil && v.vIngester.sVideoSender != nil { + v.vIngester.sVideoSender.Stop() + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + if v.aIngester == nil { + return + } + + select { + case <-ctx.Done(): + return + case <-pcDone.Done(): + } + + wg.Add(1) + go func() { + defer wg.Done() + defer logger.Tf(ctx, "aingester sender read done") + + buf := make([]byte, 1500) + for ctx.Err() == nil { + if _, _, err := v.aIngester.sAudioSender.Read(buf); err != nil { + return + } + } + }() + + for { + if err := v.aIngester.Ingest(ctx); err != nil { + if err == io.EOF { + logger.Tf(ctx, "aingester retry for %v", err) + continue + } + if err != context.Canceled { + finalErr = errors.Wrapf(err, "audio") + } + + logger.Tf(ctx, "aingester err=%v, final=%v", err, finalErr) + return + } + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + if v.vIngester == nil { + return + } + + select { + case <-ctx.Done(): + return + case <-pcDone.Done(): + logger.Tf(ctx, "PC(ICE+DTLS+SRTP) done, start ingest video %v", sourceVideo) + } + + wg.Add(1) + go func() { + defer wg.Done() + defer logger.Tf(ctx, "vingester sender read done") + + buf := make([]byte, 1500) + for ctx.Err() == nil { + // The Read() might block in r.rtcpInterceptor.Read(b, a), + // so that the Stop() can not stop it. + if _, _, err := v.vIngester.sVideoSender.Read(buf); err != nil { + return + } + } + }() + + for { + if err := v.vIngester.Ingest(ctx); err != nil { + if err == io.EOF { + logger.Tf(ctx, "vingester retry for %v", err) + continue + } + if err != context.Canceled { + finalErr = errors.Wrapf(err, "video") + } + + logger.Tf(ctx, "vingester err=%v, final=%v", err, finalErr) + return + } + } + }() + + wg.Wait() + + logger.Tf(ctx, "ingester done ctx=%v, final=%v", ctx.Err(), finalErr) + if finalErr != nil { + return finalErr + } + return ctx.Err() +} + +func TestRTCServerVersion(t *testing.T) { + api := fmt.Sprintf("http://%v:1985/api/v1/versions", *srsServer) + req, err := http.NewRequest("POST", api, nil) + if err != nil { + t.Errorf("Request %v", api) + return + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Errorf("Do request %v", api) + return + } + + b, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Errorf("Read body of %v", api) + return + } + + obj := struct { + Code int `json:"code"` + Server string `json:"server"` + Data struct { + Major int `json:"major"` + Minor int `json:"minor"` + Revision int `json:"revision"` + Version string `json:"version"` + } `json:"data"` + }{} + if err := json.Unmarshal(b, &obj); err != nil { + t.Errorf("Parse %v", string(b)) + return + } + if obj.Code != 0 { + t.Errorf("Server err code=%v, server=%v", obj.Code, obj.Server) + return + } + if obj.Data.Major == 0 && obj.Data.Minor == 0 { + t.Errorf("Invalid version %v", obj.Data) + return + } +} From 27d4080084369163ad4ea8dada1141c373e433b0 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 10 Mar 2021 07:26:18 +0800 Subject: [PATCH 017/563] Test: Fix check file bug --- trunk/3rdparty/srs-bench/srs/util_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/trunk/3rdparty/srs-bench/srs/util_test.go b/trunk/3rdparty/srs-bench/srs/util_test.go index 0c358f8e7..68187c9ef 100644 --- a/trunk/3rdparty/srs-bench/srs/util_test.go +++ b/trunk/3rdparty/srs-bench/srs/util_test.go @@ -84,9 +84,11 @@ func prepareTest() error { f, err := os.Open(filename) if err != nil { nfilename := path.Join("../", filename) - if fi, r0 := os.Stat(nfilename); r0 != nil && !fi.IsDir() { + f2, err := os.Open(nfilename) + if err != nil { return filename, errors.Wrapf(err, "No video file at %v or %v", filename, nfilename) } + defer f2.Close() return nfilename, nil } From f066914968169444ff5cb71d0ca8870fb599fb02 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 10 Mar 2021 07:38:11 +0800 Subject: [PATCH 018/563] Test: Add missing files. --- trunk/3rdparty/ccache/build_ccache.sh | 17 ----------------- trunk/3rdparty/ccache/ccache-3.1.9.zip | Bin 390557 -> 0 bytes trunk/3rdparty/ccache/readme.txt | 11 ----------- trunk/3rdparty/srs-bench/avatar.h264 | Bin 0 -> 450083 bytes 4 files changed, 28 deletions(-) delete mode 100755 trunk/3rdparty/ccache/build_ccache.sh delete mode 100644 trunk/3rdparty/ccache/ccache-3.1.9.zip delete mode 100644 trunk/3rdparty/ccache/readme.txt create mode 100644 trunk/3rdparty/srs-bench/avatar.h264 diff --git a/trunk/3rdparty/ccache/build_ccache.sh b/trunk/3rdparty/ccache/build_ccache.sh deleted file mode 100755 index 53b187882..000000000 --- a/trunk/3rdparty/ccache/build_ccache.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -# check exists. -if [[ -f /usr/local/bin/ccache ]]; then - echo "ccache is ok"; - exit 0; -fi - -# check sudoer. -sudo echo "ok" > /dev/null 2>&1; -ret=$?; if [[ 0 -ne ${ret} ]]; then echo "you must be sudoer"; exit 1; fi - -unzip ccache-3.1.9.zip && cd ccache-3.1.9 && ./configure && make -ret=$?; if [[ $ret -ne 0 ]]; then echo "build ccache failed."; exit $ret; fi - -sudo cp ccache /usr/local/bin && sudo ln -s ccache /usr/local/bin/gcc && sudo ln -s ccache /usr/local/bin/g++ && sudo ln -s ccache /usr/local/bin/cc && sudo ln -s ccache /usr/local/bin/c++ -ret=$?; if [[ $ret -ne 0 ]]; then echo "install ccache failed."; exit $ret; fi diff --git a/trunk/3rdparty/ccache/ccache-3.1.9.zip b/trunk/3rdparty/ccache/ccache-3.1.9.zip deleted file mode 100644 index 10c96dd0cfda0d5b38dfb752eeb3e58d83550e70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390557 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ⅈ7zWiHqjemg41)=#QJGKjKW(13Vn{jpE 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%0A{;~j{EmP>S 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 b|H+O21snpP!yu3j@WX-_s3OJy-URx8#@QuK diff --git a/trunk/3rdparty/ccache/readme.txt b/trunk/3rdparty/ccache/readme.txt deleted file mode 100644 index 611d9eaee..000000000 --- a/trunk/3rdparty/ccache/readme.txt +++ /dev/null @@ -1,11 +0,0 @@ -ccache是samba组织提供的加速编译过程的工具, -使用虚拟机编译可以考虑用这个工具,让编译过程飞快。 - -链接: - http://ccache.samba.org/ - http://samba.org/ftp/ccache/ccache-3.1.9.tar.xz - http://ccache.samba.org/manual.html - -安装方法: - bash build_ccache.sh -注意:要求以sudoer执行,要修改文件。 \ No newline at end of file diff --git a/trunk/3rdparty/srs-bench/avatar.h264 b/trunk/3rdparty/srs-bench/avatar.h264 new file mode 100644 index 0000000000000000000000000000000000000000..a82911a647f7c3c5ea5bc74dff96ef2be11c2b21 GIT binary patch literal 450083 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

b?`=Vd(G%oE{2MLzmx8V~UF{~f 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�Pzt=7I|LV?nwT1 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%0A(Dj|gCs!61lL=e;thy}k|S9^ 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 a_z7Hb5&mc3=H?KY-CryUqCe7@qq<+C)XhEs literal 0 HcmV?d00001 From 7f8df6f21fe217d35411ede1eaeab6564db39a38 Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 11 Mar 2021 08:15:20 +0800 Subject: [PATCH 019/563] Build: Refine script for SRTP. Because we have upgraded to openssl-1.1.1d and libsrtp-2.3, so it's able to enable ASM for SRTP for not only openssl-1.0, because libsrtp-2.3 fixed the capacity bug. --- trunk/auto/depends.sh | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/trunk/auto/depends.sh b/trunk/auto/depends.sh index 23ec86081..9033ef7e3 100755 --- a/trunk/auto/depends.sh +++ b/trunk/auto/depends.sh @@ -552,18 +552,6 @@ fi ##################################################################################### # srtp ##################################################################################### -# For openssl-1.1.*, we should disable SRTP ASM, because SRTP only works with openssl-1.0.* -if [[ $SRS_SRTP_ASM == YES ]]; then - echo " #include " > ${SRS_OBJS}/_tmp_srtp_asm_detect.c - echo " #if OPENSSL_VERSION_NUMBER >= 0x10100000L // v1.1.x " >> ${SRS_OBJS}/_tmp_srtp_asm_detect.c - echo " #error \"SRTP only works with openssl-1.0.*\" " >> ${SRS_OBJS}/_tmp_srtp_asm_detect.c - echo " #endif " >> ${SRS_OBJS}/_tmp_srtp_asm_detect.c - ${SRS_TOOL_CC} -c ${SRS_OBJS}/_tmp_srtp_asm_detect.c -I${SRS_OBJS}/openssl/include -o /dev/null >/dev/null 2>&1 - if [[ $? -ne 0 ]]; then - SRS_SRTP_ASM=NO && echo "Warning: Disable SRTP-ASM optimization, please update docker"; - fi - rm -f ${SRS_OBJS}/_tmp_srtp_asm_detect.c -fi; SRTP_CONFIG="echo SRTP without openssl(ASM) optimization" && SRTP_OPTIONS="" # If use ASM for SRTP, we enable openssl(with ASM). if [[ $SRS_SRTP_ASM == YES ]]; then From d53b5b3f2eff6b1135ffd96cef9665e8e937b1c4 Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 11 Mar 2021 12:31:15 +0800 Subject: [PATCH 020/563] Security: Support CodeQL analysis --- .github/workflows/codeql-analysis.yml | 66 +++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..1dfdd79c1 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,66 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ 4.0release, develop ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ 4.0release, develop ] + schedule: + - cron: '43 11 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'cpp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + #- name: Autobuild + # uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + - name: Build SRS + run: | + ./configure && make + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 197fe1031036c0ad719d08b2bffdb5ec32f41984 Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 11 Mar 2021 12:33:01 +0800 Subject: [PATCH 021/563] Security: Support CodeQL analysis --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1dfdd79c1..3447c962f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -60,7 +60,7 @@ jobs: # uses a compiled language - name: Build SRS run: | - ./configure && make + cd trunk && ./configure && make - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 From a53fe451ffa871f70a4c6c189349bf1ad814650c Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 11 Mar 2021 16:48:29 +0800 Subject: [PATCH 022/563] RTC: Feed TWCC then drop the specified PT packet. 1. Sometimes we might drop RTP packets, by PT(payload type). 2. For example, the padding packets from client. 3. We should feed these packets to TWCC, then drop it. --- trunk/src/app/srs_app_rtc_conn.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index c82241052..e6afab909 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -1116,14 +1116,6 @@ srs_error_t SrsRtcPublishStream::on_rtp(char* data, int nb_data) return err; } - // If payload type is configed to drop, ignore this packet. - if (pt_to_drop_) { - uint8_t pt = srs_rtp_fast_parse_pt(data, nb_data); - if (pt_to_drop_ == pt) { - return err; - } - } - // Decode the header first. if (twcc_id_) { // We must parse the TWCC from RTP header before SRTP unprotect, because: @@ -1140,6 +1132,14 @@ srs_error_t SrsRtcPublishStream::on_rtp(char* data, int nb_data) } } + // If payload type is configed to drop, ignore this packet. + if (pt_to_drop_) { + uint8_t pt = srs_rtp_fast_parse_pt(data, nb_data); + if (pt_to_drop_ == pt) { + return err; + } + } + // Decrypt the cipher to plaintext RTP data. char* plaintext = data; int nb_plaintext = nb_data; From ad7576b05d8fcd4d8b3c0126630c529afce58698 Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 11 Mar 2021 17:08:10 +0800 Subject: [PATCH 023/563] Update CI and CodeCov status in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f1d66ed6e..3e7d11069 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # SRS(Simple Realtime Server) -![](http://ossrs.net/gif/v1/sls.gif?site=github.com&path=/srs/develop) -[![](https://circleci.com/gh/ossrs/srs/tree/develop.svg?style=svg&circle-token=1ef1d5b5b0cde6c8c282ed856a18199f9e8f85a9)](https://circleci.com/gh/ossrs/srs/tree/develop) -[![](https://codecov.io/gh/ossrs/srs/branch/develop/graph/badge.svg)](https://codecov.io/gh/ossrs/srs/branch/develop) +![](http://ossrs.net/gif/v1/sls.gif?site=github.com&path=/srs/4.0release) +[![](https://circleci.com/gh/ossrs/srs/tree/4.0release.svg?style=svg&circle-token=1ef1d5b5b0cde6c8c282ed856a18199f9e8f85a9)](https://circleci.com/gh/ossrs/srs/tree/4.0release) +[![](https://codecov.io/gh/ossrs/srs/branch/4.0release/graph/badge.svg)](https://codecov.io/gh/ossrs/srs/branch/4.0release) [![](https://cloud.githubusercontent.com/assets/2777660/22814959/c51cbe72-ef92-11e6-81cc-32b657b285d5.png)](https://github.com/ossrs/srs/wiki/v1_CN_Contact#wechat) SRS/4.0,[Leo][release4],是一个流媒体集群,支持RTMP/HLS/WebRTC/SRT/GB28181,高效、稳定、易用,简单而快乐。
From a3dbb2269671ac5ccf93ac47a6243601a44670c8 Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 11 Mar 2021 17:12:44 +0800 Subject: [PATCH 024/563] Fix bugs. 4.0.85 --- trunk/src/core/srs_core_version4.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index 8a76cdb2f..ddca1c39d 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -24,6 +24,6 @@ #ifndef SRS_CORE_VERSION4_HPP #define SRS_CORE_VERSION4_HPP -#define SRS_VERSION4_REVISION 84 +#define SRS_VERSION4_REVISION 85 #endif From c19333164841b15c38bfde94e66e0c4adc5be0f1 Mon Sep 17 00:00:00 2001 From: winlin Date: Mon, 15 Mar 2021 13:45:06 +0800 Subject: [PATCH 025/563] Update README --- README.md | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3e7d11069..93200cd4d 100755 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ Other documents: - [x] Edge server supports remuxing RTMP to HTTP-FLV([CN][v3_CN_SampleHttpFlv], [EN][v3_EN_SampleHttpFlv]). As for HLS([CN][v3_CN_DeliveryHLS], [EN][v3_EN_DeliveryHLS]) edge server, recomment to use HTTP edge server, such as [NGINX](http://nginx.org/). - [x] Support HLS with audio-only([CN][v3_CN_DeliveryHLS2], [EN][v3_EN_DeliveryHLS2]), which need to build the timestamp from AAC samples, so we enhanced it please read [#547][bug #547]. - [x] Support HLS with mp3(h.264+mp3) audio codec, please read [bug #301][bug #301]. -- [x] Support remuxing RTMP to http FLV/MP3/AAC/TS live streaming, please read wiki([CN][v2_CN_DeliveryHttpStream], [EN][v2_CN_DeliveryHttpStream]). +- [x] Support transmux RTMP to HTTP-FLV/MP3/AAC/TS, please read wiki([CN][v2_CN_DeliveryHttpStream], [EN][v2_CN_DeliveryHttpStream]). - [x] Support ingesting([CN][v1_CN_Ingest], [EN][v1_EN_Ingest]) other protocols to SRS by FFMPEG. - [x] Support RTMP long time(>4.6hours) publishing/playing, with the timestamp corrected. - [x] Support publishing h264 raw stream([CN][v3_CN_SrsLibrtmp2], [EN][v3_EN_SrsLibrtmp2]) by srs-librtmp([CN][v3_CN_SrsLibrtmp], [EN][v3_EN_SrsLibrtmp]). @@ -140,42 +140,45 @@ Other documents: - [x] Support Vhost([CN][v1_CN_RtmpUrlVhost], [EN][v1_EN_RtmpUrlVhost]) and \_\_defaultVhost\_\_. - [x] Support reloading([CN][v1_CN_Reload], [EN][v1_EN_Reload]) to apply changes of config. - [x] Support listening at multiple ports. -- [x] Support forwarding([CN][v3_CN_Forward], [EN][v3_EN_Forward]) from master to slave server. -- [x] Support transcoding([CN][v3_CN_FFMPEG], [EN][v3_EN_FFMPEG]) live streaming by FFMPEG. +- [x] Support forwarding([CN][v3_CN_Forward], [EN][v3_EN_Forward]) to other RTMP servers. +- [x] Support transcoding([CN][v3_CN_FFMPEG], [EN][v3_EN_FFMPEG]) by FFMPEG. - [x] All wikis are writen in [Chinese][v3_CN_Home] and [English][v3_EN_Home]. - [x] Enhanced json, replace NXJSON(LGPL) with json-parser(BSD), read [#904][bug #904]. - [x] Support valgrind and latest ARM by patching ST, read [ST#1](https://github.com/ossrs/state-threads/issues/1) and [ST#2](https://github.com/ossrs/state-threads/issues/2). -- [x] Support tracable and session-based log([CN][v1_CN_SrsLog], [EN][v1_EN_SrsLog]). -- [x] High concurrency and performance([CN][v1_CN_Performance], [EN][v1_EN_Performance]), 6000+ connections(200kbps), CPU 82%, 203MB. +- [x] Support traceable and session-based log([CN][v1_CN_SrsLog], [EN][v1_EN_SrsLog]). +- [x] High performance([CN][v1_CN_Performance], [EN][v1_EN_Performance]) RTMP/HTTP-FLV, 6000+ connections. - [x] Enhanced complex error code with description and stack, read [#913][bug #913]. - [x] Enhanced RTMP url which supports vhost in stream, read [#1059][bug #1059]. - [x] Support origin cluster, please read [#464][bug #464], [RTMP 302][bug #92]. - [x] Support listen at IPv4 and IPv6, read [#460][bug #460]. -- [x] Support SO_REUSEPORT, to improve edge server performance, read [#775][bug #775]. - [x] Improve test coverage for core/kernel/protocol/service. -- [x] [Experimental] Support docker by [srs-docker](https://github.com/ossrs/srs-docker). +- [x] Support docker by [srs-docker](https://github.com/ossrs/srs-docker). +- [x] Support multiple processes by ReusePort([CN][v3_CN_REUSEPORT], [EN][v3_EN_REUSEPORT]), [#775][bug #775]. +- [x] Support a simple [mgmt console][console], please read [srs-ngb][srs-ngb]. +- [x] [Experimental] Support playing stream by WebRTC, [#307][bug #307]. +- [x] [Experimental] Support publishing stream by WebRTC, [#307][bug #307]. +- [x] [Experimental] Support mux RTP/RTCP/DTLS/SRTP on one port for WebRTC, [#307][bug #307]. +- [x] [Experimental] Support client address changing for WebRTC, [#307][bug #307]. +- [x] [Experimental] Support transcode RTMP/AAC to WebRTC/Opus, [#307][bug #307]. +- [x] [Experimental] Enhance HTTP Stream Server for HTTP-FLV, HTTPS, HLS etc. [#1657][bug #1657]. +- [x] [Experimental] Support push stream by GB28181, [#1500][bug #1500]. - [x] [Experimental] Support DVR in MP4 format, read [#738][bug #738]. - [x] [Experimental] Support MPEG-DASH, the future live streaming protocol, read [#299][bug #299]. - [x] [Experimental] Support pushing MPEG-TS over UDP, please read [bug #250][bug #250]. - [x] [Experimental] Support pushing RTSP, please read [bug #133][bug #133]. -- [x] [Experimental] Support pushing FLV over HTTP POST, please read [wiki]([CN][v2_CN_Streamer2], [EN][v2_EN_Streamer2]). -- [x] [Experimental] Support multiple processes by [dolphin][srs-dolphin] or [oryx][oryx]. -- [x] [Experimental] Support a simple [mgmt console][console], please read [srs-ngb][srs-ngb]. -- [x] [Experimental] Support RTMP client library: srs-librtmp([CN][v3_CN_SrsLibrtmp], [EN][v3_EN_SrsLibrtmp]) +- [x] [Experimental] Support pushing FLV over HTTP POST, please read wiki([CN][v2_CN_Streamer2], [EN][v2_EN_Streamer2]). - [x] [Experimental] Support HTTP RAW API, please read [#459][bug #459], [#470][bug #470], [#319][bug #319]. - [x] [Experimental] Support SRT server, read [#1147][bug #1147]. -- [x] [Experimental] Support playing stream by WebRTC, [#307][bug #307]. -- [x] [Experimental] Support publishing stream by WebRTC, [#307][bug #307]. -- [x] [Experimental] Support push stream by GB28181, [#1500][bug #1500]. -- [x] [Experimental] Enhance HTTP Stream Server for HTTP-FLV, HTTPS, HLS etc. [#1657][bug #1657]. +- [x] [Deprecated] Support RTMP client library: srs-librtmp([CN][v3_CN_SrsLibrtmp], [EN][v3_EN_SrsLibrtmp]) - [x] [Deprecated] Support Adobe HDS(f4m), please read wiki([CN][v2_CN_DeliveryHDS], [EN][v2_EN_DeliveryHDS]) and [#1535][bug #1535]. - [x] [Deprecated] Support bandwidth testing([CN][v1_CN_BandwidthTestTool], [EN][v1_EN_BandwidthTestTool]), please read [#1535][bug #1535]. - [x] [Deprecated] Support Adobe FMS/AMS token traverse([CN][v3_CN_DRM2], [EN][v3_EN_DRM2]) authentication, please read [#1535][bug #1535]. -- [ ] Enhanced forwarding with vhost and variables. -- [ ] Support source cleanup for idle streams. -- [ ] Support H.265 by pushing H.265 over RTMP, deliverying in HLS, read [#465][bug #465]. -- [ ] Support UDP protocol such as QUIC or KCP in cluster. -- [ ] Support H.264+Opus codec for WebRTC, [#307][bug #307]. +- [ ] Enhanced forwarding with vhost and variables, [#1342][bug #1342]. +- [ ] Support transmux RTC to RTMP, [#2093][bug #2093]. +- [ ] Support H.265 over RTMP and HLS, [#465][bug #465]. +- [ ] Support IETF-QUIC for WebRTC Cluster, [#2091][bug #2091]. +- [ ] Improve RTC performance to 5K by multiple threading, [#2188][bug #2188]. +- [ ] Support source cleanup for idle streams, [#413][bug #413]. - [ ] Support change user to run SRS, [#1111][bug #1111]. - [ ] Support HLS variant, [#463][bug #463]. @@ -1848,6 +1851,11 @@ Winlin [bug #1987]: https://github.com/ossrs/srs/issues/1987 [bug #1548]: https://github.com/ossrs/srs/issues/1548 [bug #1694]: https://github.com/ossrs/srs/issues/1694 +[bug #413]: https://github.com/ossrs/srs/issues/413 +[bug #2091]: https://github.com/ossrs/srs/issues/2091 +[bug #1342]: https://github.com/ossrs/srs/issues/1342 +[bug #2093]: https://github.com/ossrs/srs/issues/2093 +[bug #2188]: https://github.com/ossrs/srs/issues/2188 [bug #yyyyyyyyyyyyy]: https://github.com/ossrs/srs/issues/yyyyyyyyyyyyy [bug #1631]: https://github.com/ossrs/srs/issues/1631 From 42c5a935f9c6ff4f6e9f31f6f77a804a21c534a8 Mon Sep 17 00:00:00 2001 From: winlin Date: Mon, 15 Mar 2021 14:03:20 +0800 Subject: [PATCH 026/563] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 93200cd4d..e24dd9c03 100755 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ Other documents: - [x] [Deprecated] Support bandwidth testing([CN][v1_CN_BandwidthTestTool], [EN][v1_EN_BandwidthTestTool]), please read [#1535][bug #1535]. - [x] [Deprecated] Support Adobe FMS/AMS token traverse([CN][v3_CN_DRM2], [EN][v3_EN_DRM2]) authentication, please read [#1535][bug #1535]. - [ ] Enhanced forwarding with vhost and variables, [#1342][bug #1342]. +- [ ] Support DVR to Cloud Storage, [#1193][bug #1193]. - [ ] Support transmux RTC to RTMP, [#2093][bug #2093]. - [ ] Support H.265 over RTMP and HLS, [#465][bug #465]. - [ ] Support IETF-QUIC for WebRTC Cluster, [#2091][bug #2091]. @@ -1856,6 +1857,7 @@ Winlin [bug #1342]: https://github.com/ossrs/srs/issues/1342 [bug #2093]: https://github.com/ossrs/srs/issues/2093 [bug #2188]: https://github.com/ossrs/srs/issues/2188 +[bug #1193]: https://github.com/ossrs/srs/issues/1193 [bug #yyyyyyyyyyyyy]: https://github.com/ossrs/srs/issues/yyyyyyyyyyyyy [bug #1631]: https://github.com/ossrs/srs/issues/1631 From bb37a5550c9418c300e31996430cea87caa3d271 Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 23 Mar 2021 12:10:40 +0800 Subject: [PATCH 027/563] Test: Update srs-bench --- trunk/3rdparty/srs-bench/LICENSE | 2 +- trunk/3rdparty/srs-bench/README.md | 6 + trunk/3rdparty/srs-bench/auto/sync_vnet.sh | 33 + trunk/3rdparty/srs-bench/main.go | 2 +- trunk/3rdparty/srs-bench/srs/ingester.go | 11 +- trunk/3rdparty/srs-bench/srs/interceptor.go | 23 +- trunk/3rdparty/srs-bench/srs/player.go | 9 +- trunk/3rdparty/srs-bench/srs/publisher.go | 9 +- trunk/3rdparty/srs-bench/srs/rtc_test.go | 527 +++++++++---- trunk/3rdparty/srs-bench/srs/stat.go | 5 +- trunk/3rdparty/srs-bench/srs/util.go | 711 ++++++++++++++++- trunk/3rdparty/srs-bench/srs/util_test.go | 723 ------------------ .../srs-bench/vnet/example_udpproxy_test.go | 2 +- trunk/3rdparty/srs-bench/vnet/udpproxy.go | 199 +++-- .../srs-bench/vnet/udpproxy_direct.go | 2 +- .../srs-bench/vnet/udpproxy_direct_test.go | 2 +- .../3rdparty/srs-bench/vnet/udpproxy_test.go | 35 +- trunk/3rdparty/srs-bench/vnet/vnet.go | 38 + trunk/3rdparty/st-srs/.gitignore | 3 + 19 files changed, 1277 insertions(+), 1065 deletions(-) create mode 100755 trunk/3rdparty/srs-bench/auto/sync_vnet.sh delete mode 100644 trunk/3rdparty/srs-bench/srs/util_test.go create mode 100644 trunk/3rdparty/srs-bench/vnet/vnet.go diff --git a/trunk/3rdparty/srs-bench/LICENSE b/trunk/3rdparty/srs-bench/LICENSE index 77ba5769d..1cdf14566 100644 --- a/trunk/3rdparty/srs-bench/LICENSE +++ b/trunk/3rdparty/srs-bench/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 srs-bench(ossrs) +Copyright (c) 2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/3rdparty/srs-bench/README.md b/trunk/3rdparty/srs-bench/README.md index 415cb195f..6b903692c 100644 --- a/trunk/3rdparty/srs-bench/README.md +++ b/trunk/3rdparty/srs-bench/README.md @@ -142,6 +142,12 @@ go test ./srs -mod=vendor -v -srs-server=127.0.0.1 make && ./objs/srs_test -test.v -srs-server=127.0.0.1 ``` +可以只运行某个用例,并打印详细日志,比如: + +```bash +make && ./objs/srs_test -test.v -srs-log -test.run TestRtcBasic_PublishPlay +``` + 支持的参数如下: * `-srs-server`,RTC服务器地址。默认值:`127.0.0.1` diff --git a/trunk/3rdparty/srs-bench/auto/sync_vnet.sh b/trunk/3rdparty/srs-bench/auto/sync_vnet.sh new file mode 100755 index 000000000..55ef15f1a --- /dev/null +++ b/trunk/3rdparty/srs-bench/auto/sync_vnet.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +FILES=(udpproxy.go udpproxy_test.go) +for file in ${FILES[@]}; do + echo "cp vnet/udpproxy.go ~/git/transport/vnet/" && + cp vnet/udpproxy.go ~/git/transport/vnet/ +done + +# https://github.com/pion/webrtc/wiki/Contributing#run-all-automated-tests-and-checks-before-submitting +cd ~/git/transport/ + +echo ".github/lint-commit-message.sh" && +.github/lint-commit-message.sh && +echo ".github/assert-contributors.sh" && +.github/assert-contributors.sh && +echo ".github/lint-disallowed-functions-in-library.sh" && +.github/lint-disallowed-functions-in-library.sh && +echo ".github/lint-filename.sh" && +.github/lint-filename.sh +if [[ $? -ne 0 ]]; then echo "fail"; exit -1; fi + +# https://github.com/pion/webrtc/wiki/Contributing#run-all-automated-tests-and-checks-before-submitting +cd ~/git/transport/vnet/ + +echo "go test -race ./..." && +go test -race ./... +if [[ $? -ne 0 ]]; then echo "fail"; exit -1; fi + +echo "golangci-lint run --skip-files conn_map_test.go" && +golangci-lint run --skip-files conn_map_test.go +if [[ $? -ne 0 ]]; then echo "fail"; exit -1; fi + +echo "OK" diff --git a/trunk/3rdparty/srs-bench/main.go b/trunk/3rdparty/srs-bench/main.go index d56fa4995..8b83c4dcb 100644 --- a/trunk/3rdparty/srs-bench/main.go +++ b/trunk/3rdparty/srs-bench/main.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/3rdparty/srs-bench/srs/ingester.go b/trunk/3rdparty/srs-bench/srs/ingester.go index 1e3161a89..f38409e59 100644 --- a/trunk/3rdparty/srs-bench/srs/ingester.go +++ b/trunk/3rdparty/srs-bench/srs/ingester.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -22,6 +22,11 @@ package srs import ( "context" + "io" + "os" + "strings" + "time" + "github.com/ossrs/go-oryx-lib/errors" "github.com/ossrs/go-oryx-lib/logger" "github.com/pion/interceptor" @@ -31,10 +36,6 @@ import ( "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/media/h264reader" "github.com/pion/webrtc/v3/pkg/media/oggreader" - "io" - "os" - "strings" - "time" ) type videoIngester struct { diff --git a/trunk/3rdparty/srs-bench/srs/interceptor.go b/trunk/3rdparty/srs-bench/srs/interceptor.go index d853aaf7d..9757b705c 100644 --- a/trunk/3rdparty/srs-bench/srs/interceptor.go +++ b/trunk/3rdparty/srs-bench/srs/interceptor.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -31,14 +31,13 @@ type RTPInterceptorOptionFunc func(i *RTPInterceptor) // Common RTP packet interceptor for benchmark. // @remark Should never merge with RTCPInterceptor, because they has the same Write interface. type RTPInterceptor struct { - localInfo *interceptor.StreamInfo - remoteInfo *interceptor.StreamInfo // If rtpReader is nil, use the default next one to read. rtpReader interceptor.RTPReaderFunc nextRTPReader interceptor.RTPReader // If rtpWriter is nil, use the default next one to write. rtpWriter interceptor.RTPWriterFunc nextRTPWriter interceptor.RTPWriter + // Other common fields. BypassInterceptor } @@ -51,11 +50,6 @@ func NewRTPInterceptor(options ...RTPInterceptorOptionFunc) *RTPInterceptor { } func (v *RTPInterceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { - if v.localInfo != nil { - return writer // Only handle one stream. - } - - v.localInfo = info v.nextRTPWriter = writer return v // Handle all RTP } @@ -68,17 +62,9 @@ func (v *RTPInterceptor) Write(header *rtp.Header, payload []byte, attributes in } func (v *RTPInterceptor) UnbindLocalStream(info *interceptor.StreamInfo) { - if v.localInfo == nil || v.localInfo.ID != info.ID { - return - } - v.localInfo = nil // Reset the interceptor. } func (v *RTPInterceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { - if v.remoteInfo != nil { - return reader // Only handle one stream. - } - v.nextRTPReader = reader return v // Handle all RTP } @@ -91,10 +77,6 @@ func (v *RTPInterceptor) Read(b []byte, a interceptor.Attributes) (int, intercep } func (v *RTPInterceptor) UnbindRemoteStream(info *interceptor.StreamInfo) { - if v.remoteInfo == nil || v.remoteInfo.ID != info.ID { - return - } - v.remoteInfo = nil } type RTCPInterceptorOptionFunc func(i *RTCPInterceptor) @@ -108,6 +90,7 @@ type RTCPInterceptor struct { // If rtcpWriter is nil, use the default next one to write. rtcpWriter interceptor.RTCPWriterFunc nextRTCPWriter interceptor.RTCPWriter + // Other common fields. BypassInterceptor } diff --git a/trunk/3rdparty/srs-bench/srs/player.go b/trunk/3rdparty/srs-bench/srs/player.go index 0947ad41c..8dc91d030 100644 --- a/trunk/3rdparty/srs-bench/srs/player.go +++ b/trunk/3rdparty/srs-bench/srs/player.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -23,6 +23,10 @@ package srs import ( "context" "fmt" + "strings" + "sync" + "time" + "github.com/ossrs/go-oryx-lib/errors" "github.com/ossrs/go-oryx-lib/logger" "github.com/pion/interceptor" @@ -33,9 +37,6 @@ import ( "github.com/pion/webrtc/v3/pkg/media/h264writer" "github.com/pion/webrtc/v3/pkg/media/ivfwriter" "github.com/pion/webrtc/v3/pkg/media/oggwriter" - "strings" - "sync" - "time" ) // @see https://github.com/pion/webrtc/blob/master/examples/save-to-disk/main.go diff --git a/trunk/3rdparty/srs-bench/srs/publisher.go b/trunk/3rdparty/srs-bench/srs/publisher.go index 8d38fb055..49abab72a 100644 --- a/trunk/3rdparty/srs-bench/srs/publisher.go +++ b/trunk/3rdparty/srs-bench/srs/publisher.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -22,14 +22,15 @@ package srs import ( "context" + "io" + "sync" + "time" + "github.com/ossrs/go-oryx-lib/errors" "github.com/ossrs/go-oryx-lib/logger" "github.com/pion/interceptor" "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" - "io" - "sync" - "time" ) // @see https://github.com/pion/webrtc/blob/master/examples/play-from-disk/main.go diff --git a/trunk/3rdparty/srs-bench/srs/rtc_test.go b/trunk/3rdparty/srs-bench/srs/rtc_test.go index 4ac869c42..62ad26663 100644 --- a/trunk/3rdparty/srs-bench/srs/rtc_test.go +++ b/trunk/3rdparty/srs-bench/srs/rtc_test.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -23,19 +23,39 @@ package srs import ( "context" "fmt" - "github.com/ossrs/go-oryx-lib/errors" - "github.com/ossrs/go-oryx-lib/logger" - "github.com/pion/interceptor" - "github.com/pion/rtcp" - "github.com/pion/rtp" "github.com/pion/transport/vnet" + "io" + "io/ioutil" "math/rand" "os" "sync" "testing" "time" + + "github.com/ossrs/go-oryx-lib/errors" + "github.com/ossrs/go-oryx-lib/logger" + "github.com/pion/interceptor" + "github.com/pion/rtcp" + "github.com/pion/rtp" ) +func TestMain(m *testing.M) { + if err := prepareTest(); err != nil { + logger.Ef(nil, "Prepare test fail, err %+v", err) + os.Exit(-1) + } + + // Disable the logger during all tests. + if *srsLog == false { + olw := logger.Switch(ioutil.Discard) + defer func() { + logger.Switch(olw) + }() + } + + os.Exit(m.Run()) +} + // Basic use scenario, publish a stream, then play it. func TestRtcBasic_PublishPlay(t *testing.T) { ctx := logger.WithContext(context.Background()) @@ -50,12 +70,20 @@ func TestRtcBasic_PublishPlay(t *testing.T) { } }(ctx) + var resources []io.Closer + defer func() { + for _, resource := range resources { + resource.Close() + } + }() + var wg sync.WaitGroup defer wg.Wait() // The event notify. var thePublisher *TestPublisher var thePlayer *TestPlayer + mainReady, mainReadyCancel := context.WithCancel(context.Background()) publishReady, publishReadyCancel := context.WithCancel(context.Background()) @@ -66,76 +94,110 @@ func TestRtcBasic_PublishPlay(t *testing.T) { defer cancel() doInit := func() error { - playOK := *srsPlayOKPackets - vnetClientIP := *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - + playOK, vnetClientIP := *srsPlayOKPackets, *srsVnetClientIP streamSuffix := fmt.Sprintf("basic-publish-play-%v-%v", os.Getpid(), rand.Int()) - play := NewTestPlayer(api, func(play *TestPlayer) { + + // Initialize player with private api. + if play, err := NewTestPlayer(nil, func(play *TestPlayer) error { play.streamSuffix = streamSuffix - }) - defer play.Close() + resources = append(resources, play) - pub := NewTestPublisher(api, func(pub *TestPublisher) { - pub.streamSuffix = streamSuffix - pub.iceReadyCancel = publishReadyCancel - }) - defer pub.Close() + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + resources = append(resources, api) + play.api = api - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nnWriteRTP, nnReadRTP, nnWriteRTCP, nnReadRTCP int64 - api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { - i.rtpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - nn, attr, err := i.nextRTPReader.Read(buf, attributes) - nnReadRTP++ - return nn, attr, err - } - i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - nn, err := i.nextRTPWriter.Write(header, payload, attributes) - - nnWriteRTP++ - logger.Tf(ctx, "publish rtp=(read:%v write:%v), rtcp=(read:%v write:%v) packets", - nnReadRTP, nnWriteRTP, nnReadRTCP, nnWriteRTCP) - return nn, err - } - })) - api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { - i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - nn, attr, err := i.nextRTCPReader.Read(buf, attributes) - nnReadRTCP++ - return nn, attr, err - } - i.rtcpWriter = func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { - nn, err := i.nextRTCPWriter.Write(pkts, attributes) - nnWriteRTCP++ - return nn, err - } - })) - }, func(api *TestWebRTCAPI) { - var nn uint64 - api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { - i.rtpReader = func(payload []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nn++; nn >= uint64(playOK) { - cancel() // Completed. + var nnPlayWriteRTCP, nnPlayReadRTCP, nnPlayWriteRTP, nnPlayReadRTP uint64 + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpReader = func(payload []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnPlayReadRTP++; nnPlayReadRTP >= uint64(playOK) { + cancel() // Completed. + } + logger.Tf(ctx, "Play rtp=(recv:%v, send:%v), rtcp=(recv:%v send:%v) packets", + nnPlayReadRTP, nnPlayWriteRTP, nnPlayReadRTCP, nnPlayWriteRTCP) + return i.nextRTPReader.Read(payload, attributes) } - logger.Tf(ctx, "play got %v packets", nn) - return i.nextRTPReader.Read(payload, attributes) - } - })) + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + nn, attr, err := i.nextRTCPReader.Read(buf, attributes) + nnPlayReadRTCP++ + return nn, attr, err + } + i.rtcpWriter = func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { + nn, err := i.nextRTCPWriter.Write(pkts, attributes) + nnPlayWriteRTCP++ + return nn, err + } + })) + }); err != nil { + return err + } + + return nil }); err != nil { return err + } else { + thePlayer = play } - // Set the available objects. + // Initialize publisher with private api. + if pub, err := NewTestPublisher(nil, func(pub *TestPublisher) error { + pub.streamSuffix = streamSuffix + pub.iceReadyCancel = publishReadyCancel + resources = append(resources, pub) + + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + resources = append(resources, api) + pub.api = api + + var nnPubWriteRTCP, nnPubReadRTCP, nnPubWriteRTP, nnPubReadRTP uint64 + if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + nn, attr, err := i.nextRTPReader.Read(buf, attributes) + nnPubReadRTP++ + return nn, attr, err + } + i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { + nn, err := i.nextRTPWriter.Write(header, payload, attributes) + nnPubWriteRTP++ + logger.Tf(ctx, "Publish rtp=(recv:%v, send:%v), rtcp=(recv:%v send:%v) packets", + nnPubReadRTP, nnPubWriteRTP, nnPubReadRTCP, nnPubWriteRTCP) + return nn, err + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + nn, attr, err := i.nextRTCPReader.Read(buf, attributes) + nnPubReadRTCP++ + return nn, attr, err + } + i.rtcpWriter = func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { + nn, err := i.nextRTCPWriter.Write(pkts, attributes) + nnPubWriteRTCP++ + return nn, err + } + })) + }); err != nil { + return err + } + + return nil + }); err != nil { + return err + } else { + thePublisher = pub + } + + // Init done. mainReadyCancel() - thePublisher = pub - thePlayer = play <-ctx.Done() return nil @@ -158,17 +220,10 @@ func TestRtcBasic_PublishPlay(t *testing.T) { case <-mainReady.Done(): } - doPublish := func() error { - if err := thePublisher.Run(logger.WithContext(ctx), cancel); err != nil { - return err - } - - logger.Tf(ctx, "pub done") - return nil - } - if err := doPublish(); err != nil { + if err := thePublisher.Run(logger.WithContext(ctx), cancel); err != nil { r2 = err } + logger.Tf(ctx, "pub done") }() // Run player. @@ -177,30 +232,16 @@ func TestRtcBasic_PublishPlay(t *testing.T) { defer wg.Done() defer cancel() - select { - case <-ctx.Done(): - return - case <-mainReady.Done(): - } - select { case <-ctx.Done(): return case <-publishReady.Done(): } - doPlay := func() error { - if err := thePlayer.Run(logger.WithContext(ctx), cancel); err != nil { - return err - } - - logger.Tf(ctx, "play done") - return nil - } - if err := doPlay(); err != nil { + if err := thePlayer.Run(logger.WithContext(ctx), cancel); err != nil { r3 = err } - + logger.Tf(ctx, "play done") }() } @@ -222,21 +263,31 @@ func TestRtcDTLS_ClientActive_Default(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -276,21 +327,31 @@ func TestRtcDTLS_ClientPassive_Default(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-active-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -327,21 +388,31 @@ func TestRtcDTLS_ClientActive_Duplicated_Alert(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-active-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -385,21 +456,31 @@ func TestRtcDTLS_ClientPassive_Duplicated_Alert(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-active-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -450,21 +531,31 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ClientHello(t *testing.T defer api.Close() streamSuffix := fmt.Sprintf("dtls-active-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -527,21 +618,31 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ClientHello(t *testing. defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -603,21 +704,31 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ServerHello(t *testing.T defer api.Close() streamSuffix := fmt.Sprintf("dtls-active-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -690,21 +801,31 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ServerHello(t *testing. defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -774,21 +895,31 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_Certificate(t *testing.T defer api.Close() streamSuffix := fmt.Sprintf("dtls-active-arq-certificate-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -850,21 +981,31 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_Certificate(t *testing. defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-arq-certificate-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -926,21 +1067,31 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *test defer api.Close() streamSuffix := fmt.Sprintf("dtls-active-arq-certificate-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -1011,21 +1162,31 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *tes defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-arq-certificate-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -1087,10 +1248,14 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ClientHello(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { @@ -1145,10 +1310,14 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ServerHello(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { @@ -1203,10 +1372,14 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_Certificate(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { @@ -1261,10 +1434,14 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ChangeCipherSpec(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { @@ -1320,21 +1497,31 @@ func TestRtcDTLS_ClientPassive_ARQ_VeryBadNetwork(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { @@ -1397,21 +1584,31 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_After_ClientHello(t *testing.T) { defer api.Close() streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p := NewTestPublisher(api, func(p *TestPublisher) { + p, err := NewTestPublisher(api, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive + return nil }) + if err != nil { + return err + } defer p.Close() if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { - var nn int64 + var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { - if nn++; nn >= int64(publishOK) { + nnRTP++ + return i.nextRTPWriter.Write(header, payload, attributes) + } + })) + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { cancel() // Send enough packets, done. } - logger.Tf(ctx, "publish write %v packets", nn) - return i.nextRTPWriter.Write(header, payload, attributes) + logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) + return i.nextRTCPReader.Read(buf, attributes) } })) }, func(api *TestWebRTCAPI) { diff --git a/trunk/3rdparty/srs-bench/srs/stat.go b/trunk/3rdparty/srs-bench/srs/stat.go index 3ca7ed79a..ef83fe78d 100644 --- a/trunk/3rdparty/srs-bench/srs/stat.go +++ b/trunk/3rdparty/srs-bench/srs/stat.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -23,9 +23,10 @@ package srs import ( "context" "encoding/json" - "github.com/ossrs/go-oryx-lib/logger" "net/http" "strings" + + "github.com/ossrs/go-oryx-lib/logger" ) type statRTC struct { diff --git a/trunk/3rdparty/srs-bench/srs/util.go b/trunk/3rdparty/srs-bench/srs/util.go index 8c5a5434d..941b0bb76 100644 --- a/trunk/3rdparty/srs-bench/srs/util.go +++ b/trunk/3rdparty/srs-bench/srs/util.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -24,21 +24,113 @@ import ( "bytes" "context" "encoding/json" + "flag" "fmt" - "github.com/ossrs/go-oryx-lib/errors" - "github.com/ossrs/go-oryx-lib/logger" - "github.com/pion/transport/vnet" - "github.com/pion/webrtc/v3" - "github.com/pion/webrtc/v3/pkg/media/h264reader" + "io" "io/ioutil" "net" "net/http" "net/url" + "os" + "path" "strconv" "strings" + "sync" + "testing" "time" + + "github.com/ossrs/go-oryx-lib/errors" + "github.com/ossrs/go-oryx-lib/logger" + vnet_proxy "github.com/ossrs/srs-bench/vnet" + "github.com/pion/interceptor" + "github.com/pion/logging" + "github.com/pion/rtcp" + "github.com/pion/transport/vnet" + "github.com/pion/webrtc/v3" + "github.com/pion/webrtc/v3/pkg/media/h264reader" ) +var srsHttps *bool +var srsLog *bool + +var srsTimeout *int +var srsPlayPLI *int +var srsPlayOKPackets *int +var srsPublishOKPackets *int +var srsPublishVideoFps *int +var srsDTLSDropPackets *int + +var srsSchema string +var srsServer *string +var srsStream *string +var srsPublishAudio *string +var srsPublishVideo *string +var srsVnetClientIP *string + +func prepareTest() error { + var err error + + srsHttps = flag.Bool("srs-https", false, "Whther connect to HTTPS-API") + srsServer = flag.String("srs-server", "127.0.0.1", "The RTC server to connect to") + srsStream = flag.String("srs-stream", "/rtc/regression", "The RTC stream to play") + srsLog = flag.Bool("srs-log", false, "Whether enable the detail log") + srsTimeout = flag.Int("srs-timeout", 5000, "For each case, the timeout in ms") + srsPlayPLI = flag.Int("srs-play-pli", 5000, "The PLI interval in seconds for player.") + srsPlayOKPackets = flag.Int("srs-play-ok-packets", 10, "If recv N RTP packets, it's ok, or fail") + srsPublishOKPackets = flag.Int("srs-publish-ok-packets", 3, "If send N RTP, recv N RTCP packets, it's ok, or fail") + srsPublishAudio = flag.String("srs-publish-audio", "avatar.ogg", "The audio file for publisher.") + srsPublishVideo = flag.String("srs-publish-video", "avatar.h264", "The video file for publisher.") + srsPublishVideoFps = flag.Int("srs-publish-video-fps", 25, "The video fps for publisher.") + srsVnetClientIP = flag.String("srs-vnet-client-ip", "192.168.168.168", "The client ip in pion/vnet.") + srsDTLSDropPackets = flag.Int("srs-dtls-drop-packets", 5, "If dropped N packets, it's ok, or fail") + + // Should parse it first. + flag.Parse() + + // The stream should starts with /, for example, /rtc/regression + if !strings.HasPrefix(*srsStream, "/") { + *srsStream = "/" + *srsStream + } + + // Generate srs protocol from whether use HTTPS. + srsSchema = "http" + if *srsHttps { + srsSchema = "https" + } + + // Check file. + tryOpenFile := func(filename string) (string, error) { + if filename == "" { + return filename, nil + } + + f, err := os.Open(filename) + if err != nil { + nfilename := path.Join("../", filename) + f2, err := os.Open(nfilename) + if err != nil { + return filename, errors.Wrapf(err, "No video file at %v or %v", filename, nfilename) + } + defer f2.Close() + + return nfilename, nil + } + defer f.Close() + + return filename, nil + } + + if *srsPublishVideo, err = tryOpenFile(*srsPublishVideo); err != nil { + return err + } + + if *srsPublishAudio, err = tryOpenFile(*srsPublishAudio); err != nil { + return err + } + + return nil +} + func apiRtcRequest(ctx context.Context, apiPath, r, offer string) (string, error) { u, err := url.Parse(r) if err != nil { @@ -367,7 +459,11 @@ type ChunkMessageType struct { func (v *ChunkMessageType) String() string { if v.chunk == ChunkTypeDTLS { - return fmt.Sprintf("%v-%v-%v", v.chunk, v.content, v.handshake) + if v.content == DTLSContentTypeHandshake { + return fmt.Sprintf("%v-%v-%v", v.chunk, v.content, v.handshake) + } else { + return fmt.Sprintf("%v-%v", v.chunk, v.content) + } } return fmt.Sprintf("%v", v.chunk) } @@ -466,3 +562,604 @@ func (v *DTLSRecord) Unmarshal(b []byte) error { v.Data = b[13:] return nil } + +type TestWebRTCAPIOptionFunc func(api *TestWebRTCAPI) + +type TestWebRTCAPI struct { + // The options to setup the api. + options []TestWebRTCAPIOptionFunc + // The api and settings. + api *webrtc.API + mediaEngine *webrtc.MediaEngine + registry *interceptor.Registry + settingEngine *webrtc.SettingEngine + // The vnet router, can be shared by different apis, but we do not share it. + router *vnet.Router + // The network for api. + network *vnet.Net + // The vnet UDP proxy bind to the router. + proxy *vnet_proxy.UDPProxy +} + +func NewTestWebRTCAPI(options ...TestWebRTCAPIOptionFunc) (*TestWebRTCAPI, error) { + v := &TestWebRTCAPI{} + + v.mediaEngine = &webrtc.MediaEngine{} + if err := v.mediaEngine.RegisterDefaultCodecs(); err != nil { + return nil, err + } + + v.registry = &interceptor.Registry{} + if err := webrtc.RegisterDefaultInterceptors(v.mediaEngine, v.registry); err != nil { + return nil, err + } + + for _, setup := range options { + setup(v) + } + + v.settingEngine = &webrtc.SettingEngine{} + + return v, nil +} + +func (v *TestWebRTCAPI) Close() error { + if v.proxy != nil { + v.proxy.Close() + } + + if v.router != nil { + v.router.Stop() + } + + return nil +} + +func (v *TestWebRTCAPI) Setup(vnetClientIP string, options ...TestWebRTCAPIOptionFunc) error { + // Setting engine for https://github.com/pion/transport/tree/master/vnet + setupVnet := func(vnetClientIP string) (err error) { + // We create a private router for a api, however, it's possible to share the + // same router between apis. + if v.router, err = vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", // Accept all ip, no sub router. + LoggerFactory: logging.NewDefaultLoggerFactory(), + }); err != nil { + return errors.Wrapf(err, "create router for api") + } + + // Each api should bind to a network, however, it's possible to share it + // for different apis. + v.network = vnet.NewNet(&vnet.NetConfig{ + StaticIP: vnetClientIP, + }) + + if err = v.router.AddNet(v.network); err != nil { + return errors.Wrapf(err, "create network for api") + } + + v.settingEngine.SetVNet(v.network) + + // Create a proxy bind to the router. + if v.proxy, err = vnet_proxy.NewProxy(v.router); err != nil { + return errors.Wrapf(err, "create proxy for router") + } + + return v.router.Start() + } + if err := setupVnet(vnetClientIP); err != nil { + return err + } + + for _, setup := range options { + setup(v) + } + + for _, setup := range v.options { + setup(v) + } + + v.api = webrtc.NewAPI( + webrtc.WithMediaEngine(v.mediaEngine), + webrtc.WithInterceptorRegistry(v.registry), + webrtc.WithSettingEngine(*v.settingEngine), + ) + + return nil +} + +func (v *TestWebRTCAPI) NewPeerConnection(configuration webrtc.Configuration) (*webrtc.PeerConnection, error) { + return v.api.NewPeerConnection(configuration) +} + +type TestPlayerOptionFunc func(p *TestPlayer) error + +type TestPlayer struct { + pc *webrtc.PeerConnection + receivers []*webrtc.RTPReceiver + // root api object + api *TestWebRTCAPI + // Optional suffix for stream url. + streamSuffix string +} + +func NewTestPlayer(api *TestWebRTCAPI, options ...TestPlayerOptionFunc) (*TestPlayer, error) { + v := &TestPlayer{api: api} + + for _, opt := range options { + if err := opt(v); err != nil { + return nil, err + } + } + + // The api might be override by options. + api = v.api + + return v, nil +} + +func (v *TestPlayer) Close() error { + if v.pc != nil { + v.pc.Close() + } + + for _, receiver := range v.receivers { + receiver.Stop() + } + + return nil +} + +func (v *TestPlayer) Run(ctx context.Context, cancel context.CancelFunc) error { + r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream) + if v.streamSuffix != "" { + r = fmt.Sprintf("%v-%v", r, v.streamSuffix) + } + pli := time.Duration(*srsPlayPLI) * time.Millisecond + logger.Tf(ctx, "Start play url=%v", r) + + pc, err := v.api.NewPeerConnection(webrtc.Configuration{}) + if err != nil { + return errors.Wrapf(err, "Create PC") + } + v.pc = pc + + pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{ + Direction: webrtc.RTPTransceiverDirectionRecvonly, + }) + pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{ + Direction: webrtc.RTPTransceiverDirectionRecvonly, + }) + + offer, err := pc.CreateOffer(nil) + if err != nil { + return errors.Wrapf(err, "Create Offer") + } + + if err := pc.SetLocalDescription(offer); err != nil { + return errors.Wrapf(err, "Set offer %v", offer) + } + + answer, err := apiRtcRequest(ctx, "/rtc/v1/play", r, offer.SDP) + if err != nil { + return errors.Wrapf(err, "Api request offer=%v", offer.SDP) + } + + // Start a proxy for real server and vnet. + if address, err := parseAddressOfCandidate(answer); err != nil { + return errors.Wrapf(err, "parse address of %v", answer) + } else if err := v.api.proxy.Proxy(v.api.network, address); err != nil { + return errors.Wrapf(err, "proxy %v to %v", v.api.network, address) + } + + if err := pc.SetRemoteDescription(webrtc.SessionDescription{ + Type: webrtc.SDPTypeAnswer, SDP: answer, + }); err != nil { + return errors.Wrapf(err, "Set answer %v", answer) + } + + handleTrack := func(ctx context.Context, track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) error { + // Send a PLI on an interval so that the publisher is pushing a keyframe + go func() { + if track.Kind() == webrtc.RTPCodecTypeAudio { + return + } + + for { + select { + case <-ctx.Done(): + return + case <-time.After(pli): + _ = pc.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ + MediaSSRC: uint32(track.SSRC()), + }}) + } + } + }() + + v.receivers = append(v.receivers, receiver) + + for ctx.Err() == nil { + _, _, err := track.ReadRTP() + if err != nil { + return errors.Wrapf(err, "Read RTP") + } + } + + return nil + } + + pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { + err = handleTrack(ctx, track, receiver) + if err != nil { + codec := track.Codec() + err = errors.Wrapf(err, "Handle track %v, pt=%v", codec.MimeType, codec.PayloadType) + cancel() + } + }) + + pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { + if state == webrtc.ICEConnectionStateFailed || state == webrtc.ICEConnectionStateClosed { + err = errors.Errorf("Close for ICE state %v", state) + cancel() + } + }) + + <-ctx.Done() + return err +} + +type TestPublisherOptionFunc func(p *TestPublisher) error + +type TestPublisher struct { + onOffer func(s *webrtc.SessionDescription) error + onAnswer func(s *webrtc.SessionDescription) error + iceReadyCancel context.CancelFunc + // internal objects + aIngester *audioIngester + vIngester *videoIngester + pc *webrtc.PeerConnection + // root api object + api *TestWebRTCAPI + // Optional suffix for stream url. + streamSuffix string +} + +func NewTestPublisher(api *TestWebRTCAPI, options ...TestPublisherOptionFunc) (*TestPublisher, error) { + sourceVideo, sourceAudio := *srsPublishVideo, *srsPublishAudio + + v := &TestPublisher{api: api} + + for _, opt := range options { + if err := opt(v); err != nil { + return nil, err + } + } + + // The api might be override by options. + api = v.api + + // Create ingesters. + if sourceAudio != "" { + v.aIngester = NewAudioIngester(sourceAudio) + } + if sourceVideo != "" { + v.vIngester = NewVideoIngester(sourceVideo) + } + + // Setup the interceptors for packets. + api.options = append(api.options, func(api *TestWebRTCAPI) { + // Filter for RTCP packets. + rtcpInterceptor := &RTCPInterceptor{} + rtcpInterceptor.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + return rtcpInterceptor.nextRTCPReader.Read(buf, attributes) + } + rtcpInterceptor.rtcpWriter = func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { + return rtcpInterceptor.nextRTCPWriter.Write(pkts, attributes) + } + api.registry.Add(rtcpInterceptor) + + // Filter for ingesters. + if sourceAudio != "" { + api.registry.Add(v.aIngester.audioLevelInterceptor) + } + if sourceVideo != "" { + api.registry.Add(v.vIngester.markerInterceptor) + } + }) + + return v, nil +} + +func (v *TestPublisher) Close() error { + if v.vIngester != nil { + v.vIngester.Close() + } + + if v.aIngester != nil { + v.aIngester.Close() + } + + if v.pc != nil { + v.pc.Close() + } + + return nil +} + +func (v *TestPublisher) SetStreamSuffix(suffix string) *TestPublisher { + v.streamSuffix = suffix + return v +} + +func (v *TestPublisher) Run(ctx context.Context, cancel context.CancelFunc) error { + r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream) + if v.streamSuffix != "" { + r = fmt.Sprintf("%v-%v", r, v.streamSuffix) + } + sourceVideo, sourceAudio, fps := *srsPublishVideo, *srsPublishAudio, *srsPublishVideoFps + + logger.Tf(ctx, "Start publish url=%v, audio=%v, video=%v, fps=%v", + r, sourceAudio, sourceVideo, fps) + + pc, err := v.api.NewPeerConnection(webrtc.Configuration{}) + if err != nil { + return errors.Wrapf(err, "Create PC") + } + v.pc = pc + + if v.vIngester != nil { + if err := v.vIngester.AddTrack(pc, fps); err != nil { + return errors.Wrapf(err, "Add track") + } + } + + if v.aIngester != nil { + if err := v.aIngester.AddTrack(pc); err != nil { + return errors.Wrapf(err, "Add track") + } + } + + offer, err := pc.CreateOffer(nil) + if err != nil { + return errors.Wrapf(err, "Create Offer") + } + + if err := pc.SetLocalDescription(offer); err != nil { + return errors.Wrapf(err, "Set offer %v", offer) + } + + if v.onOffer != nil { + if err := v.onOffer(&offer); err != nil { + return errors.Wrapf(err, "sdp %v %v", offer.Type, offer.SDP) + } + } + + answerSDP, err := apiRtcRequest(ctx, "/rtc/v1/publish", r, offer.SDP) + if err != nil { + return errors.Wrapf(err, "Api request offer=%v", offer.SDP) + } + + // Start a proxy for real server and vnet. + if address, err := parseAddressOfCandidate(answerSDP); err != nil { + return errors.Wrapf(err, "parse address of %v", answerSDP) + } else if err := v.api.proxy.Proxy(v.api.network, address); err != nil { + return errors.Wrapf(err, "proxy %v to %v", v.api.network, address) + } + + answer := &webrtc.SessionDescription{ + Type: webrtc.SDPTypeAnswer, SDP: answerSDP, + } + if v.onAnswer != nil { + if err := v.onAnswer(answer); err != nil { + return errors.Wrapf(err, "on answerSDP") + } + } + + if err := pc.SetRemoteDescription(*answer); err != nil { + return errors.Wrapf(err, "Set answerSDP %v", answerSDP) + } + + logger.Tf(ctx, "State signaling=%v, ice=%v, conn=%v", pc.SignalingState(), pc.ICEConnectionState(), pc.ConnectionState()) + + // ICE state management. + pc.OnICEGatheringStateChange(func(state webrtc.ICEGathererState) { + logger.Tf(ctx, "ICE gather state %v", state) + }) + pc.OnICECandidate(func(candidate *webrtc.ICECandidate) { + logger.Tf(ctx, "ICE candidate %v %v:%v", candidate.Protocol, candidate.Address, candidate.Port) + + }) + pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { + logger.Tf(ctx, "ICE state %v", state) + }) + + pc.OnSignalingStateChange(func(state webrtc.SignalingState) { + logger.Tf(ctx, "Signaling state %v", state) + }) + + if v.aIngester != nil { + v.aIngester.sAudioSender.Transport().OnStateChange(func(state webrtc.DTLSTransportState) { + logger.Tf(ctx, "DTLS state %v", state) + }) + } + + pcDone, pcDoneCancel := context.WithCancel(context.Background()) + pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + logger.Tf(ctx, "PC state %v", state) + + if state == webrtc.PeerConnectionStateConnected { + pcDoneCancel() + if v.iceReadyCancel != nil { + v.iceReadyCancel() + } + } + + if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateClosed { + err = errors.Errorf("Close for PC state %v", state) + cancel() + } + }) + + // Wait for event from context or tracks. + var wg sync.WaitGroup + var finalErr error + + wg.Add(1) + go func() { + defer wg.Done() + defer logger.Tf(ctx, "ingest notify done") + + <-ctx.Done() + + if v.aIngester != nil && v.aIngester.sAudioSender != nil { + v.aIngester.sAudioSender.Stop() + } + + if v.vIngester != nil && v.vIngester.sVideoSender != nil { + v.vIngester.sVideoSender.Stop() + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + if v.aIngester == nil { + return + } + + select { + case <-ctx.Done(): + return + case <-pcDone.Done(): + } + + wg.Add(1) + go func() { + defer wg.Done() + defer logger.Tf(ctx, "aingester sender read done") + + buf := make([]byte, 1500) + for ctx.Err() == nil { + if _, _, err := v.aIngester.sAudioSender.Read(buf); err != nil { + return + } + } + }() + + for { + if err := v.aIngester.Ingest(ctx); err != nil { + if err == io.EOF { + logger.Tf(ctx, "aingester retry for %v", err) + continue + } + if err != context.Canceled { + finalErr = errors.Wrapf(err, "audio") + } + + logger.Tf(ctx, "aingester err=%v, final=%v", err, finalErr) + return + } + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + if v.vIngester == nil { + return + } + + select { + case <-ctx.Done(): + return + case <-pcDone.Done(): + logger.Tf(ctx, "PC(ICE+DTLS+SRTP) done, start ingest video %v", sourceVideo) + } + + wg.Add(1) + go func() { + defer wg.Done() + defer logger.Tf(ctx, "vingester sender read done") + + buf := make([]byte, 1500) + for ctx.Err() == nil { + // The Read() might block in r.rtcpInterceptor.Read(b, a), + // so that the Stop() can not stop it. + if _, _, err := v.vIngester.sVideoSender.Read(buf); err != nil { + return + } + } + }() + + for { + if err := v.vIngester.Ingest(ctx); err != nil { + if err == io.EOF { + logger.Tf(ctx, "vingester retry for %v", err) + continue + } + if err != context.Canceled { + finalErr = errors.Wrapf(err, "video") + } + + logger.Tf(ctx, "vingester err=%v, final=%v", err, finalErr) + return + } + } + }() + + wg.Wait() + + logger.Tf(ctx, "ingester done ctx=%v, final=%v", ctx.Err(), finalErr) + if finalErr != nil { + return finalErr + } + return ctx.Err() +} + +func TestRTCServerVersion(t *testing.T) { + api := fmt.Sprintf("http://%v:1985/api/v1/versions", *srsServer) + req, err := http.NewRequest("POST", api, nil) + if err != nil { + t.Errorf("Request %v", api) + return + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Errorf("Do request %v", api) + return + } + + b, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Errorf("Read body of %v", api) + return + } + + obj := struct { + Code int `json:"code"` + Server string `json:"server"` + Data struct { + Major int `json:"major"` + Minor int `json:"minor"` + Revision int `json:"revision"` + Version string `json:"version"` + } `json:"data"` + }{} + if err := json.Unmarshal(b, &obj); err != nil { + t.Errorf("Parse %v", string(b)) + return + } + if obj.Code != 0 { + t.Errorf("Server err code=%v, server=%v", obj.Code, obj.Server) + return + } + if obj.Data.Major == 0 && obj.Data.Minor == 0 { + t.Errorf("Invalid version %v", obj.Data) + return + } +} diff --git a/trunk/3rdparty/srs-bench/srs/util_test.go b/trunk/3rdparty/srs-bench/srs/util_test.go deleted file mode 100644 index 68187c9ef..000000000 --- a/trunk/3rdparty/srs-bench/srs/util_test.go +++ /dev/null @@ -1,723 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2021 srs-bench(ossrs) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -package srs - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "github.com/ossrs/go-oryx-lib/errors" - "github.com/ossrs/go-oryx-lib/logger" - vnet_proxy "github.com/ossrs/srs-bench/vnet" - "github.com/pion/interceptor" - "github.com/pion/logging" - "github.com/pion/rtcp" - "github.com/pion/transport/vnet" - "github.com/pion/webrtc/v3" - "io" - "io/ioutil" - "net/http" - "os" - "path" - "strings" - "sync" - "testing" - "time" -) - -var srsSchema = "http" -var srsHttps = flag.Bool("srs-https", false, "Whther connect to HTTPS-API") -var srsServer = flag.String("srs-server", "127.0.0.1", "The RTC server to connect to") -var srsStream = flag.String("srs-stream", "/rtc/regression", "The RTC stream to play") -var srsLog = flag.Bool("srs-log", false, "Whether enable the detail log") -var srsTimeout = flag.Int("srs-timeout", 5000, "For each case, the timeout in ms") -var srsPlayPLI = flag.Int("srs-play-pli", 5000, "The PLI interval in seconds for player.") -var srsPlayOKPackets = flag.Int("srs-play-ok-packets", 10, "If got N packets, it's ok, or fail") -var srsPublishOKPackets = flag.Int("srs-publish-ok-packets", 10, "If send N packets, it's ok, or fail") -var srsPublishAudio = flag.String("srs-publish-audio", "avatar.ogg", "The audio file for publisher.") -var srsPublishVideo = flag.String("srs-publish-video", "avatar.h264", "The video file for publisher.") -var srsPublishVideoFps = flag.Int("srs-publish-video-fps", 25, "The video fps for publisher.") -var srsVnetClientIP = flag.String("srs-vnet-client-ip", "192.168.168.168", "The client ip in pion/vnet.") -var srsDTLSDropPackets = flag.Int("srs-dtls-drop-packets", 5, "If dropped N packets, it's ok, or fail") - -func prepareTest() error { - var err error - - // Should parse it first. - flag.Parse() - - // The stream should starts with /, for example, /rtc/regression - if !strings.HasPrefix(*srsStream, "/") { - *srsStream = "/" + *srsStream - } - - // Generate srs protocol from whether use HTTPS. - if *srsHttps { - srsSchema = "https" - } - - // Check file. - tryOpenFile := func(filename string) (string, error) { - if filename == "" { - return filename, nil - } - - f, err := os.Open(filename) - if err != nil { - nfilename := path.Join("../", filename) - f2, err := os.Open(nfilename) - if err != nil { - return filename, errors.Wrapf(err, "No video file at %v or %v", filename, nfilename) - } - defer f2.Close() - - return nfilename, nil - } - defer f.Close() - - return filename, nil - } - - if *srsPublishVideo, err = tryOpenFile(*srsPublishVideo); err != nil { - return err - } - - if *srsPublishAudio, err = tryOpenFile(*srsPublishAudio); err != nil { - return err - } - - return nil -} - -func TestMain(m *testing.M) { - if err := prepareTest(); err != nil { - logger.Ef(nil, "Prepare test fail, err %+v", err) - os.Exit(-1) - } - - // Disable the logger during all tests. - if *srsLog == false { - olw := logger.Switch(ioutil.Discard) - defer func() { - logger.Switch(olw) - }() - } - - os.Exit(m.Run()) -} - -type TestWebRTCAPIOptionFunc func(api *TestWebRTCAPI) - -type TestWebRTCAPI struct { - // The options to setup the api. - options []TestWebRTCAPIOptionFunc - // The api and settings. - api *webrtc.API - mediaEngine *webrtc.MediaEngine - registry *interceptor.Registry - settingEngine *webrtc.SettingEngine - // The vnet router, can be shared by different apis, but we do not share it. - router *vnet.Router - // The network for api. - network *vnet.Net - // The vnet UDP proxy bind to the router. - proxy *vnet_proxy.UDPProxy -} - -func NewTestWebRTCAPI(options ...TestWebRTCAPIOptionFunc) (*TestWebRTCAPI, error) { - v := &TestWebRTCAPI{} - - v.mediaEngine = &webrtc.MediaEngine{} - if err := v.mediaEngine.RegisterDefaultCodecs(); err != nil { - return nil, err - } - - v.registry = &interceptor.Registry{} - if err := webrtc.RegisterDefaultInterceptors(v.mediaEngine, v.registry); err != nil { - return nil, err - } - - for _, setup := range options { - setup(v) - } - - v.settingEngine = &webrtc.SettingEngine{} - - return v, nil -} - -func (v *TestWebRTCAPI) Close() error { - if v.proxy != nil { - v.proxy.Close() - v.proxy = nil - } - - if v.router != nil { - v.router.Stop() - v.router = nil - } - - return nil -} - -func (v *TestWebRTCAPI) Setup(vnetClientIP string, options ...TestWebRTCAPIOptionFunc) error { - // Setting engine for https://github.com/pion/transport/tree/master/vnet - setupVnet := func(vnetClientIP string) (err error) { - // We create a private router for a api, however, it's possible to share the - // same router between apis. - if v.router, err = vnet.NewRouter(&vnet.RouterConfig{ - CIDR: "0.0.0.0/0", // Accept all ip, no sub router. - LoggerFactory: logging.NewDefaultLoggerFactory(), - }); err != nil { - return errors.Wrapf(err, "create router for api") - } - - // Each api should bind to a network, however, it's possible to share it - // for different apis. - v.network = vnet.NewNet(&vnet.NetConfig{ - StaticIP: vnetClientIP, - }) - - if err = v.router.AddNet(v.network); err != nil { - return errors.Wrapf(err, "create network for api") - } - - v.settingEngine.SetVNet(v.network) - - // Create a proxy bind to the router. - if v.proxy, err = vnet_proxy.NewProxy(v.router); err != nil { - return errors.Wrapf(err, "create proxy for router") - } - - return v.router.Start() - } - if err := setupVnet(vnetClientIP); err != nil { - return err - } - - for _, setup := range options { - setup(v) - } - - for _, setup := range v.options { - setup(v) - } - - v.api = webrtc.NewAPI( - webrtc.WithMediaEngine(v.mediaEngine), - webrtc.WithInterceptorRegistry(v.registry), - webrtc.WithSettingEngine(*v.settingEngine), - ) - - return nil -} - -func (v *TestWebRTCAPI) NewPeerConnection(configuration webrtc.Configuration) (*webrtc.PeerConnection, error) { - return v.api.NewPeerConnection(configuration) -} - -type TestPlayerOptionFunc func(p *TestPlayer) - -type TestPlayer struct { - pc *webrtc.PeerConnection - receivers []*webrtc.RTPReceiver - // root api object - api *TestWebRTCAPI - // Optional suffix for stream url. - streamSuffix string -} - -func NewTestPlayer(api *TestWebRTCAPI, options ...TestPlayerOptionFunc) *TestPlayer { - v := &TestPlayer{api: api} - - for _, opt := range options { - opt(v) - } - - return v -} - -func (v *TestPlayer) Close() error { - if v.pc != nil { - v.pc.Close() - v.pc = nil - } - - for _, receiver := range v.receivers { - receiver.Stop() - } - v.receivers = nil - - return nil -} - -func (v *TestPlayer) Run(ctx context.Context, cancel context.CancelFunc) error { - r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream) - if v.streamSuffix != "" { - r = fmt.Sprintf("%v-%v", r, v.streamSuffix) - } - pli := time.Duration(*srsPlayPLI) * time.Millisecond - logger.Tf(ctx, "Start play url=%v", r) - - pc, err := v.api.NewPeerConnection(webrtc.Configuration{}) - if err != nil { - return errors.Wrapf(err, "Create PC") - } - v.pc = pc - - pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{ - Direction: webrtc.RTPTransceiverDirectionRecvonly, - }) - pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{ - Direction: webrtc.RTPTransceiverDirectionRecvonly, - }) - - offer, err := pc.CreateOffer(nil) - if err != nil { - return errors.Wrapf(err, "Create Offer") - } - - if err := pc.SetLocalDescription(offer); err != nil { - return errors.Wrapf(err, "Set offer %v", offer) - } - - answer, err := apiRtcRequest(ctx, "/rtc/v1/play", r, offer.SDP) - if err != nil { - return errors.Wrapf(err, "Api request offer=%v", offer.SDP) - } - - // Start a proxy for real server and vnet. - if address, err := parseAddressOfCandidate(answer); err != nil { - return errors.Wrapf(err, "parse address of %v", answer) - } else if err := v.api.proxy.Proxy(v.api.network, address); err != nil { - return errors.Wrapf(err, "proxy %v to %v", v.api.network, address) - } - - if err := pc.SetRemoteDescription(webrtc.SessionDescription{ - Type: webrtc.SDPTypeAnswer, SDP: answer, - }); err != nil { - return errors.Wrapf(err, "Set answer %v", answer) - } - - handleTrack := func(ctx context.Context, track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) error { - // Send a PLI on an interval so that the publisher is pushing a keyframe - go func() { - if track.Kind() == webrtc.RTPCodecTypeAudio { - return - } - - for { - select { - case <-ctx.Done(): - return - case <-time.After(pli): - _ = pc.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ - MediaSSRC: uint32(track.SSRC()), - }}) - } - } - }() - - v.receivers = append(v.receivers, receiver) - - for ctx.Err() == nil { - _, _, err := track.ReadRTP() - if err != nil { - return errors.Wrapf(err, "Read RTP") - } - } - - return nil - } - - pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { - err = handleTrack(ctx, track, receiver) - if err != nil { - codec := track.Codec() - err = errors.Wrapf(err, "Handle track %v, pt=%v", codec.MimeType, codec.PayloadType) - cancel() - } - }) - - pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { - if state == webrtc.ICEConnectionStateFailed || state == webrtc.ICEConnectionStateClosed { - err = errors.Errorf("Close for ICE state %v", state) - cancel() - } - }) - - <-ctx.Done() - return err -} - -type TestPublisherOptionFunc func(p *TestPublisher) - -type TestPublisher struct { - onOffer func(s *webrtc.SessionDescription) error - onAnswer func(s *webrtc.SessionDescription) error - iceReadyCancel context.CancelFunc - // internal objects - aIngester *audioIngester - vIngester *videoIngester - pc *webrtc.PeerConnection - // root api object - api *TestWebRTCAPI - // Optional suffix for stream url. - streamSuffix string -} - -func NewTestPublisher(api *TestWebRTCAPI, options ...TestPublisherOptionFunc) *TestPublisher { - sourceVideo, sourceAudio := *srsPublishVideo, *srsPublishAudio - - v := &TestPublisher{api: api} - - for _, opt := range options { - opt(v) - } - - // Create ingesters. - if sourceAudio != "" { - v.aIngester = NewAudioIngester(sourceAudio) - } - if sourceVideo != "" { - v.vIngester = NewVideoIngester(sourceVideo) - } - - // Setup the interceptors for packets. - api.options = append(api.options, func(api *TestWebRTCAPI) { - // Filter for RTCP packets. - rtcpInterceptor := &RTCPInterceptor{} - rtcpInterceptor.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - return rtcpInterceptor.nextRTCPReader.Read(buf, attributes) - } - rtcpInterceptor.rtcpWriter = func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { - return rtcpInterceptor.nextRTCPWriter.Write(pkts, attributes) - } - api.registry.Add(rtcpInterceptor) - - // Filter for ingesters. - if sourceAudio != "" { - api.registry.Add(v.aIngester.audioLevelInterceptor) - } - if sourceVideo != "" { - api.registry.Add(v.vIngester.markerInterceptor) - } - }) - - return v -} - -func (v *TestPublisher) Close() error { - if v.vIngester != nil { - v.vIngester.Close() - } - - if v.aIngester != nil { - v.aIngester.Close() - } - - if v.pc != nil { - v.pc.Close() - } - - return nil -} - -func (v *TestPublisher) SetStreamSuffix(suffix string) *TestPublisher { - v.streamSuffix = suffix - return v -} - -func (v *TestPublisher) Run(ctx context.Context, cancel context.CancelFunc) error { - r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream) - if v.streamSuffix != "" { - r = fmt.Sprintf("%v-%v", r, v.streamSuffix) - } - sourceVideo, sourceAudio, fps := *srsPublishVideo, *srsPublishAudio, *srsPublishVideoFps - - logger.Tf(ctx, "Start publish url=%v, audio=%v, video=%v, fps=%v", - r, sourceAudio, sourceVideo, fps) - - pc, err := v.api.NewPeerConnection(webrtc.Configuration{}) - if err != nil { - return errors.Wrapf(err, "Create PC") - } - v.pc = pc - - if v.vIngester != nil { - if err := v.vIngester.AddTrack(pc, fps); err != nil { - return errors.Wrapf(err, "Add track") - } - defer v.vIngester.Close() - } - - if v.aIngester != nil { - if err := v.aIngester.AddTrack(pc); err != nil { - return errors.Wrapf(err, "Add track") - } - defer v.aIngester.Close() - } - - offer, err := pc.CreateOffer(nil) - if err != nil { - return errors.Wrapf(err, "Create Offer") - } - - if err := pc.SetLocalDescription(offer); err != nil { - return errors.Wrapf(err, "Set offer %v", offer) - } - - if v.onOffer != nil { - if err := v.onOffer(&offer); err != nil { - return errors.Wrapf(err, "sdp %v %v", offer.Type, offer.SDP) - } - } - - answerSDP, err := apiRtcRequest(ctx, "/rtc/v1/publish", r, offer.SDP) - if err != nil { - return errors.Wrapf(err, "Api request offer=%v", offer.SDP) - } - - // Start a proxy for real server and vnet. - if address, err := parseAddressOfCandidate(answerSDP); err != nil { - return errors.Wrapf(err, "parse address of %v", answerSDP) - } else if err := v.api.proxy.Proxy(v.api.network, address); err != nil { - return errors.Wrapf(err, "proxy %v to %v", v.api.network, address) - } - - answer := &webrtc.SessionDescription{ - Type: webrtc.SDPTypeAnswer, SDP: answerSDP, - } - if v.onAnswer != nil { - if err := v.onAnswer(answer); err != nil { - return errors.Wrapf(err, "on answerSDP") - } - } - - if err := pc.SetRemoteDescription(*answer); err != nil { - return errors.Wrapf(err, "Set answerSDP %v", answerSDP) - } - - logger.Tf(ctx, "State signaling=%v, ice=%v, conn=%v", pc.SignalingState(), pc.ICEConnectionState(), pc.ConnectionState()) - - // ICE state management. - pc.OnICEGatheringStateChange(func(state webrtc.ICEGathererState) { - logger.Tf(ctx, "ICE gather state %v", state) - }) - pc.OnICECandidate(func(candidate *webrtc.ICECandidate) { - logger.Tf(ctx, "ICE candidate %v %v:%v", candidate.Protocol, candidate.Address, candidate.Port) - - }) - pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { - logger.Tf(ctx, "ICE state %v", state) - }) - - pc.OnSignalingStateChange(func(state webrtc.SignalingState) { - logger.Tf(ctx, "Signaling state %v", state) - }) - - if v.aIngester != nil { - v.aIngester.sAudioSender.Transport().OnStateChange(func(state webrtc.DTLSTransportState) { - logger.Tf(ctx, "DTLS state %v", state) - }) - } - - pcDone, pcDoneCancel := context.WithCancel(context.Background()) - pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { - logger.Tf(ctx, "PC state %v", state) - - if state == webrtc.PeerConnectionStateConnected { - pcDoneCancel() - if v.iceReadyCancel != nil { - v.iceReadyCancel() - } - } - - if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateClosed { - err = errors.Errorf("Close for PC state %v", state) - cancel() - } - }) - - // Wait for event from context or tracks. - var wg sync.WaitGroup - var finalErr error - - wg.Add(1) - go func() { - defer wg.Done() - defer logger.Tf(ctx, "ingest notify done") - - <-ctx.Done() - - if v.aIngester != nil && v.aIngester.sAudioSender != nil { - v.aIngester.sAudioSender.Stop() - } - - if v.vIngester != nil && v.vIngester.sVideoSender != nil { - v.vIngester.sVideoSender.Stop() - } - }() - - wg.Add(1) - go func() { - defer wg.Done() - defer cancel() - - if v.aIngester == nil { - return - } - - select { - case <-ctx.Done(): - return - case <-pcDone.Done(): - } - - wg.Add(1) - go func() { - defer wg.Done() - defer logger.Tf(ctx, "aingester sender read done") - - buf := make([]byte, 1500) - for ctx.Err() == nil { - if _, _, err := v.aIngester.sAudioSender.Read(buf); err != nil { - return - } - } - }() - - for { - if err := v.aIngester.Ingest(ctx); err != nil { - if err == io.EOF { - logger.Tf(ctx, "aingester retry for %v", err) - continue - } - if err != context.Canceled { - finalErr = errors.Wrapf(err, "audio") - } - - logger.Tf(ctx, "aingester err=%v, final=%v", err, finalErr) - return - } - } - }() - - wg.Add(1) - go func() { - defer wg.Done() - defer cancel() - - if v.vIngester == nil { - return - } - - select { - case <-ctx.Done(): - return - case <-pcDone.Done(): - logger.Tf(ctx, "PC(ICE+DTLS+SRTP) done, start ingest video %v", sourceVideo) - } - - wg.Add(1) - go func() { - defer wg.Done() - defer logger.Tf(ctx, "vingester sender read done") - - buf := make([]byte, 1500) - for ctx.Err() == nil { - // The Read() might block in r.rtcpInterceptor.Read(b, a), - // so that the Stop() can not stop it. - if _, _, err := v.vIngester.sVideoSender.Read(buf); err != nil { - return - } - } - }() - - for { - if err := v.vIngester.Ingest(ctx); err != nil { - if err == io.EOF { - logger.Tf(ctx, "vingester retry for %v", err) - continue - } - if err != context.Canceled { - finalErr = errors.Wrapf(err, "video") - } - - logger.Tf(ctx, "vingester err=%v, final=%v", err, finalErr) - return - } - } - }() - - wg.Wait() - - logger.Tf(ctx, "ingester done ctx=%v, final=%v", ctx.Err(), finalErr) - if finalErr != nil { - return finalErr - } - return ctx.Err() -} - -func TestRTCServerVersion(t *testing.T) { - api := fmt.Sprintf("http://%v:1985/api/v1/versions", *srsServer) - req, err := http.NewRequest("POST", api, nil) - if err != nil { - t.Errorf("Request %v", api) - return - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - t.Errorf("Do request %v", api) - return - } - - b, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Errorf("Read body of %v", api) - return - } - - obj := struct { - Code int `json:"code"` - Server string `json:"server"` - Data struct { - Major int `json:"major"` - Minor int `json:"minor"` - Revision int `json:"revision"` - Version string `json:"version"` - } `json:"data"` - }{} - if err := json.Unmarshal(b, &obj); err != nil { - t.Errorf("Parse %v", string(b)) - return - } - if obj.Code != 0 { - t.Errorf("Server err code=%v, server=%v", obj.Code, obj.Server) - return - } - if obj.Data.Major == 0 && obj.Data.Minor == 0 { - t.Errorf("Invalid version %v", obj.Data) - return - } -} diff --git a/trunk/3rdparty/srs-bench/vnet/example_udpproxy_test.go b/trunk/3rdparty/srs-bench/vnet/example_udpproxy_test.go index d54e71653..0a8ebb55a 100644 --- a/trunk/3rdparty/srs-bench/vnet/example_udpproxy_test.go +++ b/trunk/3rdparty/srs-bench/vnet/example_udpproxy_test.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/3rdparty/srs-bench/vnet/udpproxy.go b/trunk/3rdparty/srs-bench/vnet/udpproxy.go index c21fe4603..caba86e0e 100644 --- a/trunk/3rdparty/srs-bench/vnet/udpproxy.go +++ b/trunk/3rdparty/srs-bench/vnet/udpproxy.go @@ -1,34 +1,13 @@ -// The MIT License (MIT) -// -// Copyright (c) 2021 srs-bench(ossrs) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package vnet import ( + "context" "net" "sync" "time" - - "github.com/pion/transport/vnet" ) -// A UDP proxy between real server(net.UDPConn) and vnet.UDPConn. +// UDPProxy is a proxy between real server(net.UDPConn) and vnet.UDPConn. // // High level design: // .............................................. @@ -44,32 +23,9 @@ import ( // : | | ............................: // : +--------+ : // ............................................... -// -// The whole big picture: -// ...................................... -// : Virtual Network (vnet) : -// : : -// +-------+ * 1 +----+ +--------+ : -// | :App |------------>|:Net|--o<-----|:Router | ............................. -// +-------+ +----+ | | : UDPProxy : -// +-----------+ * 1 +----+ | | +----+ +---------+ +---------+ +--------+ -// |:STUNServer|-------->|:Net|--o<-----| |--->o--|:Net|-->o-| vnet. |-->o-| net. |--->-| :Real | -// +-----------+ +----+ | | +----+ | UDPConn | | UDPConn | | Server | -// +-----------+ * 1 +----+ | | : +---------+ +---------+ +--------+ -// |:TURNServer|-------->|:Net|--o<-----| | ............................: -// +-----------+ +----+ [1] | | : -// : 1 | | 1 <> : -// : +---<>| |<>----+ [2] : -// : | +--------+ | : -// To form | *| v 0..1 : -// a subnet tree | o [3] +-----+ : -// : | ^ |:NAT | : -// : | | +-----+ : -// : +-------+ : -// ...................................... type UDPProxy struct { // The router bind to. - router *vnet.Router + router *Router // Each vnet source, bind to a real socket to server. // key is real server addr, which is net.Addr @@ -88,19 +44,22 @@ type UDPProxy struct { // NewProxy create a proxy, the router for this proxy belongs/bind to. If need to proxy for // please create a new proxy for each router. For all addresses we proxy, we will create a // vnet.Net in this router and proxy all packets. -func NewProxy(router *vnet.Router) (*UDPProxy, error) { +func NewProxy(router *Router) (*UDPProxy, error) { v := &UDPProxy{router: router, timeout: 2 * time.Minute} return v, nil } // Close the proxy, stop all workers. func (v *UDPProxy) Close() error { - // nolint:godox // TODO: FIXME: Do cleanup. + v.workers.Range(func(key, value interface{}) bool { + _ = value.(*aUDPProxyWorker).Close() + return true + }) return nil } // Proxy starts a worker for server, ignore if already started. -func (v *UDPProxy) Proxy(client *vnet.Net, server *net.UDPAddr) error { +func (v *UDPProxy) Proxy(client *Net, server *net.UDPAddr) error { // Note that even if the worker exists, it's also ok to create a same worker, // because the router will use the last one, and the real server will see a address // change event after we switch to the next worker. @@ -113,25 +72,44 @@ func (v *UDPProxy) Proxy(client *vnet.Net, server *net.UDPAddr) error { worker := &aUDPProxyWorker{ router: v.router, mockRealServerAddr: v.mockRealServerAddr, } + + // Create context for cleanup. + var ctx context.Context + ctx, worker.ctxDisposeCancel = context.WithCancel(context.Background()) + v.workers.Store(server.String(), worker) - return worker.Proxy(client, server) + return worker.Proxy(ctx, client, server) } // A proxy worker for a specified proxy server. type aUDPProxyWorker struct { - router *vnet.Router + router *Router mockRealServerAddr *net.UDPAddr // Each vnet source, bind to a real socket to server. // key is vnet client addr, which is net.Addr // value is *net.UDPConn endpoints sync.Map + + // For cleanup. + ctxDisposeCancel context.CancelFunc + wg sync.WaitGroup } -func (v *aUDPProxyWorker) Proxy(client *vnet.Net, serverAddr *net.UDPAddr) error { // nolint:gocognit +func (v *aUDPProxyWorker) Close() error { + // Notify all goroutines to dispose. + v.ctxDisposeCancel() + + // Wait for all goroutines quit. + v.wg.Wait() + + return nil +} + +func (v *aUDPProxyWorker) Proxy(ctx context.Context, client *Net, serverAddr *net.UDPAddr) error { // nolint:gocognit // Create vnet for real server by serverAddr. - nw := vnet.NewNet(&vnet.NetConfig{ + nw := NewNet(&NetConfig{ StaticIP: serverAddr.IP.String(), }) if err := v.router.AddNet(nw); err != nil { @@ -145,10 +123,71 @@ func (v *aUDPProxyWorker) Proxy(client *vnet.Net, serverAddr *net.UDPAddr) error return err } - // Start a proxy goroutine. - var findEndpointBy func(addr net.Addr) (*net.UDPConn, error) - // nolint:godox // TODO: FIXME: Do cleanup. + // User stop proxy, we should close the socket. go func() { + <-ctx.Done() + _ = vnetSocket.Close() + }() + + // Got new vnet client, start a new endpoint. + findEndpointBy := func(addr net.Addr) (*net.UDPConn, error) { + // Exists binding. + if value, ok := v.endpoints.Load(addr.String()); ok { + // Exists endpoint, reuse it. + return value.(*net.UDPConn), nil + } + + // The real server we proxy to, for utest to mock it. + realAddr := serverAddr + if v.mockRealServerAddr != nil { + realAddr = v.mockRealServerAddr + } + + // Got new vnet client, create new endpoint. + realSocket, err := net.DialUDP("udp4", nil, realAddr) + if err != nil { + return nil, err + } + + // User stop proxy, we should close the socket. + go func() { + <-ctx.Done() + _ = realSocket.Close() + }() + + // Bind address. + v.endpoints.Store(addr.String(), realSocket) + + // Got packet from real serverAddr, we should proxy it to vnet. + v.wg.Add(1) + go func(vnetClientAddr net.Addr) { + defer v.wg.Done() + + buf := make([]byte, 1500) + for { + n, _, err := realSocket.ReadFrom(buf) + if err != nil { + return + } + + if n <= 0 { + continue // Drop packet + } + + if _, err := vnetSocket.WriteTo(buf[:n], vnetClientAddr); err != nil { + return + } + } + }(addr) + + return realSocket, nil + } + + // Start a proxy goroutine. + v.wg.Add(1) + go func() { + defer v.wg.Done() + buf := make([]byte, 1500) for { @@ -172,51 +211,5 @@ func (v *aUDPProxyWorker) Proxy(client *vnet.Net, serverAddr *net.UDPAddr) error } }() - // Got new vnet client, start a new endpoint. - findEndpointBy = func(addr net.Addr) (*net.UDPConn, error) { - // Exists binding. - if value, ok := v.endpoints.Load(addr.String()); ok { - // Exists endpoint, reuse it. - return value.(*net.UDPConn), nil - } - - // The real server we proxy to, for utest to mock it. - realAddr := serverAddr - if v.mockRealServerAddr != nil { - realAddr = v.mockRealServerAddr - } - - // Got new vnet client, create new endpoint. - realSocket, err := net.DialUDP("udp4", nil, realAddr) - if err != nil { - return nil, err - } - - // Bind address. - v.endpoints.Store(addr.String(), realSocket) - - // Got packet from real serverAddr, we should proxy it to vnet. - // nolint:godox // TODO: FIXME: Do cleanup. - go func(vnetClientAddr net.Addr) { - buf := make([]byte, 1500) - for { - n, _, err := realSocket.ReadFrom(buf) - if err != nil { - return - } - - if n <= 0 { - continue // Drop packet - } - - if _, err := vnetSocket.WriteTo(buf[:n], vnetClientAddr); err != nil { - return - } - } - }(addr) - - return realSocket, nil - } - return nil } diff --git a/trunk/3rdparty/srs-bench/vnet/udpproxy_direct.go b/trunk/3rdparty/srs-bench/vnet/udpproxy_direct.go index 6d49494ed..35e4618d7 100644 --- a/trunk/3rdparty/srs-bench/vnet/udpproxy_direct.go +++ b/trunk/3rdparty/srs-bench/vnet/udpproxy_direct.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/3rdparty/srs-bench/vnet/udpproxy_direct_test.go b/trunk/3rdparty/srs-bench/vnet/udpproxy_direct_test.go index b347c682c..48b776957 100644 --- a/trunk/3rdparty/srs-bench/vnet/udpproxy_direct_test.go +++ b/trunk/3rdparty/srs-bench/vnet/udpproxy_direct_test.go @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2021 srs-bench(ossrs) +// Copyright (c) 2021 Winlin // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/3rdparty/srs-bench/vnet/udpproxy_test.go b/trunk/3rdparty/srs-bench/vnet/udpproxy_test.go index c0c1c4a2b..e5689bc18 100644 --- a/trunk/3rdparty/srs-bench/vnet/udpproxy_test.go +++ b/trunk/3rdparty/srs-bench/vnet/udpproxy_test.go @@ -1,23 +1,5 @@ -// The MIT License (MIT) -// -// Copyright (c) 2021 srs-bench(ossrs) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +build !wasm + package vnet import ( @@ -32,7 +14,6 @@ import ( "time" "github.com/pion/logging" - "github.com/pion/transport/vnet" ) type MockUDPEchoServer struct { @@ -163,7 +144,7 @@ func TestUDPProxyOne2One(t *testing.T) { } doVnetProxy := func() error { - router, err := vnet.NewRouter(&vnet.RouterConfig{ + router, err := NewRouter(&RouterConfig{ CIDR: "0.0.0.0/0", LoggerFactory: logging.NewDefaultLoggerFactory(), }) @@ -171,7 +152,7 @@ func TestUDPProxyOne2One(t *testing.T) { return err } - clientNetwork := vnet.NewNet(&vnet.NetConfig{ + clientNetwork := NewNet(&NetConfig{ StaticIP: "10.0.0.11", }) if err = router.AddNet(clientNetwork); err != nil { @@ -309,7 +290,7 @@ func TestUDPProxyTwo2One(t *testing.T) { } doVnetProxy := func() error { - router, err := vnet.NewRouter(&vnet.RouterConfig{ + router, err := NewRouter(&RouterConfig{ CIDR: "0.0.0.0/0", LoggerFactory: logging.NewDefaultLoggerFactory(), }) @@ -317,7 +298,7 @@ func TestUDPProxyTwo2One(t *testing.T) { return err } - clientNetwork := vnet.NewNet(&vnet.NetConfig{ + clientNetwork := NewNet(&NetConfig{ StaticIP: "10.0.0.11", }) if err = router.AddNet(clientNetwork); err != nil { @@ -487,7 +468,7 @@ func TestUDPProxyProxyTwice(t *testing.T) { } doVnetProxy := func() error { - router, err := vnet.NewRouter(&vnet.RouterConfig{ + router, err := NewRouter(&RouterConfig{ CIDR: "0.0.0.0/0", LoggerFactory: logging.NewDefaultLoggerFactory(), }) @@ -495,7 +476,7 @@ func TestUDPProxyProxyTwice(t *testing.T) { return err } - clientNetwork := vnet.NewNet(&vnet.NetConfig{ + clientNetwork := NewNet(&NetConfig{ StaticIP: "10.0.0.11", }) if err = router.AddNet(clientNetwork); err != nil { diff --git a/trunk/3rdparty/srs-bench/vnet/vnet.go b/trunk/3rdparty/srs-bench/vnet/vnet.go new file mode 100644 index 000000000..ceab0847f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vnet/vnet.go @@ -0,0 +1,38 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 Winlin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package vnet + +import ( + "github.com/pion/transport/vnet" +) + +type Router = vnet.Router +type Net = vnet.Net + +type NetConfig = vnet.NetConfig +type RouterConfig = vnet.RouterConfig + +func NewNet(config *NetConfig) *Net { + return vnet.NewNet(config) +} +func NewRouter(config *RouterConfig) (*Router, error) { + return vnet.NewRouter(config) +} diff --git a/trunk/3rdparty/st-srs/.gitignore b/trunk/3rdparty/st-srs/.gitignore index 97cd5081d..18d2ab26f 100644 --- a/trunk/3rdparty/st-srs/.gitignore +++ b/trunk/3rdparty/st-srs/.gitignore @@ -2,3 +2,6 @@ DARWIN_*_DBG LINUX_*_DBG obj st.pc + +coverage +utest From 3fea5c0ec3c7d8721450cae60d0960dcb3ed4334 Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 23 Mar 2021 19:32:59 +0800 Subject: [PATCH 028/563] Test: Add republish regression test, should fail --- trunk/3rdparty/srs-bench/srs/ingester.go | 31 +- trunk/3rdparty/srs-bench/srs/rtc_test.go | 576 +++++++++++------------ trunk/3rdparty/srs-bench/srs/util.go | 161 ++++--- trunk/src/app/srs_app_rtc_conn.cpp | 2 +- 4 files changed, 397 insertions(+), 373 deletions(-) diff --git a/trunk/3rdparty/srs-bench/srs/ingester.go b/trunk/3rdparty/srs-bench/srs/ingester.go index f38409e59..bcedebbb6 100644 --- a/trunk/3rdparty/srs-bench/srs/ingester.go +++ b/trunk/3rdparty/srs-bench/srs/ingester.go @@ -44,16 +44,20 @@ type videoIngester struct { markerInterceptor *RTPInterceptor sVideoTrack *webrtc.TrackLocalStaticSample sVideoSender *webrtc.RTPSender + ready context.Context + readyCancel context.CancelFunc } func NewVideoIngester(sourceVideo string) *videoIngester { - return &videoIngester{markerInterceptor: &RTPInterceptor{}, sourceVideo: sourceVideo} + v := &videoIngester{markerInterceptor: &RTPInterceptor{}, sourceVideo: sourceVideo} + v.ready, v.readyCancel = context.WithCancel(context.Background()) + return v } func (v *videoIngester) Close() error { + v.readyCancel() if v.sVideoSender != nil { - v.sVideoSender.Stop() - v.sVideoSender = nil + _ = v.sVideoSender.Stop() } return nil } @@ -102,6 +106,9 @@ func (v *videoIngester) Ingest(ctx context.Context) error { logger.Tf(ctx, "Video %v, tbn=%v, fps=%v, ssrc=%v, pt=%v, header=%v", codec.MimeType, codec.ClockRate, fps, enc.SSRC, codec.PayloadType, headers) + // OK, we are ready. + v.readyCancel() + clock := newWallClock() sampleDuration := time.Duration(uint64(time.Millisecond) * 1000 / uint64(fps)) for ctx.Err() == nil { @@ -179,16 +186,21 @@ type audioIngester struct { audioLevelInterceptor *RTPInterceptor sAudioTrack *webrtc.TrackLocalStaticSample sAudioSender *webrtc.RTPSender + ready context.Context + readyCancel context.CancelFunc } func NewAudioIngester(sourceAudio string) *audioIngester { - return &audioIngester{audioLevelInterceptor: &RTPInterceptor{}, sourceAudio: sourceAudio} + v := &audioIngester{audioLevelInterceptor: &RTPInterceptor{}, sourceAudio: sourceAudio} + v.ready, v.readyCancel = context.WithCancel(context.Background()) + return v } func (v *audioIngester) Close() error { + v.readyCancel() // OK we are closed, also ready. + if v.sAudioSender != nil { - v.sAudioSender.Stop() - v.sAudioSender = nil + _ = v.sAudioSender.Stop() } return nil } @@ -240,6 +252,9 @@ func (v *audioIngester) Ingest(ctx context.Context) error { } } + // OK, we are ready. + v.readyCancel() + clock := newWallClock() var lastGranule uint64 @@ -253,7 +268,7 @@ func (v *audioIngester) Ingest(ctx context.Context) error { } // The amount of samples is the difference between the last and current timestamp - sampleCount := uint64(pageHeader.GranulePosition - lastGranule) + sampleCount := pageHeader.GranulePosition - lastGranule lastGranule = pageHeader.GranulePosition sampleDuration := time.Duration(uint64(time.Millisecond) * 1000 * sampleCount / uint64(codec.ClockRate)) @@ -266,7 +281,7 @@ func (v *audioIngester) Ingest(ctx context.Context) error { return 0, err } - header.SetExtension(uint8(audioLevel.ID), audioLevelPayload) + _ = header.SetExtension(uint8(audioLevel.ID), audioLevelPayload) } return ri.nextRTPWriter.Write(header, payload, attributes) diff --git a/trunk/3rdparty/srs-bench/srs/rtc_test.go b/trunk/3rdparty/srs-bench/srs/rtc_test.go index 62ad26663..de5ac0ff0 100644 --- a/trunk/3rdparty/srs-bench/srs/rtc_test.go +++ b/trunk/3rdparty/srs-bench/srs/rtc_test.go @@ -22,11 +22,13 @@ package srs import ( "context" + "encoding/json" "fmt" "github.com/pion/transport/vnet" "io" "io/ioutil" "math/rand" + "net/http" "os" "sync" "testing" @@ -73,7 +75,7 @@ func TestRtcBasic_PublishPlay(t *testing.T) { var resources []io.Closer defer func() { for _, resource := range resources { - resource.Close() + _ = resource.Close() } }() @@ -93,27 +95,19 @@ func TestRtcBasic_PublishPlay(t *testing.T) { defer wg.Done() defer cancel() - doInit := func() error { - playOK, vnetClientIP := *srsPlayOKPackets, *srsVnetClientIP + doInit := func() (err error) { streamSuffix := fmt.Sprintf("basic-publish-play-%v-%v", os.Getpid(), rand.Int()) // Initialize player with private api. - if play, err := NewTestPlayer(nil, func(play *TestPlayer) error { + if thePlayer, err = NewTestPlayer(CreateApiForPlayer, func(play *TestPlayer) error { play.streamSuffix = streamSuffix resources = append(resources, play) - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - resources = append(resources, api) - play.api = api - var nnPlayWriteRTCP, nnPlayReadRTCP, nnPlayWriteRTP, nnPlayReadRTP uint64 - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + return play.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpReader = func(payload []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnPlayReadRTP++; nnPlayReadRTP >= uint64(playOK) { + if nnPlayReadRTP++; nnPlayReadRTP >= uint64(*srsPlayOKPackets) { cancel() // Completed. } logger.Tf(ctx, "Play rtp=(recv:%v, send:%v), rtcp=(recv:%v send:%v) packets", @@ -133,32 +127,19 @@ func TestRtcBasic_PublishPlay(t *testing.T) { return nn, err } })) - }); err != nil { - return err - } - - return nil + }) }); err != nil { return err - } else { - thePlayer = play } // Initialize publisher with private api. - if pub, err := NewTestPublisher(nil, func(pub *TestPublisher) error { + if thePublisher, err = NewTestPublisher(CreateApiForPublisher, func(pub *TestPublisher) error { pub.streamSuffix = streamSuffix pub.iceReadyCancel = publishReadyCancel resources = append(resources, pub) - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - resources = append(resources, api) - pub.api = api - var nnPubWriteRTCP, nnPubReadRTCP, nnPubWriteRTP, nnPubReadRTP uint64 - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + return pub.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { nn, attr, err := i.nextRTPReader.Read(buf, attributes) @@ -185,15 +166,9 @@ func TestRtcBasic_PublishPlay(t *testing.T) { return nn, err } })) - }); err != nil { - return err - } - - return nil + }) }); err != nil { return err - } else { - thePublisher = pub } // Init done. @@ -216,14 +191,10 @@ func TestRtcBasic_PublishPlay(t *testing.T) { select { case <-ctx.Done(): - return case <-mainReady.Done(): + r2 = thePublisher.Run(logger.WithContext(ctx), cancel) + logger.Tf(ctx, "pub done") } - - if err := thePublisher.Run(logger.WithContext(ctx), cancel); err != nil { - r2 = err - } - logger.Tf(ctx, "pub done") }() // Run player. @@ -234,14 +205,159 @@ func TestRtcBasic_PublishPlay(t *testing.T) { select { case <-ctx.Done(): - return case <-publishReady.Done(): + r3 = thePlayer.Run(logger.WithContext(ctx), cancel) + logger.Tf(ctx, "play done") + } + }() +} + +// When republish a stream, the player stream SHOULD be continuous. +func TestRtcBasic_Republish(t *testing.T) { + ctx := logger.WithContext(context.Background()) + ctx, cancel := context.WithTimeout(ctx, time.Duration(*srsTimeout)*time.Millisecond) + + var r0, r1, r2, r3, r4 error + defer func(ctx context.Context) { + if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4); err != nil { + t.Errorf("Fail for err %+v", err) + } else { + logger.Tf(ctx, "test done with err %+v", err) + } + }(ctx) + + var resources []io.Closer + defer func() { + for _, resource := range resources { + _ = resource.Close() + } + }() + + var wg sync.WaitGroup + defer wg.Wait() + + // The event notify. + var thePublisher, theRepublisher *TestPublisher + var thePlayer *TestPlayer + + mainReady, mainReadyCancel := context.WithCancel(context.Background()) + publishReady, publishReadyCancel := context.WithCancel(context.Background()) + republishReady, republishReadyCancel := context.WithCancel(context.Background()) + + // Objects init. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + doInit := func() (err error) { + streamSuffix := fmt.Sprintf("basic-publish-play-%v-%v", os.Getpid(), rand.Int()) + + // Initialize player with private api. + if thePlayer, err = NewTestPlayer(CreateApiForPlayer, func(play *TestPlayer) error { + play.streamSuffix = streamSuffix + resources = append(resources, play) + + var nnPlayReadRTP uint64 + return play.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { + api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { + i.rtpReader = func(payload []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + select { + case <-republishReady.Done(): + if nnPlayReadRTP++; nnPlayReadRTP >= uint64(*srsPlayOKPackets) { + cancel() // Completed. + } + logger.Tf(ctx, "Play recv rtp %v packets", nnPlayReadRTP) + default: + logger.Tf(ctx, "Play recv rtp packet before republish") + } + return i.nextRTPReader.Read(payload, attributes) + } + })) + }) + }); err != nil { + return err + } + + // Initialize publisher with private api. + if thePublisher, err = NewTestPublisher(CreateApiForPublisher, func(pub *TestPublisher) error { + pub.streamSuffix = streamSuffix + pub.iceReadyCancel = publishReadyCancel + resources = append(resources, pub) + + var nnPubReadRTCP uint64 + return pub.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { + api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { + i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { + nn, attr, err := i.nextRTCPReader.Read(buf, attributes) + if nnPubReadRTCP++; nnPubReadRTCP > 0 && pub.cancel != nil { + pub.cancel() // We only cancel the publisher itself. + } + logger.Tf(ctx, "Publish recv rtcp %v packets", nnPubReadRTCP) + return nn, attr, err + } + })) + }) + }); err != nil { + return err + } + + // Initialize re-publisher with private api. + if theRepublisher, err = NewTestPublisher(CreateApiForPublisher, func(pub *TestPublisher) error { + pub.streamSuffix = streamSuffix + pub.iceReadyCancel = republishReadyCancel + resources = append(resources, pub) + + return pub.Setup(*srsVnetClientIP) + }); err != nil { + return err + } + + // Init done. + mainReadyCancel() + + <-ctx.Done() + return nil } - if err := thePlayer.Run(logger.WithContext(ctx), cancel); err != nil { - r3 = err + if err := doInit(); err != nil { + r1 = err + } + }() + + // Run publisher. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + select { + case <-ctx.Done(): + case <-mainReady.Done(): + pubCtx, pubCancel := context.WithCancel(ctx) + r2 = thePublisher.Run(logger.WithContext(pubCtx), pubCancel) + logger.Tf(ctx, "pub done, re-publish again") + + // Dispose the stream. + _ = thePublisher.Close() + + r4 = theRepublisher.Run(logger.WithContext(ctx), cancel) + logger.Tf(ctx, "re-pub done") + } + }() + + // Run player. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + select { + case <-ctx.Done(): + case <-publishReady.Done(): + r3 = thePlayer.Run(logger.WithContext(ctx), cancel) + logger.Tf(ctx, "play done") } - logger.Tf(ctx, "play done") }() } @@ -252,18 +368,8 @@ func TestRtcBasic_PublishPlay(t *testing.T) { // No.4 srs-server: ChangeCipherSpec, Finished func TestRtcDTLS_ClientActive_Default(t *testing.T) { if err := filterTestError(func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive return nil @@ -273,7 +379,8 @@ func TestRtcDTLS_ClientActive_Default(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -283,7 +390,7 @@ func TestRtcDTLS_ClientActive_Default(t *testing.T) { })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -316,18 +423,8 @@ func TestRtcDTLS_ClientActive_Default(t *testing.T) { // No.4 srs-bench: ChangeCipherSpec, Finished func TestRtcDTLS_ClientPassive_Default(t *testing.T) { if err := filterTestError(func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-active-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -337,7 +434,8 @@ func TestRtcDTLS_ClientPassive_Default(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -347,7 +445,7 @@ func TestRtcDTLS_ClientPassive_Default(t *testing.T) { })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -377,18 +475,8 @@ func TestRtcDTLS_ClientPassive_Default(t *testing.T) { // When srs-bench close the PC, it will send DTLS alert and might retransmit it. func TestRtcDTLS_ClientActive_Duplicated_Alert(t *testing.T) { if err := filterTestError(func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-active-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive return nil @@ -398,7 +486,8 @@ func TestRtcDTLS_ClientActive_Duplicated_Alert(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -408,7 +497,7 @@ func TestRtcDTLS_ClientActive_Duplicated_Alert(t *testing.T) { })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -445,18 +534,8 @@ func TestRtcDTLS_ClientActive_Duplicated_Alert(t *testing.T) { // When srs-bench close the PC, it will send DTLS alert and might retransmit it. func TestRtcDTLS_ClientPassive_Duplicated_Alert(t *testing.T) { if err := filterTestError(func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-active-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -466,7 +545,8 @@ func TestRtcDTLS_ClientPassive_Duplicated_Alert(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -476,7 +556,7 @@ func TestRtcDTLS_ClientPassive_Duplicated_Alert(t *testing.T) { })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -520,18 +600,8 @@ func TestRtcDTLS_ClientPassive_Duplicated_Alert(t *testing.T) { func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ClientHello(t *testing.T) { var r0 error err := func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-active-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive return nil @@ -541,7 +611,8 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ClientHello(t *testing.T } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -551,7 +622,7 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ClientHello(t *testing.T })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -579,7 +650,7 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ClientHello(t *testing.T lastClientHello = record nnClientHello++ - ok = (nnClientHello > nnMaxDrop) + ok = nnClientHello > nnMaxDrop logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnClientHello, chunk, record, ok, len(c.UserData())) return }) @@ -607,18 +678,8 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ClientHello(t *testing.T func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ClientHello(t *testing.T) { var r0 error err := func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -628,7 +689,8 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ClientHello(t *testing. } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -638,7 +700,7 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ClientHello(t *testing. })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -666,7 +728,7 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ClientHello(t *testing. lastClientHello = record nnClientHello++ - ok = (nnClientHello > nnMaxDrop) + ok = nnClientHello > nnMaxDrop logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnClientHello, chunk, record, ok, len(c.UserData())) return }) @@ -693,18 +755,8 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ClientHello(t *testing. func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ServerHello(t *testing.T) { var r0, r1 error err := func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-active-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive return nil @@ -714,7 +766,8 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ServerHello(t *testing.T } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -724,7 +777,7 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ServerHello(t *testing.T })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -761,7 +814,7 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ServerHello(t *testing.T lastServerHello = record nnServerHello++ - ok = (nnServerHello > nnMaxDrop) + ok = nnServerHello > nnMaxDrop logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnServerHello, chunk, record, ok, len(c.UserData())) return }) @@ -790,18 +843,8 @@ func TestRtcDTLS_ClientActive_ARQ_ClientHello_ByDropped_ServerHello(t *testing.T func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ServerHello(t *testing.T) { var r0, r1 error err := func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-arq-client-hello-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -811,7 +854,8 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ServerHello(t *testing. } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -821,7 +865,7 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ServerHello(t *testing. })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -858,7 +902,7 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ServerHello(t *testing. lastServerHello = record nnServerHello++ - ok = (nnServerHello > nnMaxDrop) + ok = nnServerHello > nnMaxDrop logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnServerHello, chunk, record, ok, len(c.UserData())) return }) @@ -884,18 +928,8 @@ func TestRtcDTLS_ClientPassive_ARQ_ClientHello_ByDropped_ServerHello(t *testing. func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_Certificate(t *testing.T) { var r0 error err := func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-active-arq-certificate-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive return nil @@ -905,7 +939,8 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_Certificate(t *testing.T } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -915,7 +950,7 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_Certificate(t *testing.T })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -943,7 +978,7 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_Certificate(t *testing.T lastCertificate = record nnCertificate++ - ok = (nnCertificate > nnMaxDrop) + ok = nnCertificate > nnMaxDrop logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnCertificate, chunk, record, ok, len(c.UserData())) return }) @@ -970,18 +1005,8 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_Certificate(t *testing.T func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_Certificate(t *testing.T) { var r0 error err := func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-arq-certificate-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -991,7 +1016,8 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_Certificate(t *testing. } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -1001,7 +1027,7 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_Certificate(t *testing. })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -1029,7 +1055,7 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_Certificate(t *testing. lastCertificate = record nnCertificate++ - ok = (nnCertificate > nnMaxDrop) + ok = nnCertificate > nnMaxDrop logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnCertificate, chunk, record, ok, len(c.UserData())) return }) @@ -1056,18 +1082,8 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_Certificate(t *testing. func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *testing.T) { var r0, r1 error err := func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-active-arq-certificate-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupActive return nil @@ -1077,7 +1093,8 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *test } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -1087,7 +1104,7 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *test })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -1123,7 +1140,7 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *test lastChangeCipherSepc = record nnCertificate++ - ok = (nnCertificate > nnMaxDrop) + ok = nnCertificate > nnMaxDrop logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnCertificate, chunk, record, ok, len(c.UserData())) return }) @@ -1151,18 +1168,8 @@ func TestRtcDTLS_ClientActive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *test func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *testing.T) { var r0, r1 error err := func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-arq-certificate-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -1172,7 +1179,8 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *tes } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -1182,7 +1190,7 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *tes })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -1218,7 +1226,7 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *tes lastChangeCipherSepc = record nnCertificate++ - ok = (nnCertificate > nnMaxDrop) + ok = nnCertificate > nnMaxDrop logger.Tf(ctx, "NN=%v, Chunk %v, %v, ok=%v %v bytes", nnCertificate, chunk, record, ok, len(c.UserData())) return }) @@ -1237,18 +1245,8 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_ByDropped_ChangeCipherSpec(t *tes // Drop all DTLS packets when got ClientHello, to test the server ARQ thread cleanup. func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ClientHello(t *testing.T) { if err := filterTestError(func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - vnetClientIP, dtlsDropPackets := *srsVnetClientIP, *srsDTLSDropPackets - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -1258,7 +1256,8 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ClientHello(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { nnDrop, dropAll := 0, false api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { chunk, parsed := NewChunkMessageType(c) @@ -1275,7 +1274,7 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ClientHello(t *testing.T) { return true } - if nnDrop++; nnDrop >= dtlsDropPackets { + if nnDrop++; nnDrop >= *srsDTLSDropPackets { cancel() // Done, server transmit 5 Client Hello. } @@ -1299,18 +1298,8 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ClientHello(t *testing.T) { // Drop all DTLS packets when got ServerHello, to test the server ARQ thread cleanup. func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ServerHello(t *testing.T) { if err := filterTestError(func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - vnetClientIP, dtlsDropPackets := *srsVnetClientIP, *srsDTLSDropPackets - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -1320,7 +1309,8 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ServerHello(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { nnDrop, dropAll := 0, false api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { chunk, parsed := NewChunkMessageType(c) @@ -1337,7 +1327,7 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ServerHello(t *testing.T) { return true } - if nnDrop++; nnDrop >= dtlsDropPackets { + if nnDrop++; nnDrop >= *srsDTLSDropPackets { cancel() // Done, server transmit 5 Client Hello. } @@ -1361,18 +1351,8 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ServerHello(t *testing.T) { // Drop all DTLS packets when got Certificate, to test the server ARQ thread cleanup. func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_Certificate(t *testing.T) { if err := filterTestError(func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - vnetClientIP, dtlsDropPackets := *srsVnetClientIP, *srsDTLSDropPackets - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -1382,7 +1362,8 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_Certificate(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { nnDrop, dropAll := 0, false api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { chunk, parsed := NewChunkMessageType(c) @@ -1399,7 +1380,7 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_Certificate(t *testing.T) { return true } - if nnDrop++; nnDrop >= dtlsDropPackets { + if nnDrop++; nnDrop >= *srsDTLSDropPackets { cancel() // Done, server transmit 5 Client Hello. } @@ -1423,18 +1404,8 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_Certificate(t *testing.T) { // Drop all DTLS packets when got ChangeCipherSpec, to test the server ARQ thread cleanup. func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ChangeCipherSpec(t *testing.T) { if err := filterTestError(func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - vnetClientIP, dtlsDropPackets := *srsVnetClientIP, *srsDTLSDropPackets - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -1444,7 +1415,8 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ChangeCipherSpec(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { nnDrop, dropAll := 0, false api.router.AddChunkFilter(func(c vnet.Chunk) (ok bool) { chunk, parsed := NewChunkMessageType(c) @@ -1461,7 +1433,7 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ChangeCipherSpec(t *testing.T) { return true } - if nnDrop++; nnDrop >= dtlsDropPackets { + if nnDrop++; nnDrop >= *srsDTLSDropPackets { cancel() // Done, server transmit 5 Client Hello. } @@ -1486,18 +1458,8 @@ func TestRtcDTLS_ClientPassive_ARQ_DropAllAfter_ChangeCipherSpec(t *testing.T) { // which also consume about 750ms, but finally should be done successfully. func TestRtcDTLS_ClientPassive_ARQ_VeryBadNetwork(t *testing.T) { if err := filterTestError(func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP, dtlsDropPackets := *srsPublishOKPackets, *srsVnetClientIP, *srsDTLSDropPackets - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -1507,7 +1469,8 @@ func TestRtcDTLS_ClientPassive_ARQ_VeryBadNetwork(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -1517,7 +1480,7 @@ func TestRtcDTLS_ClientPassive_ARQ_VeryBadNetwork(t *testing.T) { })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -1545,7 +1508,7 @@ func TestRtcDTLS_ClientPassive_ARQ_VeryBadNetwork(t *testing.T) { } if chunk.IsCertificate() { - if nnDropCertificate >= dtlsDropPackets { + if nnDropCertificate >= *srsDTLSDropPackets { return true } nnDropCertificate++ @@ -1573,18 +1536,8 @@ func TestRtcDTLS_ClientPassive_ARQ_VeryBadNetwork(t *testing.T) { func TestRtcDTLS_ClientPassive_ARQ_Certificate_After_ClientHello(t *testing.T) { var r0 error err := func() error { - ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) - publishOK, vnetClientIP := *srsPublishOKPackets, *srsVnetClientIP - - // Create top level test object. - api, err := NewTestWebRTCAPI() - if err != nil { - return err - } - defer api.Close() - streamSuffix := fmt.Sprintf("dtls-passive-no-arq-%v-%v", os.Getpid(), rand.Int()) - p, err := NewTestPublisher(api, func(p *TestPublisher) error { + p, err := NewTestPublisher(CreateApiForPublisher, func(p *TestPublisher) error { p.streamSuffix = streamSuffix p.onOffer = testUtilSetupPassive return nil @@ -1594,7 +1547,8 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_After_ClientHello(t *testing.T) { } defer p.Close() - if err := api.Setup(vnetClientIP, func(api *TestWebRTCAPI) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + if err := p.Setup(*srsVnetClientIP, func(api *TestWebRTCAPI) { var nnRTCP, nnRTP int64 api.registry.Add(NewRTPInterceptor(func(i *RTPInterceptor) { i.rtpWriter = func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { @@ -1604,7 +1558,7 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_After_ClientHello(t *testing.T) { })) api.registry.Add(NewRTCPInterceptor(func(i *RTCPInterceptor) { i.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { - if nnRTCP++; nnRTCP >= int64(publishOK) && nnRTP >= int64(publishOK) { + if nnRTCP++; nnRTCP >= int64(*srsPublishOKPackets) && nnRTP >= int64(*srsPublishOKPackets) { cancel() // Send enough packets, done. } logger.Tf(ctx, "publish write %v RTP read %v RTCP packets", nnRTP, nnRTCP) @@ -1662,3 +1616,47 @@ func TestRtcDTLS_ClientPassive_ARQ_Certificate_After_ClientHello(t *testing.T) { t.Errorf("err %+v", err) } } + +func TestRTCServerVersion(t *testing.T) { + api := fmt.Sprintf("http://%v:1985/api/v1/versions", *srsServer) + req, err := http.NewRequest("POST", api, nil) + if err != nil { + t.Errorf("Request %v", api) + return + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Errorf("Do request %v", api) + return + } + + b, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Errorf("Read body of %v", api) + return + } + + obj := struct { + Code int `json:"code"` + Server string `json:"server"` + Data struct { + Major int `json:"major"` + Minor int `json:"minor"` + Revision int `json:"revision"` + Version string `json:"version"` + } `json:"data"` + }{} + if err := json.Unmarshal(b, &obj); err != nil { + t.Errorf("Parse %v", string(b)) + return + } + if obj.Code != 0 { + t.Errorf("Server err code=%v, server=%v", obj.Code, obj.Server) + return + } + if obj.Data.Major == 0 && obj.Data.Minor == 0 { + t.Errorf("Invalid version %v", obj.Data) + return + } +} diff --git a/trunk/3rdparty/srs-bench/srs/util.go b/trunk/3rdparty/srs-bench/srs/util.go index 941b0bb76..e8cb47dad 100644 --- a/trunk/3rdparty/srs-bench/srs/util.go +++ b/trunk/3rdparty/srs-bench/srs/util.go @@ -36,7 +36,6 @@ import ( "strconv" "strings" "sync" - "testing" "time" "github.com/ossrs/go-oryx-lib/errors" @@ -207,7 +206,7 @@ func apiRtcRequest(ctx context.Context, apiPath, r, offer string) (string, error logger.Tf(ctx, "Parse response to code=%v, session=%v, sdp=%v bytes", resBody.Code, resBody.Session, len(resBody.SDP)) - return string(resBody.SDP), nil + return resBody.SDP, nil } func escapeSDP(sdp string) string { @@ -219,7 +218,7 @@ func packageAsSTAPA(frames ...*h264reader.NAL) *h264reader.NAL { buf := bytes.Buffer{} buf.WriteByte( - byte(first.RefIdc<<5)&0x60 | byte(24), // STAP-A + first.RefIdc<<5&0x60 | byte(24), // STAP-A ) for _, frame := range frames { @@ -325,6 +324,14 @@ func filterTestError(errs ...error) error { if err == nil || errors.Cause(err) == context.Canceled { continue } + + // If url error, server maybe error, do not print the detail log. + if r0 := errors.Cause(err); r0 != nil { + if r1, ok := r0.(*url.Error); ok { + err = r1 + } + } + filteredErrors = append(filteredErrors, err) } @@ -352,13 +359,13 @@ func srsIsStun(b []byte) bool { // @see https://tools.ietf.org/html/rfc2246#section-6.2.1 // @see srs_is_dtls of https://github.com/ossrs/srs func srsIsDTLS(b []byte) bool { - return (len(b) >= 13 && (b[0] > 19 && b[0] < 64)) + return len(b) >= 13 && (b[0] > 19 && b[0] < 64) } // For RTP or RTCP, the V=2 which is in the high 2bits, 0xC0 (1100 0000) // @see srs_is_rtp_or_rtcp of https://github.com/ossrs/srs func srsIsRTPOrRTCP(b []byte) bool { - return (len(b) >= 12 && (b[0]&0xC0) == 0x80) + return len(b) >= 12 && (b[0]&0xC0) == 0x80 } // For RTCP, PT is [128, 223] (or without marker [0, 95]). @@ -554,7 +561,7 @@ func (v *DTLSRecord) Unmarshal(b []byte) error { return errors.Errorf("requires 13B only %v", len(b)) } - v.ContentType = DTLSContentType(uint8(b[0])) + v.ContentType = DTLSContentType(b[0]) v.Version = uint16(b[1])<<8 | uint16(b[2]) v.Epoch = uint16(b[3])<<8 | uint16(b[4]) v.SequenceNumber = uint64(b[5])<<40 | uint64(b[6])<<32 | uint64(b[7])<<24 | uint64(b[8])<<16 | uint64(b[9])<<8 | uint64(b[10]) @@ -605,11 +612,11 @@ func NewTestWebRTCAPI(options ...TestWebRTCAPIOptionFunc) (*TestWebRTCAPI, error func (v *TestWebRTCAPI) Close() error { if v.proxy != nil { - v.proxy.Close() + _ = v.proxy.Close() } if v.router != nil { - v.router.Stop() + _ = v.router.Stop() } return nil @@ -676,14 +683,24 @@ type TestPlayerOptionFunc func(p *TestPlayer) error type TestPlayer struct { pc *webrtc.PeerConnection receivers []*webrtc.RTPReceiver - // root api object + // We should dispose it. api *TestWebRTCAPI // Optional suffix for stream url. streamSuffix string } -func NewTestPlayer(api *TestWebRTCAPI, options ...TestPlayerOptionFunc) (*TestPlayer, error) { - v := &TestPlayer{api: api} +func CreateApiForPlayer(play *TestPlayer) error { + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + + play.api = api + return nil +} + +func NewTestPlayer(options ...TestPlayerOptionFunc) (*TestPlayer, error) { + v := &TestPlayer{} for _, opt := range options { if err := opt(v); err != nil { @@ -691,19 +708,24 @@ func NewTestPlayer(api *TestWebRTCAPI, options ...TestPlayerOptionFunc) (*TestPl } } - // The api might be override by options. - api = v.api - return v, nil } +func (v *TestPlayer) Setup(vnetClientIP string, options ...TestWebRTCAPIOptionFunc) error { + return v.api.Setup(vnetClientIP, options...) +} + func (v *TestPlayer) Close() error { if v.pc != nil { - v.pc.Close() + _ = v.pc.Close() } for _, receiver := range v.receivers { - receiver.Stop() + _ = receiver.Stop() + } + + if v.api != nil { + _ = v.api.Close() } return nil @@ -723,12 +745,16 @@ func (v *TestPlayer) Run(ctx context.Context, cancel context.CancelFunc) error { } v.pc = pc - pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{ + if _, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{ Direction: webrtc.RTPTransceiverDirectionRecvonly, - }) - pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{ + }); err != nil { + return errors.Wrapf(err, "add track") + } + if _, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{ Direction: webrtc.RTPTransceiverDirectionRecvonly, - }) + }); err != nil { + return errors.Wrapf(err, "add track") + } offer, err := pc.CreateOffer(nil) if err != nil { @@ -818,16 +844,28 @@ type TestPublisher struct { aIngester *audioIngester vIngester *videoIngester pc *webrtc.PeerConnection - // root api object + // We should dispose it. api *TestWebRTCAPI // Optional suffix for stream url. streamSuffix string + // To cancel the publisher, pass by Run. + cancel context.CancelFunc } -func NewTestPublisher(api *TestWebRTCAPI, options ...TestPublisherOptionFunc) (*TestPublisher, error) { +func CreateApiForPublisher(pub *TestPublisher) error { + api, err := NewTestWebRTCAPI() + if err != nil { + return err + } + + pub.api = api + return nil +} + +func NewTestPublisher(options ...TestPublisherOptionFunc) (*TestPublisher, error) { sourceVideo, sourceAudio := *srsPublishVideo, *srsPublishAudio - v := &TestPublisher{api: api} + v := &TestPublisher{} for _, opt := range options { if err := opt(v); err != nil { @@ -835,9 +873,6 @@ func NewTestPublisher(api *TestWebRTCAPI, options ...TestPublisherOptionFunc) (* } } - // The api might be override by options. - api = v.api - // Create ingesters. if sourceAudio != "" { v.aIngester = NewAudioIngester(sourceAudio) @@ -847,6 +882,7 @@ func NewTestPublisher(api *TestWebRTCAPI, options ...TestPublisherOptionFunc) (* } // Setup the interceptors for packets. + api := v.api api.options = append(api.options, func(api *TestWebRTCAPI) { // Filter for RTCP packets. rtcpInterceptor := &RTCPInterceptor{} @@ -870,17 +906,25 @@ func NewTestPublisher(api *TestWebRTCAPI, options ...TestPublisherOptionFunc) (* return v, nil } +func (v *TestPublisher) Setup(vnetClientIP string, options ...TestWebRTCAPIOptionFunc) error { + return v.api.Setup(vnetClientIP, options...) +} + func (v *TestPublisher) Close() error { if v.vIngester != nil { - v.vIngester.Close() + _ = v.vIngester.Close() } if v.aIngester != nil { - v.aIngester.Close() + _ = v.aIngester.Close() } if v.pc != nil { - v.pc.Close() + _ = v.pc.Close() + } + + if v.api != nil { + _ = v.api.Close() } return nil @@ -892,6 +936,9 @@ func (v *TestPublisher) SetStreamSuffix(suffix string) *TestPublisher { } func (v *TestPublisher) Run(ctx context.Context, cancel context.CancelFunc) error { + // Save the cancel. + v.cancel = cancel + r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream) if v.streamSuffix != "" { r = fmt.Sprintf("%v-%v", r, v.streamSuffix) @@ -1012,11 +1059,17 @@ func (v *TestPublisher) Run(ctx context.Context, cancel context.CancelFunc) erro <-ctx.Done() if v.aIngester != nil && v.aIngester.sAudioSender != nil { - v.aIngester.sAudioSender.Stop() + // We MUST wait for the ingester ready(or closed), because it might crash if sender is disposed. + <-v.aIngester.ready.Done() + + _ = v.aIngester.Close() } if v.vIngester != nil && v.vIngester.sVideoSender != nil { - v.vIngester.sVideoSender.Stop() + // We MUST wait for the ingester ready(or closed), because it might crash if sender is disposed. + <-v.vIngester.ready.Done() + + _ = v.vIngester.Close() } }() @@ -1028,6 +1081,7 @@ func (v *TestPublisher) Run(ctx context.Context, cancel context.CancelFunc) erro if v.aIngester == nil { return } + defer v.aIngester.readyCancel() select { case <-ctx.Done(): @@ -1072,6 +1126,7 @@ func (v *TestPublisher) Run(ctx context.Context, cancel context.CancelFunc) erro if v.vIngester == nil { return } + defer v.vIngester.readyCancel() select { case <-ctx.Done(): @@ -1119,47 +1174,3 @@ func (v *TestPublisher) Run(ctx context.Context, cancel context.CancelFunc) erro } return ctx.Err() } - -func TestRTCServerVersion(t *testing.T) { - api := fmt.Sprintf("http://%v:1985/api/v1/versions", *srsServer) - req, err := http.NewRequest("POST", api, nil) - if err != nil { - t.Errorf("Request %v", api) - return - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - t.Errorf("Do request %v", api) - return - } - - b, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Errorf("Read body of %v", api) - return - } - - obj := struct { - Code int `json:"code"` - Server string `json:"server"` - Data struct { - Major int `json:"major"` - Minor int `json:"minor"` - Revision int `json:"revision"` - Version string `json:"version"` - } `json:"data"` - }{} - if err := json.Unmarshal(b, &obj); err != nil { - t.Errorf("Parse %v", string(b)) - return - } - if obj.Code != 0 { - t.Errorf("Server err code=%v, server=%v", obj.Code, obj.Server) - return - } - if obj.Data.Major == 0 && obj.Data.Minor == 0 { - t.Errorf("Invalid version %v", obj.Data) - return - } -} diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index e6afab909..8e5928a06 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -605,7 +605,7 @@ srs_error_t SrsRtcPlayStream::send_packet(SrsRtpPacket2*& pkt) // TODO: FIXME: Maybe refine for performance issue. if (!audio_tracks_.count(pkt->header.get_ssrc()) && !video_tracks_.count(pkt->header.get_ssrc())) { - srs_warn("ssrc %u not found", pkt->header.get_ssrc()); + srs_warn("RTC: Drop for ssrc %u not found", pkt->header.get_ssrc()); return err; } From f5ff28d47a37c5e77e15683ba40fbc2000d584cb Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 24 Mar 2021 10:50:13 +0800 Subject: [PATCH 029/563] RTC: Refine play stream init --- trunk/src/app/srs_app_rtc_conn.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 8e5928a06..536ab2d10 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -429,19 +429,18 @@ srs_error_t SrsRtcPlayStream::initialize(SrsRequest* req, std::map::iterator it = sub_relations.begin(); - while (it != sub_relations.end()) { - if (it->second->type_ == "audio") { - SrsRtcAudioSendTrack* track = new SrsRtcAudioSendTrack(session_, it->second); - audio_tracks_.insert(make_pair(it->first, track)); - } + for (map::iterator it = sub_relations.begin(); it != sub_relations.end(); ++it) { + uint32_t ssrc = it->first; + SrsRtcTrackDescription* desc = it->second; - if (it->second->type_ == "video") { - SrsRtcVideoSendTrack* track = new SrsRtcVideoSendTrack(session_, it->second); - video_tracks_.insert(make_pair(it->first, track)); - } - ++it; + if (desc->type_ == "audio") { + SrsRtcAudioSendTrack* track = new SrsRtcAudioSendTrack(session_, desc); + audio_tracks_.insert(make_pair(ssrc, track)); + } + + if (desc->type_ == "video") { + SrsRtcVideoSendTrack* track = new SrsRtcVideoSendTrack(session_, desc); + video_tracks_.insert(make_pair(ssrc, track)); } } From 67c5f8ad7e539e07905e68c5858800ec025cb3c2 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 24 Mar 2021 11:07:03 +0800 Subject: [PATCH 030/563] Refine code --- README.md | 6 +++--- trunk/3rdparty/st-srs/.gitignore | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c2d3f544e..5a7f7a13d 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # SRS(Simple Realtime Server) -![](http://ossrs.net/gif/v1/sls.gif?site=github.com&path=/srs/4.0release) -[![](https://circleci.com/gh/ossrs/srs/tree/4.0release.svg?style=svg&circle-token=1ef1d5b5b0cde6c8c282ed856a18199f9e8f85a9)](https://circleci.com/gh/ossrs/srs/tree/4.0release) -[![](https://codecov.io/gh/ossrs/srs/branch/4.0release/graph/badge.svg)](https://codecov.io/gh/ossrs/srs/branch/4.0release) +![](http://ossrs.net/gif/v1/sls.gif?site=github.com&path=/srs/develop) +[![](https://circleci.com/gh/ossrs/srs/tree/develop.svg?style=svg&circle-token=1ef1d5b5b0cde6c8c282ed856a18199f9e8f85a9)](https://circleci.com/gh/ossrs/srs/tree/develop) +[![](https://codecov.io/gh/ossrs/srs/branch/develop/graph/badge.svg)](https://codecov.io/gh/ossrs/srs/branch/develop) [![](https://cloud.githubusercontent.com/assets/2777660/22814959/c51cbe72-ef92-11e6-81cc-32b657b285d5.png)](https://github.com/ossrs/srs/wiki/v1_CN_Contact#wechat) SRS/4.0,[Leo][release4],是一个流媒体集群,支持RTMP/HLS/WebRTC/SRT/GB28181,高效、稳定、易用,简单而快乐。
diff --git a/trunk/3rdparty/st-srs/.gitignore b/trunk/3rdparty/st-srs/.gitignore index bd8ffdb68..e62491c96 100644 --- a/trunk/3rdparty/st-srs/.gitignore +++ b/trunk/3rdparty/st-srs/.gitignore @@ -10,4 +10,3 @@ googletest-* *.gcno coverage codecov - From 4c39cc7c2f6374145515e3a710423f34d8933e82 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 24 Mar 2021 12:29:17 +0800 Subject: [PATCH 031/563] RTC: Use fast parse TWCCID, ignore in packet parsing. 4.0.86 1. TWCC should not be passed from end to end. 2. Publisher TWCC information, should be ignore when pass to player 3. Player should regenerate its own TWCC. --- README.md | 1 + trunk/src/app/srs_app_rtc_conn.cpp | 2 +- trunk/src/core/srs_core_version4.hpp | 2 +- trunk/src/kernel/srs_kernel_rtc_rtp.cpp | 19 +++++++++++++------ trunk/src/kernel/srs_kernel_rtc_rtp.hpp | 8 +++++++- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e24dd9c03..89646ae61 100755 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ Other documents: ## V4 changes +* v4.0, 2021-03-24, RTC: Use fast parse TWCCID, ignore in packet parsing. 4.0.86 * v4.0, 2021-03-09, DTLS: Fix ARQ bug, use openssl timeout. 4.0.84 * v4.0, 2021-03-08, DTLS: Fix dead loop by duplicated Alert message. 4.0.83 * v4.0, 2021-03-08, Fix bug when client DTLS is passive. 4.0.82 diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 536ab2d10..4fdee4cf2 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -1122,7 +1122,7 @@ srs_error_t SrsRtcPublishStream::on_rtp(char* data, int nb_data) // 2. Server may send multiple duplicated NACK to client, and got more than one ARQ packet, which also fail SRTP. // so, we must parse the header before SRTP unprotect(which may fail and drop packet). uint16_t twcc_sn = 0; - if ((err = srs_rtp_fast_parse_twcc(data, nb_data, &extension_types_, twcc_sn)) == srs_success) { + if ((err = srs_rtp_fast_parse_twcc(data, nb_data, twcc_id_, twcc_sn)) == srs_success) { if((err = on_twcc(twcc_sn)) != srs_success) { return srs_error_wrap(err, "on twcc"); } diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index ddca1c39d..6a0585266 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -24,6 +24,6 @@ #ifndef SRS_CORE_VERSION4_HPP #define SRS_CORE_VERSION4_HPP -#define SRS_VERSION4_REVISION 85 +#define SRS_VERSION4_REVISION 86 #endif diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp index 30d42e017..f65e42653 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp @@ -80,7 +80,7 @@ uint8_t srs_rtp_fast_parse_pt(char* buf, int size) } return buf[1] & 0x7f; } -srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, SrsRtpExtensionTypes* ext_types, uint16_t& twcc_sn) +srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, uint8_t twcc_id, uint16_t& twcc_sn) { srs_error_t err = srs_success; @@ -129,8 +129,7 @@ srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, SrsRtpExtensionTypes* e uint8_t id = (v & 0xF0) >>4; uint8_t len = (v & 0x0F) + 1; - SrsRtpExtensionType xtype = ext_types->get_type(id); - if(xtype == kRtpExtensionTransportSequenceNumber) { + if(id == twcc_id) { twcc_sn = ntohs(*((uint16_t*)buf)); return err; } else { @@ -348,6 +347,7 @@ SrsRtpExtensions::SrsRtpExtensions() { types_ = NULL; has_ext_ = false; + decode_twcc_extension_ = false; } SrsRtpExtensions::~SrsRtpExtensions() @@ -415,10 +415,17 @@ srs_error_t SrsRtpExtensions::decode_0xbede(SrsBuffer* buf) SrsRtpExtensionType xtype = types_? types_->get_type(id) : kRtpExtensionNone; if (xtype == kRtpExtensionTransportSequenceNumber) { - if ((err = twcc_.decode(buf)) != srs_success) { - return srs_error_wrap(err, "decode twcc extension"); + if (decode_twcc_extension_) { + if ((err = twcc_.decode(buf)) != srs_success) { + return srs_error_wrap(err, "decode twcc extension"); + } + has_ext_ = true; + } else { + if (!buf->require(len+1+1)) { + return srs_error_new(ERROR_RTC_RTP_MUXER, "requires %d bytes", len+1+1); + } + buf->skip(len + 1 + 1); } - has_ext_ = true; } else if (xtype == kRtpExtensionAudioLevel) { if((err = audio_level_.decode(buf)) != srs_success) { return srs_error_wrap(err, "decode audio level extension"); diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp index 5c29d67b5..7b1fe5d24 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp @@ -64,7 +64,7 @@ class SrsRtpExtensionTypes; // Fast parse the SSRC from RTP packet. Return 0 if invalid. uint32_t srs_rtp_fast_parse_ssrc(char* buf, int size); uint8_t srs_rtp_fast_parse_pt(char* buf, int size); -srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, SrsRtpExtensionTypes* types, uint16_t& twcc_sn); +srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, uint8_t twcc_id, uint16_t& twcc_sn); // The "distance" between two uint16 number, for example: // distance(prev_value=3, value=5) === (int16_t)(uint16_t)((uint16_t)3-(uint16_t)5) === -2 @@ -177,6 +177,8 @@ class SrsRtpExtensions// : public ISrsCodec { private: bool has_ext_; + // by default, twcc isnot decoded. Because it is decoded by fast function(srs_rtp_fast_parse_twcc) + bool decode_twcc_extension_; private: // The extension types is used to decode the packet, which is reference to // the types in publish stream. @@ -188,6 +190,7 @@ public: SrsRtpExtensions(); virtual ~SrsRtpExtensions(); public: + void enable_twcc_decode() { decode_twcc_extension_ = true; } // SrsRtpExtensions::enable_twcc_decode inline bool exists() { return has_ext_; } // SrsRtpExtensions::exists void set_types_(SrsRtpExtensionTypes* types); srs_error_t get_twcc_sequence_number(uint16_t& twcc_sn); @@ -229,6 +232,7 @@ public: virtual srs_error_t encode(SrsBuffer* buf); virtual uint64_t nb_bytes(); public: + void enable_twcc_decode() { extensions_.enable_twcc_decode(); } // SrsRtpHeader::enable_twcc_decode void set_marker(bool v); bool get_marker() const; void set_payload_type(uint8_t v); @@ -326,6 +330,8 @@ public: // Copy the RTP packet. virtual SrsRtpPacket2* copy(); public: + // Parse the TWCC extension, ignore by default. + void enable_twcc_decode() { header.enable_twcc_decode(); } // SrsRtpPacket2::enable_twcc_decode // Get and set the payload of packet. void set_payload(ISrsRtpPayloader* p, SrsRtpPacketPayloadType pt) { payload_ = p; payload_type_ = pt; } ISrsRtpPayloader* payload() { return payload_; } From 875201b161dc017f2d4b0330cb37b46ea4f3e40e Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 24 Mar 2021 14:16:31 +0800 Subject: [PATCH 032/563] Merge 4.0release --- README.md | 1 + trunk/src/app/srs_app_rtc_conn.cpp | 2 +- trunk/src/core/srs_core_version4.hpp | 2 +- trunk/src/kernel/srs_kernel_rtc_rtp.cpp | 19 +++++++++++++------ trunk/src/kernel/srs_kernel_rtc_rtp.hpp | 8 +++++++- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5a7f7a13d..1101d38f4 100755 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ Other documents: ## V4 changes +* v4.0, 2021-03-24, RTC: Use fast parse TWCCID, ignore in packet parsing. 4.0.86 * v4.0, 2021-03-09, DTLS: Fix ARQ bug, use openssl timeout. 4.0.84 * v4.0, 2021-03-08, DTLS: Fix dead loop by duplicated Alert message. 4.0.83 * v4.0, 2021-03-08, Fix bug when client DTLS is passive. 4.0.82 diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index b31eec2e3..02cc6f5ac 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -1122,7 +1122,7 @@ srs_error_t SrsRtcPublishStream::on_rtp(char* data, int nb_data) // 2. Server may send multiple duplicated NACK to client, and got more than one ARQ packet, which also fail SRTP. // so, we must parse the header before SRTP unprotect(which may fail and drop packet). uint16_t twcc_sn = 0; - if ((err = srs_rtp_fast_parse_twcc(data, nb_data, &extension_types_, twcc_sn)) == srs_success) { + if ((err = srs_rtp_fast_parse_twcc(data, nb_data, twcc_id_, twcc_sn)) == srs_success) { if((err = on_twcc(twcc_sn)) != srs_success) { return srs_error_wrap(err, "on twcc"); } diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index 0e7f31831..e3c8a3820 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -26,6 +26,6 @@ #define VERSION_MAJOR 4 #define VERSION_MINOR 0 -#define SRS_VERSION4_REVISION 85 +#define SRS_VERSION4_REVISION 86 #endif diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp index 30d42e017..f65e42653 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp @@ -80,7 +80,7 @@ uint8_t srs_rtp_fast_parse_pt(char* buf, int size) } return buf[1] & 0x7f; } -srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, SrsRtpExtensionTypes* ext_types, uint16_t& twcc_sn) +srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, uint8_t twcc_id, uint16_t& twcc_sn) { srs_error_t err = srs_success; @@ -129,8 +129,7 @@ srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, SrsRtpExtensionTypes* e uint8_t id = (v & 0xF0) >>4; uint8_t len = (v & 0x0F) + 1; - SrsRtpExtensionType xtype = ext_types->get_type(id); - if(xtype == kRtpExtensionTransportSequenceNumber) { + if(id == twcc_id) { twcc_sn = ntohs(*((uint16_t*)buf)); return err; } else { @@ -348,6 +347,7 @@ SrsRtpExtensions::SrsRtpExtensions() { types_ = NULL; has_ext_ = false; + decode_twcc_extension_ = false; } SrsRtpExtensions::~SrsRtpExtensions() @@ -415,10 +415,17 @@ srs_error_t SrsRtpExtensions::decode_0xbede(SrsBuffer* buf) SrsRtpExtensionType xtype = types_? types_->get_type(id) : kRtpExtensionNone; if (xtype == kRtpExtensionTransportSequenceNumber) { - if ((err = twcc_.decode(buf)) != srs_success) { - return srs_error_wrap(err, "decode twcc extension"); + if (decode_twcc_extension_) { + if ((err = twcc_.decode(buf)) != srs_success) { + return srs_error_wrap(err, "decode twcc extension"); + } + has_ext_ = true; + } else { + if (!buf->require(len+1+1)) { + return srs_error_new(ERROR_RTC_RTP_MUXER, "requires %d bytes", len+1+1); + } + buf->skip(len + 1 + 1); } - has_ext_ = true; } else if (xtype == kRtpExtensionAudioLevel) { if((err = audio_level_.decode(buf)) != srs_success) { return srs_error_wrap(err, "decode audio level extension"); diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp index 5c29d67b5..7b1fe5d24 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp @@ -64,7 +64,7 @@ class SrsRtpExtensionTypes; // Fast parse the SSRC from RTP packet. Return 0 if invalid. uint32_t srs_rtp_fast_parse_ssrc(char* buf, int size); uint8_t srs_rtp_fast_parse_pt(char* buf, int size); -srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, SrsRtpExtensionTypes* types, uint16_t& twcc_sn); +srs_error_t srs_rtp_fast_parse_twcc(char* buf, int size, uint8_t twcc_id, uint16_t& twcc_sn); // The "distance" between two uint16 number, for example: // distance(prev_value=3, value=5) === (int16_t)(uint16_t)((uint16_t)3-(uint16_t)5) === -2 @@ -177,6 +177,8 @@ class SrsRtpExtensions// : public ISrsCodec { private: bool has_ext_; + // by default, twcc isnot decoded. Because it is decoded by fast function(srs_rtp_fast_parse_twcc) + bool decode_twcc_extension_; private: // The extension types is used to decode the packet, which is reference to // the types in publish stream. @@ -188,6 +190,7 @@ public: SrsRtpExtensions(); virtual ~SrsRtpExtensions(); public: + void enable_twcc_decode() { decode_twcc_extension_ = true; } // SrsRtpExtensions::enable_twcc_decode inline bool exists() { return has_ext_; } // SrsRtpExtensions::exists void set_types_(SrsRtpExtensionTypes* types); srs_error_t get_twcc_sequence_number(uint16_t& twcc_sn); @@ -229,6 +232,7 @@ public: virtual srs_error_t encode(SrsBuffer* buf); virtual uint64_t nb_bytes(); public: + void enable_twcc_decode() { extensions_.enable_twcc_decode(); } // SrsRtpHeader::enable_twcc_decode void set_marker(bool v); bool get_marker() const; void set_payload_type(uint8_t v); @@ -326,6 +330,8 @@ public: // Copy the RTP packet. virtual SrsRtpPacket2* copy(); public: + // Parse the TWCC extension, ignore by default. + void enable_twcc_decode() { header.enable_twcc_decode(); } // SrsRtpPacket2::enable_twcc_decode // Get and set the payload of packet. void set_payload(ISrsRtpPayloader* p, SrsRtpPacketPayloadType pt) { payload_ = p; payload_type_ = pt; } ISrsRtpPayloader* payload() { return payload_; } From 6c7e24fc6d8929122cb4cd96c0a2a2bdbb2c4be1 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 24 Mar 2021 15:21:40 +0800 Subject: [PATCH 033/563] RTC: Eliminate dead code, we nerver send offer --- trunk/src/app/srs_app_rtc_conn.cpp | 47 ------------------- trunk/src/app/srs_app_rtc_conn.hpp | 2 - trunk/src/app/srs_app_rtc_server.cpp | 70 ---------------------------- trunk/src/app/srs_app_rtc_server.hpp | 4 -- 4 files changed, 123 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 4fdee4cf2..5dceaabb8 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -1856,53 +1856,6 @@ srs_error_t SrsRtcConnection::add_player(SrsRequest* req, const SrsSdp& remote_s return err; } -srs_error_t SrsRtcConnection::add_player2(SrsRequest* req, bool unified_plan, SrsSdp& local_sdp) -{ - srs_error_t err = srs_success; - - if (_srs_rtc_hijacker) { - if ((err = _srs_rtc_hijacker->on_before_play(this, req)) != srs_success) { - return srs_error_wrap(err, "before play"); - } - } - - std::map play_sub_relations; - if ((err = fetch_source_capability(req, play_sub_relations)) != srs_success) { - return srs_error_wrap(err, "play negotiate"); - } - - if (!play_sub_relations.size()) { - return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no play relations"); - } - - SrsRtcStreamDescription* stream_desc = new SrsRtcStreamDescription(); - SrsAutoFree(SrsRtcStreamDescription, stream_desc); - - std::map::iterator it = play_sub_relations.begin(); - while (it != play_sub_relations.end()) { - SrsRtcTrackDescription* track_desc = it->second; - - if (track_desc->type_ == "audio" || !stream_desc->audio_track_desc_) { - stream_desc->audio_track_desc_ = track_desc->copy(); - } - - if (track_desc->type_ == "video") { - stream_desc->video_track_descs_.push_back(track_desc->copy()); - } - ++it; - } - - if ((err = generate_play_local_sdp(req, local_sdp, stream_desc, unified_plan)) != srs_success) { - return srs_error_wrap(err, "generate local sdp"); - } - - if ((err = create_player(req, play_sub_relations)) != srs_success) { - return srs_error_wrap(err, "create player"); - } - - return err; -} - srs_error_t SrsRtcConnection::initialize(SrsRequest* r, bool dtls, bool srtp, string username) { srs_error_t err = srs_success; diff --git a/trunk/src/app/srs_app_rtc_conn.hpp b/trunk/src/app/srs_app_rtc_conn.hpp index 4f0373a46..767d6b0dd 100644 --- a/trunk/src/app/srs_app_rtc_conn.hpp +++ b/trunk/src/app/srs_app_rtc_conn.hpp @@ -475,8 +475,6 @@ public: public: srs_error_t add_publisher(SrsRequest* request, const SrsSdp& remote_sdp, SrsSdp& local_sdp); srs_error_t add_player(SrsRequest* request, const SrsSdp& remote_sdp, SrsSdp& local_sdp); - // server send offer sdp to client, local sdp derivate from source stream desc. - srs_error_t add_player2(SrsRequest* request, bool unified_plan, SrsSdp& local_sdp); public: // Before initialize, user must set the local SDP, which is used to inititlize DTLS. srs_error_t initialize(SrsRequest* r, bool dtls, bool srtp, std::string username); diff --git a/trunk/src/app/srs_app_rtc_server.cpp b/trunk/src/app/srs_app_rtc_server.cpp index 9b0300682..6ca4cfbf4 100644 --- a/trunk/src/app/srs_app_rtc_server.cpp +++ b/trunk/src/app/srs_app_rtc_server.cpp @@ -624,76 +624,6 @@ srs_error_t SrsRtcServer::do_create_session( return err; } -srs_error_t SrsRtcServer::create_session2(SrsRequest* req, SrsSdp& local_sdp, const std::string& mock_eip, bool unified_plan, SrsRtcConnection** psession) -{ - srs_error_t err = srs_success; - - SrsContextId cid = _srs_context->get_id(); - - std::string local_pwd = srs_random_str(32); - // TODO: FIXME: Collision detect. - std::string local_ufrag = srs_random_str(8); - - SrsRtcConnection* session = new SrsRtcConnection(this, cid); - // first add player for negotiate local sdp media info - if ((err = session->add_player2(req, unified_plan, local_sdp)) != srs_success) { - srs_freep(session); - return srs_error_wrap(err, "add player2"); - } - *psession = session; - - local_sdp.set_dtls_role("actpass"); - local_sdp.set_ice_ufrag(local_ufrag); - local_sdp.set_ice_pwd(local_pwd); - local_sdp.set_fingerprint_algo("sha-256"); - local_sdp.set_fingerprint(_srs_rtc_dtls_certificate->get_fingerprint()); - - // We allows to mock the eip of server. - if (!mock_eip.empty()) { - string host; - int port = _srs_config->get_rtc_server_listen(); - srs_parse_hostport(mock_eip, host, port); - - local_sdp.add_candidate(host, port, "host"); - srs_trace("RTC: Use candidate mock_eip %s as %s:%d", mock_eip.c_str(), host.c_str(), port); - } else { - std::vector candidate_ips = get_candidate_ips(); - for (int i = 0; i < (int)candidate_ips.size(); ++i) { - local_sdp.add_candidate(candidate_ips[i], _srs_config->get_rtc_server_listen(), "host"); - } - srs_trace("RTC: Use candidates %s", srs_join_vector_string(candidate_ips, ", ").c_str()); - } - - session->set_local_sdp(local_sdp); - session->set_state(WAITING_ANSWER); - - return err; -} - -srs_error_t SrsRtcServer::setup_session2(SrsRtcConnection* session, SrsRequest* req, const SrsSdp& remote_sdp) -{ - srs_error_t err = srs_success; - - if (session->state() != WAITING_ANSWER) { - return err; - } - - // TODO: FIXME: Collision detect. - string username = session->get_local_sdp()->get_ice_ufrag() + ":" + remote_sdp.get_ice_ufrag(); - - if ((err = session->initialize(req, true, true, username)) != srs_success) { - return srs_error_wrap(err, "init"); - } - - // We allows username is optional, but it never empty here. - _srs_rtc_manager->add_with_name(username, session); - - session->set_remote_sdp(remote_sdp); - session->set_state(WAITING_STUN); - - return err; -} - SrsRtcConnection* SrsRtcServer::find_session_by_username(const std::string& username) { ISrsResource* conn = _srs_rtc_manager->find_by_name(username); diff --git a/trunk/src/app/srs_app_rtc_server.hpp b/trunk/src/app/srs_app_rtc_server.hpp index 0507666ea..ecae6d980 100644 --- a/trunk/src/app/srs_app_rtc_server.hpp +++ b/trunk/src/app/srs_app_rtc_server.hpp @@ -122,10 +122,6 @@ private: SrsRtcConnection* session, SrsRequest* req, const SrsSdp& remote_sdp, SrsSdp& local_sdp, const std::string& mock_eip, bool publish, bool dtls, bool srtp ); -public: - // We start offering, create_session2 to generate offer, setup_session2 to handle answer. - srs_error_t create_session2(SrsRequest* req, SrsSdp& local_sdp, const std::string& mock_eip, bool unified_plan, SrsRtcConnection** psession); - srs_error_t setup_session2(SrsRtcConnection* session, SrsRequest* req, const SrsSdp& remote_sdp); public: SrsRtcConnection* find_session_by_username(const std::string& ufrag); // interface ISrsHourGlass From 89f941fadc25d0c614994294df5518c5b50b5a70 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 24 Mar 2021 15:24:21 +0800 Subject: [PATCH 034/563] Refine version file --- trunk/src/core/srs_core.hpp | 3 --- trunk/src/core/srs_core_version4.hpp | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/trunk/src/core/srs_core.hpp b/trunk/src/core/srs_core.hpp index 0fbe07c39..6eb1e2f2b 100644 --- a/trunk/src/core/srs_core.hpp +++ b/trunk/src/core/srs_core.hpp @@ -25,10 +25,7 @@ #define SRS_CORE_HPP // The version config. -#define VERSION_MAJOR 4 -#define VERSION_MINOR 0 #include -#define VERSION_REVISION SRS_VERSION4_REVISION // The macros generated by configure script. #include diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index 6a0585266..ef82d1886 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -24,6 +24,8 @@ #ifndef SRS_CORE_VERSION4_HPP #define SRS_CORE_VERSION4_HPP -#define SRS_VERSION4_REVISION 86 +#define VERSION_MAJOR 4 +#define VERSION_MINOR 0 +#define VERSION_REVISION 86 #endif From 0cb05a295379de10e987a13643ff0eba732f2348 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 24 Mar 2021 18:58:01 +0800 Subject: [PATCH 035/563] RTC: Refine ID parsing --- trunk/src/kernel/srs_kernel_rtc_rtp.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp index f65e42653..73ba782e0 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp @@ -411,7 +411,7 @@ srs_error_t SrsRtpExtensions::decode_0xbede(SrsBuffer* buf) // Note that 'len' is the header extension element length, which is the // number of bytes - 1. uint8_t id = (v & 0xF0) >> 4; - uint8_t len = (v & 0x0F); + uint8_t len = (v & 0x0F) + 1; SrsRtpExtensionType xtype = types_? types_->get_type(id) : kRtpExtensionNone; if (xtype == kRtpExtensionTransportSequenceNumber) { @@ -421,10 +421,10 @@ srs_error_t SrsRtpExtensions::decode_0xbede(SrsBuffer* buf) } has_ext_ = true; } else { - if (!buf->require(len+1+1)) { - return srs_error_new(ERROR_RTC_RTP_MUXER, "requires %d bytes", len+1+1); + if (!buf->require(len+1)) { + return srs_error_new(ERROR_RTC_RTP_MUXER, "requires %d bytes", len+1); } - buf->skip(len + 1 + 1); + buf->skip(len + 1); } } else if (xtype == kRtpExtensionAudioLevel) { if((err = audio_level_.decode(buf)) != srs_success) { @@ -432,7 +432,7 @@ srs_error_t SrsRtpExtensions::decode_0xbede(SrsBuffer* buf) } has_ext_ = true; } else { - buf->skip(1 + (len + 1)); + buf->skip(1 + len); } } From d6c16a7e236e03eba754c763e865464ec82d4516 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 24 Mar 2021 20:01:02 +0800 Subject: [PATCH 036/563] RTC: Support WebRTC re-publish stream. 4.0.87 --- README.md | 1 + trunk/src/app/srs_app_rtc_conn.cpp | 27 +++++++++++++++ trunk/src/app/srs_app_rtc_conn.hpp | 9 +++-- trunk/src/app/srs_app_rtc_source.cpp | 51 +++++++++++++++++++++------- trunk/src/app/srs_app_rtc_source.hpp | 25 ++++++++++++-- trunk/src/core/srs_core_version4.hpp | 2 +- 6 files changed, 95 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 89646ae61..7875a5b09 100755 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ Other documents: ## V4 changes +* v4.0, 2021-03-24, RTC: Support WebRTC re-publish stream. 4.0.87 * v4.0, 2021-03-24, RTC: Use fast parse TWCCID, ignore in packet parsing. 4.0.86 * v4.0, 2021-03-09, DTLS: Fix ARQ bug, use openssl timeout. 4.0.84 * v4.0, 2021-03-08, DTLS: Fix dead loop by duplicated Alert message. 4.0.83 diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 5dceaabb8..e28ab2f42 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -466,6 +466,30 @@ srs_error_t SrsRtcPlayStream::initialize(SrsRequest* req, std::mapaudio_track_desc_ && audio_tracks_.size() == 1) { + uint32_t ssrc = desc->audio_track_desc_->ssrc_; + SrsRtcAudioSendTrack* track = audio_tracks_.begin()->second; + + audio_tracks_.clear(); + audio_tracks_.insert(make_pair(ssrc, track)); + } + + // Refresh the relation for video. + // TODO: FIMXE: Match by label? + if (desc->video_track_descs_.size() == 1 && desc->video_track_descs_.size() == 1) { + SrsRtcTrackDescription* vdesc = desc->video_track_descs_.at(0); + uint32_t ssrc = vdesc->ssrc_; + SrsRtcVideoSendTrack* track = video_tracks_.begin()->second; + + video_tracks_.clear(); + video_tracks_.insert(make_pair(ssrc, track)); + } +} + srs_error_t SrsRtcPlayStream::on_reload_vhost_play(string vhost) { if (req_->vhost != vhost) { @@ -546,6 +570,9 @@ srs_error_t SrsRtcPlayStream::cycle() return srs_error_wrap(err, "create consumer, source=%s", req_->get_stream_url().c_str()); } + srs_assert(consumer); + consumer->set_handler(this); + // TODO: FIXME: Dumps the SPS/PPS from gop cache, without other frames. if ((err = source->consumer_dumps(consumer)) != srs_success) { return srs_error_wrap(err, "dumps consumer, url=%s", req_->get_stream_url().c_str()); diff --git a/trunk/src/app/srs_app_rtc_conn.hpp b/trunk/src/app/srs_app_rtc_conn.hpp index 767d6b0dd..12187fae2 100644 --- a/trunk/src/app/srs_app_rtc_conn.hpp +++ b/trunk/src/app/srs_app_rtc_conn.hpp @@ -211,7 +211,7 @@ public: // A RTC play stream, client pull and play stream from SRS. class SrsRtcPlayStream : virtual public ISrsCoroutineHandler, virtual public ISrsReloadHandler - , virtual public ISrsHourGlass, virtual public ISrsRtcPLIWorkerHandler + , virtual public ISrsHourGlass, virtual public ISrsRtcPLIWorkerHandler, public ISrsRtcStreamChangeCallback { private: SrsContextId cid_; @@ -235,13 +235,16 @@ private: bool nack_enabled_; bool nack_no_copy_; private: - // Whether palyer started. + // Whether player started. bool is_started; public: SrsRtcPlayStream(SrsRtcConnection* s, const SrsContextId& cid); virtual ~SrsRtcPlayStream(); public: srs_error_t initialize(SrsRequest* request, std::map sub_relations); +// Interface ISrsRtcStreamChangeCallback +public: + void on_stream_change(SrsRtcStreamDescription* desc); // interface ISrsReloadHandler public: virtual srs_error_t on_reload_vhost_play(std::string vhost); @@ -268,7 +271,7 @@ private: srs_error_t on_rtcp_ps_feedback(SrsRtcpPsfbCommon* rtcp); srs_error_t on_rtcp_rr(SrsRtcpRR* rtcp); uint32_t get_video_publish_ssrc(uint32_t play_ssrc); -// inteface ISrsRtcPLIWorkerHandler +// Interface ISrsRtcPLIWorkerHandler public: virtual srs_error_t do_request_keyframe(uint32_t ssrc, SrsContextId cid); }; diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index d111d5e57..e9a34533a 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -152,10 +152,19 @@ SrsNtp SrsNtp::to_time_ms(uint64_t ntp) return srs_ntp; } +ISrsRtcStreamChangeCallback::ISrsRtcStreamChangeCallback() +{ +} + +ISrsRtcStreamChangeCallback::~ISrsRtcStreamChangeCallback() +{ +} + SrsRtcConsumer::SrsRtcConsumer(SrsRtcStream* s) { source = s; should_update_source_id = false; + handler_ = NULL; mw_wait = srs_cond_new(); mw_min_msgs = 0; @@ -231,6 +240,13 @@ void SrsRtcConsumer::wait(int nb_msgs) srs_cond_wait(mw_wait); } +void SrsRtcConsumer::on_stream_change(SrsRtcStreamDescription* desc) +{ + if (handler_) { + handler_->on_stream_change(desc); + } +} + SrsRtcStreamManager::SrsRtcStreamManager() { lock = NULL; @@ -354,24 +370,34 @@ void SrsRtcStream::update_auth(SrsRequest* r) req->update_auth(r); } -srs_error_t SrsRtcStream::on_source_id_changed(SrsContextId id) +srs_error_t SrsRtcStream::on_source_changed() { srs_error_t err = srs_success; - if (!_source_id.compare(id)) { - return err; + // Update context id if changed. + bool id_changed = false; + const SrsContextId& id = _srs_context->get_id(); + if (_source_id.compare(id)) { + id_changed = true; + + if (_pre_source_id.empty()) { + _pre_source_id = id; + } + _source_id = id; } - if (_pre_source_id.empty()) { - _pre_source_id = id; - } - _source_id = id; - - // notice all consumer + // Notify all consumers. std::vector::iterator it; for (it = consumers.begin(); it != consumers.end(); ++it) { SrsRtcConsumer* consumer = *it; - consumer->update_source_id(); + + // Notify if context id changed. + if (id_changed) { + consumer->update_source_id(); + } + + // Notify about stream description. + consumer->on_stream_change(stream_desc_); } return err; @@ -456,9 +482,8 @@ srs_error_t SrsRtcStream::on_publish() is_created_ = true; is_delivering_packets_ = true; - // whatever, the publish thread is the source or edge source, - // save its id to srouce id. - if ((err = on_source_id_changed(_srs_context->get_id())) != srs_success) { + // Notify the consumers about stream change event. + if ((err = on_source_changed()) != srs_success) { return srs_error_wrap(err, "source id change"); } diff --git a/trunk/src/app/srs_app_rtc_source.hpp b/trunk/src/app/srs_app_rtc_source.hpp index f22bffc27..1ea9c5a41 100644 --- a/trunk/src/app/srs_app_rtc_source.hpp +++ b/trunk/src/app/srs_app_rtc_source.hpp @@ -75,6 +75,17 @@ public: static uint64_t kMagicNtpFractionalUnit; }; +// When RTC stream publish and re-publish. +class ISrsRtcStreamChangeCallback +{ +public: + ISrsRtcStreamChangeCallback(); + virtual ~ISrsRtcStreamChangeCallback(); +public: + virtual void on_stream_change(SrsRtcStreamDescription* desc) = 0; +}; + +// The RTC stream consumer, consume packets from RTC stream source. class SrsRtcConsumer { private: @@ -87,6 +98,9 @@ private: srs_cond_t mw_wait; bool mw_waiting; int mw_min_msgs; +private: + // The callback for stream change event. + ISrsRtcStreamChangeCallback* handler_; public: SrsRtcConsumer(SrsRtcStream* s); virtual ~SrsRtcConsumer(); @@ -100,6 +114,9 @@ public: virtual srs_error_t dump_packet(SrsRtpPacket2** ppkt); // Wait for at-least some messages incoming in queue. virtual void wait(int nb_msgs); +public: + void set_handler(ISrsRtcStreamChangeCallback* h) { handler_ = h; } // SrsRtcConsumer::set_handler() + void on_stream_change(SrsRtcStreamDescription* desc); }; class SrsRtcStreamManager @@ -154,7 +171,7 @@ private: // For publish, it's the publish client id. // For edge, it's the edge ingest id. // when source id changed, for example, the edge reconnect, - // invoke the on_source_id_changed() to let all clients know. + // invoke the on_source_changed() to let all clients know. SrsContextId _source_id; // previous source id. SrsContextId _pre_source_id; @@ -180,8 +197,10 @@ public: virtual srs_error_t initialize(SrsRequest* r); // Update the authentication information in request. virtual void update_auth(SrsRequest* r); - // The source id changed. - virtual srs_error_t on_source_id_changed(SrsContextId id); +private: + // The stream source changed. + virtual srs_error_t on_source_changed(); +public: // Get current source id. virtual SrsContextId source_id(); virtual SrsContextId pre_source_id(); diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index ef82d1886..2add34d5b 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -26,6 +26,6 @@ #define VERSION_MAJOR 4 #define VERSION_MINOR 0 -#define VERSION_REVISION 86 +#define VERSION_REVISION 87 #endif From f2d0c34244006dbc9f206a3bbe2401e038471085 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 31 Mar 2021 17:46:45 +0800 Subject: [PATCH 037/563] RTC: Refine comments for SrsRtpPacket2 --- trunk/src/kernel/srs_kernel_rtc_rtp.cpp | 1 + trunk/src/kernel/srs_kernel_rtc_rtp.hpp | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp index 73ba782e0..4b60ca353 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp @@ -1040,6 +1040,7 @@ srs_error_t SrsRtpPacket2::decode(SrsBuffer* buf) nalu_type = SrsAvcNaluType((uint8_t)(buf->head()[0] & kNalTypeMask)); } + // TODO: FIXME: We should keep payload to NULL and return if buffer is empty. // If user set the decode handler, call it to set the payload. if (decode_handler) { decode_handler->on_before_decode_payload(this, buf, &payload_, &payload_type_); diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp index 7b1fe5d24..09ce5c716 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp @@ -294,8 +294,10 @@ private: private: // The original shared message, all RTP packets can refer to its data. // Note that the size of shared msg, is not the packet size, it's a larger aligned buffer. + // @remark Note that it may point to the whole RTP packet(for RTP parser, which decode RTP packet from buffer), + // and it may point to the RTP payload(for RTMP to RTP, which build RTP header and payload). SrsSharedPtrMessage* shared_buffer_; - // The size of original packet. + // The size of RTP packet or RTP payload. int actual_buffer_size_; // Helper fields. public: @@ -333,6 +335,7 @@ public: // Parse the TWCC extension, ignore by default. void enable_twcc_decode() { header.enable_twcc_decode(); } // SrsRtpPacket2::enable_twcc_decode // Get and set the payload of packet. + // @remark Note that return NULL if no payload. void set_payload(ISrsRtpPayloader* p, SrsRtpPacketPayloadType pt) { payload_ = p; payload_type_ = pt; } ISrsRtpPayloader* payload() { return payload_; } // Set the padding of RTP packet. From 3d4d250eb96107b751ed0305e10cc1cfa5838162 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 31 Mar 2021 18:22:28 +0800 Subject: [PATCH 038/563] RTC: Update performance data --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 7875a5b09..ae624f9a0 100755 --- a/README.md +++ b/README.md @@ -1061,6 +1061,7 @@ The performance benchmark data and corelative commits are listed here. * See also: [Performance for x86/x64 Test Guide][v1_CN_Performance]. * See also: [Performance for RaspberryPi][v1_CN_RaspberryPi]. * For multiple processes performance, read [#775: REUSEPORT][bug #775] or OriginCluster([CN][v3_EN_OriginCluster]/[EN][v3_EN_OriginCluster]) or [go-oryx][oryx]. +* For RTC benchmark, please use [srs-bench](https://github.com/ossrs/srs-bench/tree/feature/rtc#usage).
**Play RTMP benchmark** @@ -1114,6 +1115,19 @@ The data for playing HTTP FLV was benchmarked by [SB][srs-bench]: | 2014-05-24 | 2.0.168 | 2.3k(2300) | players | 92% | 276MB | [code][p17] | | 2014-05-24 | 2.0.167 | 1.0k(1000) | players | 82% | 86MB | - | + +**RTC benchmark** + +The RTC benchmark data, by [srs-bench](https://github.com/ossrs/srs-bench/tree/feature/rtc#usage): + + +| Update | SRS | Clients | Type | CPU | Memory | Threads | +| ------------- | --------- | ------------- | ------------- | --------- | -------- | ------- | +| 2021-03-31 | 4.0.87 | 550 | publishers | ~86% | 1.3GB | 1 | +| 2021-03-31 | 4.0.87 | 800 | players | ~94% | 444MB | 1 | + +> Note: CentOS7, 500Kbps, 4CPU, 2.5 GHz Intel Xeon Platinum 8163/8269CY. + **Latency benchmark** From 4d5c7e0a7319c52ae2ac671de93e6f11f4d63d4b Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 1 Apr 2021 10:21:19 +0800 Subject: [PATCH 039/563] RTC: Fix object cache bug, reset payload when recycle --- trunk/src/kernel/srs_kernel_rtc_rtp.cpp | 16 ++++++++++++++++ trunk/src/kernel/srs_kernel_rtc_rtp.hpp | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp index 4b60ca353..d4f5d01a1 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp @@ -1078,6 +1078,12 @@ SrsRtpRawPayload::~SrsRtpRawPayload() { } +bool SrsRtpRawPayload::recycle() +{ + payload=NULL; nn_payload=0; + return true; +} + uint64_t SrsRtpRawPayload::nb_bytes() { return nn_payload; @@ -1538,6 +1544,16 @@ SrsRtpFUAPayload2::~SrsRtpFUAPayload2() { } +bool SrsRtpFUAPayload2::recycle() +{ + start = end = false; + nri = nalu_type = (SrsAvcNaluType)0; + + payload = NULL; + size = 0; + return true; +} + uint64_t SrsRtpFUAPayload2::nb_bytes() { return 2 + size; diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp index 09ce5c716..2433e92cf 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp @@ -473,7 +473,7 @@ public: SrsRtpRawPayload(); virtual ~SrsRtpRawPayload(); public: - bool recycle() { return true; } + bool recycle(); // interface ISrsRtpPayloader public: virtual uint64_t nb_bytes(); @@ -573,7 +573,7 @@ public: SrsRtpFUAPayload2(); virtual ~SrsRtpFUAPayload2(); public: - bool recycle() { return true; } + bool recycle(); // interface ISrsRtpPayloader public: virtual uint64_t nb_bytes(); From f4b791a9d5e78e1259be39c3737adcbe38214862 Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 1 Apr 2021 10:22:07 +0800 Subject: [PATCH 040/563] Kernel: Never assert for SrsBuffer::require --- trunk/src/kernel/srs_kernel_buffer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/trunk/src/kernel/srs_kernel_buffer.cpp b/trunk/src/kernel/srs_kernel_buffer.cpp index ff5cdbeb8..6e7e45754 100644 --- a/trunk/src/kernel/srs_kernel_buffer.cpp +++ b/trunk/src/kernel/srs_kernel_buffer.cpp @@ -99,7 +99,9 @@ bool SrsBuffer::empty() bool SrsBuffer::require(int required_size) { - srs_assert(required_size >= 0); + if (required_size < 0) { + return false; + } return required_size <= nb_bytes - (p - bytes); } From 7ac4a4f4ca260a895d2bb573f8f778d804b39c81 Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 1 Apr 2021 10:24:37 +0800 Subject: [PATCH 041/563] RTC: Fix audio track description bug --- trunk/src/app/srs_app_rtc_conn.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index e28ab2f42..7bdf38a3c 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -941,7 +941,10 @@ srs_error_t SrsRtcPublishStream::initialize(SrsRequest* r, SrsRtcStreamDescripti req = r->copy(); - audio_tracks_.push_back(new SrsRtcAudioRecvTrack(session_, stream_desc->audio_track_desc_)); + if (stream_desc->audio_track_desc_) { + audio_tracks_.push_back(new SrsRtcAudioRecvTrack(session_, stream_desc->audio_track_desc_)); + } + for (int i = 0; i < (int)stream_desc->video_track_descs_.size(); ++i) { SrsRtcTrackDescription* desc = stream_desc->video_track_descs_.at(i); video_tracks_.push_back(new SrsRtcVideoRecvTrack(session_, desc)); @@ -1862,7 +1865,8 @@ srs_error_t SrsRtcConnection::add_player(SrsRequest* req, const SrsSdp& remote_s while (it != play_sub_relations.end()) { SrsRtcTrackDescription* track_desc = it->second; - if (track_desc->type_ == "audio" || !stream_desc->audio_track_desc_) { + // TODO: FIXME: we only support one audio track. + if (track_desc->type_ == "audio" && !stream_desc->audio_track_desc_) { stream_desc->audio_track_desc_ = track_desc->copy(); } From bd1752a4b2f37a519a12ac3bcf8b47692886d228 Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 1 Apr 2021 10:47:50 +0800 Subject: [PATCH 042/563] RTC: Fix TWCC send bug --- trunk/src/app/srs_app_rtc_conn.cpp | 28 ++++++------ trunk/src/kernel/srs_kernel_rtc_rtcp.cpp | 57 +++++++++++++++++------- trunk/src/kernel/srs_kernel_rtc_rtcp.hpp | 7 +-- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 7bdf38a3c..e5d013853 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -1345,23 +1345,25 @@ srs_error_t SrsRtcPublishStream::send_periodic_twcc() ++_srs_pps_srtcps->sugar; - char pkt[kRtcpPacketSize]; - SrsBuffer *buffer = new SrsBuffer(pkt, sizeof(pkt)); - SrsAutoFree(SrsBuffer, buffer); + // limit the max count=1024 to avoid dead loop. + for (int i = 0; i < 1024 && rtcp_twcc_.need_feedback(); ++i) { + char pkt[kMaxUDPDataSize]; + SrsBuffer *buffer = new SrsBuffer(pkt, sizeof(pkt)); + SrsAutoFree(SrsBuffer, buffer); - rtcp_twcc_.set_feedback_count(twcc_fb_count_); - twcc_fb_count_++; + rtcp_twcc_.set_feedback_count(twcc_fb_count_); + twcc_fb_count_++; - if((err = rtcp_twcc_.encode(buffer)) != srs_success) { - return srs_error_wrap(err, "encode, count=%u", twcc_fb_count_); + if((err = rtcp_twcc_.encode(buffer)) != srs_success) { + return srs_error_wrap(err, "encode, count=%u", twcc_fb_count_); + } + + if((err = session_->send_rtcp(pkt, buffer->pos())) != srs_success) { + return srs_error_wrap(err, "send twcc, count=%u", twcc_fb_count_); + } } - int nb_protected_buf = buffer->pos(); - if ((err = session_->transport_->protect_rtcp(pkt, &nb_protected_buf)) != srs_success) { - return srs_error_wrap(err, "protect rtcp, size=%u", nb_protected_buf); - } - - return session_->sendonly_skt->sendto(pkt, nb_protected_buf, 0); + return err; } srs_error_t SrsRtcPublishStream::on_rtcp(SrsRtcpCommon* rtcp) diff --git a/trunk/src/kernel/srs_kernel_rtc_rtcp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtcp.cpp index fb91a5bc7..2552887fe 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtcp.cpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtcp.cpp @@ -716,9 +716,9 @@ SrsRtcpTWCC::SrsRtcpTWCC(uint32_t sender_ssrc) : pkt_len(0) ssrc_ = sender_ssrc; media_ssrc_ = 0; base_sn_ = 0; - packet_count_ = 0; reference_time_ = 0; fb_pkt_count_ = 0; + next_base_sn_ = 0; } SrsRtcpTWCC::~SrsRtcpTWCC() @@ -731,6 +731,7 @@ void SrsRtcpTWCC::clear() pkt_deltas_.clear(); recv_packets_.clear(); recv_sns_.clear(); + next_base_sn_ = 0; } uint32_t SrsRtcpTWCC::get_media_ssrc() const @@ -751,11 +752,6 @@ uint8_t SrsRtcpTWCC::get_feedback_count() const { return fb_pkt_count_; } - -uint16_t SrsRtcpTWCC::get_packet_status_count() const -{ - return packet_count_; -} vector SrsRtcpTWCC::get_packet_chucks() const { @@ -776,11 +772,6 @@ void SrsRtcpTWCC::set_base_sn(uint16_t sn) base_sn_ = sn; } -void SrsRtcpTWCC::set_packet_status_count(uint16_t count) -{ - packet_count_ = count; -} - void SrsRtcpTWCC::set_reference_time(uint32_t time) { reference_time_ = time; @@ -865,7 +856,7 @@ srs_error_t SrsRtcpTWCC::decode(SrsBuffer *buffer) uint64_t SrsRtcpTWCC::nb_bytes() { - return kRtcpPacketSize; + return kMaxUDPDataSize; } srs_utime_t SrsRtcpTWCC::calculate_delta_us(srs_utime_t ts, srs_utime_t last) @@ -1057,7 +1048,9 @@ srs_error_t SrsRtcpTWCC::encode(SrsBuffer *buffer) err = do_encode(buffer); - clear(); + if (err != srs_success || next_base_sn_ == 0) { + clear(); + } return err; } @@ -1099,21 +1092,41 @@ srs_error_t SrsRtcpTWCC::do_encode(SrsBuffer *buffer) } pkt_len = kTwccFbPktHeaderSize; + set::iterator it_sn = recv_sns_.begin(); - base_sn_ = *it_sn; + if (!next_base_sn_) { + base_sn_ = *it_sn; + } else { + base_sn_ = next_base_sn_; + it_sn = recv_sns_.find(base_sn_); + } + map::iterator it_ts = recv_packets_.find(base_sn_); srs_utime_t ts = it_ts->second; + reference_time_ = (ts % kTwccFbReferenceTimeDivisor) / kTwccFbTimeMultiplier; srs_utime_t last_ts = (srs_utime_t)(reference_time_) * kTwccFbTimeMultiplier; + uint16_t last_sn = base_sn_; - packet_count_ = recv_packets_.size(); + uint16_t packet_count = 0; // encode chunk SrsRtcpTWCC::SrsRtcpTWCCChunk chunk; for(; it_sn != recv_sns_.end(); ++it_sn) { + // check whether exceed buffer len + // max recv_delta_size = 2 + if (pkt_len + 2 >= buffer->left()) { + break; + } + uint16_t current_sn = *it_sn; // calculate delta it_ts = recv_packets_.find(current_sn); + if (it_ts == recv_packets_.end()) { + continue; + } + + packet_count++; srs_utime_t delta_us = calculate_delta_us(it_ts->second, last_ts); int16_t delta = delta_us; if(delta != delta_us) { @@ -1124,7 +1137,7 @@ srs_error_t SrsRtcpTWCC::do_encode(SrsBuffer *buffer) // lost packet for(uint16_t lost_sn = last_sn + 1; lost_sn < current_sn; ++lost_sn) { process_pkt_chunk(chunk, 0); - packet_count_++; + packet_count++; } } @@ -1138,6 +1151,13 @@ srs_error_t SrsRtcpTWCC::do_encode(SrsBuffer *buffer) last_ts += delta * kTwccFbDeltaUnit; pkt_len += recv_delta_size; last_sn = current_sn; + + recv_packets_.erase(it_ts); + } + + next_base_sn_ = 0; + if (it_sn != recv_sns_.end()) { + next_base_sn_ = *it_sn; } if(0 < chunk.size) { @@ -1159,7 +1179,7 @@ srs_error_t SrsRtcpTWCC::do_encode(SrsBuffer *buffer) } buffer->write_4bytes(media_ssrc_); buffer->write_2bytes(base_sn_); - buffer->write_2bytes(packet_count_); + buffer->write_2bytes(packet_count); buffer->write_3bytes(reference_time_); buffer->write_1bytes(fb_pkt_count_); @@ -1181,6 +1201,9 @@ srs_error_t SrsRtcpTWCC::do_encode(SrsBuffer *buffer) pkt_len++; } + encoded_chucks_.clear(); + pkt_deltas_.clear(); + return err; } diff --git a/trunk/src/kernel/srs_kernel_rtc_rtcp.hpp b/trunk/src/kernel/srs_kernel_rtc_rtcp.hpp index 279a08e27..b469b814a 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtcp.hpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtcp.hpp @@ -36,6 +36,9 @@ const int kRtcpPacketSize = 1500; const uint8_t kRtcpVersion = 0x2; +// 1500 - 20(ip_header) - 8(udp_header) +const int kMaxUDPDataSize = 1472; + // RTCP Packet Types, @see http://www.networksorcery.com/enp/protocol/rtcp.htm enum SrsRtcpType { @@ -266,7 +269,6 @@ class SrsRtcpTWCC : public SrsRtcpCommon private: uint32_t media_ssrc_; uint16_t base_sn_; - uint16_t packet_count_; int32_t reference_time_; uint8_t fb_pkt_count_; std::vector encoded_chucks_; @@ -284,6 +286,7 @@ private: }; int pkt_len; + uint16_t next_base_sn_; private: void clear(); srs_utime_t calculate_delta_us(srs_utime_t ts, srs_utime_t last); @@ -302,7 +305,6 @@ public: uint32_t get_media_ssrc() const; uint16_t get_base_sn() const; - uint16_t get_packet_status_count() const; uint32_t get_reference_time() const; uint8_t get_feedback_count() const; std::vector get_packet_chucks() const; @@ -310,7 +312,6 @@ public: void set_media_ssrc(uint32_t ssrc); void set_base_sn(uint16_t sn); - void set_packet_status_count(uint16_t count); void set_reference_time(uint32_t time); void set_feedback_count(uint8_t count); void add_packet_chuck(uint16_t chuck); From aa5d872b8c42bd5ef7eabc924771f274dad7cdab Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 1 Apr 2021 10:50:20 +0800 Subject: [PATCH 043/563] RTC: Refine TWCC and SDP exchange. 4.0.88 --- README.md | 3 ++- trunk/src/core/srs_core_version4.hpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae624f9a0..81ba1ebd0 100755 --- a/README.md +++ b/README.md @@ -190,8 +190,9 @@ Other documents: ## V4 changes +* v4.0, 2021-04-01, RTC: Refine TWCC and SDP exchange. 4.0.88 * v4.0, 2021-03-24, RTC: Support WebRTC re-publish stream. 4.0.87 -* v4.0, 2021-03-24, RTC: Use fast parse TWCCID, ignore in packet parsing. 4.0.86 +* v4.0, 2021-03-24, RTC: Use fast parse TWCC-ID, ignore in packet parsing. 4.0.86 * v4.0, 2021-03-09, DTLS: Fix ARQ bug, use openssl timeout. 4.0.84 * v4.0, 2021-03-08, DTLS: Fix dead loop by duplicated Alert message. 4.0.83 * v4.0, 2021-03-08, Fix bug when client DTLS is passive. 4.0.82 diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index 2add34d5b..42f973eaa 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -26,6 +26,6 @@ #define VERSION_MAJOR 4 #define VERSION_MINOR 0 -#define VERSION_REVISION 87 +#define VERSION_REVISION 88 #endif From 7823d75a38288b0dbc24f8fded937beb8b57456a Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 1 Apr 2021 14:46:28 +0800 Subject: [PATCH 044/563] RTC: Refine payload NALU type parser --- trunk/src/app/srs_app_rtc_source.cpp | 4 +++- trunk/src/kernel/srs_kernel_rtc_rtp.cpp | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index e9a34533a..17630bddc 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -1965,7 +1965,9 @@ void SrsRtcVideoRecvTrack::on_before_decode_payload(SrsRtpPacket2* pkt, SrsBuffe return; } - uint8_t v = (uint8_t)pkt->nalu_type; + uint8_t v = (uint8_t)(buf->head()[0] & kNalTypeMask); + pkt->nalu_type = SrsAvcNaluType(v); + if (v == kStapA) { *ppayload = new SrsRtpSTAPPayload(); *ppt = SrsRtpPacketPayloadTypeSTAP; diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp index d4f5d01a1..6dc5e41c6 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp @@ -1035,11 +1035,6 @@ srs_error_t SrsRtpPacket2::decode(SrsBuffer* buf) } buf->set_size(buf->size() - padding); - // Try to parse the NALU type for video decoder. - if (!buf->empty()) { - nalu_type = SrsAvcNaluType((uint8_t)(buf->head()[0] & kNalTypeMask)); - } - // TODO: FIXME: We should keep payload to NULL and return if buffer is empty. // If user set the decode handler, call it to set the payload. if (decode_handler) { From 3c59fedab6d01630b9dd92917227b682c3d79e5d Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 1 Apr 2021 17:34:47 +0800 Subject: [PATCH 045/563] RTC: Fix memory leak --- trunk/src/app/srs_app_rtc_conn.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index e5d013853..beefdb000 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -1699,7 +1699,11 @@ SrsRtcConnection::~SrsRtcConnection() srs_freep(addr); } - srs_freep(cache_iov_); + if (true) { + char* iov_base = (char*)cache_iov_->iov_base; + srs_freepa(iov_base); + srs_freep(cache_iov_); + } srs_freep(cache_buffer_); srs_freep(transport_); From 8d9dd532b93f125ece4bab7a5e319c611714bcef Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 4 Apr 2021 18:32:15 +0800 Subject: [PATCH 046/563] RTC: Fix bug for republish stream. 4.0.89 --- trunk/src/app/srs_app_rtc_conn.cpp | 4 ++-- trunk/src/core/srs_core_version4.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index beefdb000..c312af7d3 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -470,7 +470,7 @@ void SrsRtcPlayStream::on_stream_change(SrsRtcStreamDescription* desc) { // Refresh the relation for audio. // TODO: FIMXE: Match by label? - if (desc->audio_track_desc_ && audio_tracks_.size() == 1) { + if (desc && desc->audio_track_desc_ && audio_tracks_.size() == 1) { uint32_t ssrc = desc->audio_track_desc_->ssrc_; SrsRtcAudioSendTrack* track = audio_tracks_.begin()->second; @@ -480,7 +480,7 @@ void SrsRtcPlayStream::on_stream_change(SrsRtcStreamDescription* desc) // Refresh the relation for video. // TODO: FIMXE: Match by label? - if (desc->video_track_descs_.size() == 1 && desc->video_track_descs_.size() == 1) { + if (desc && desc->video_track_descs_.size() == 1 && desc->video_track_descs_.size() == 1) { SrsRtcTrackDescription* vdesc = desc->video_track_descs_.at(0); uint32_t ssrc = vdesc->ssrc_; SrsRtcVideoSendTrack* track = video_tracks_.begin()->second; diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index 42f973eaa..54ba852bf 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -26,6 +26,6 @@ #define VERSION_MAJOR 4 #define VERSION_MINOR 0 -#define VERSION_REVISION 88 +#define VERSION_REVISION 89 #endif From 96003d4a52236eb82faa8c820cccb44dfdb3ca15 Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 4 Apr 2021 19:01:42 +0800 Subject: [PATCH 047/563] RTC: Fix bug for republish stream. 4.0.89 --- trunk/src/app/srs_app_rtc_source.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index 17630bddc..5d4c333a2 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -482,11 +482,6 @@ srs_error_t SrsRtcStream::on_publish() is_created_ = true; is_delivering_packets_ = true; - // Notify the consumers about stream change event. - if ((err = on_source_changed()) != srs_success) { - return srs_error_wrap(err, "source id change"); - } - // Create a new bridger, because it's been disposed when unpublish. #ifdef SRS_FFMPEG_FIT SrsRtcFromRtmpBridger* impl = new SrsRtcFromRtmpBridger(this); @@ -497,6 +492,11 @@ srs_error_t SrsRtcStream::on_publish() bridger_->setup(impl); #endif + // Notify the consumers about stream change event. + if ((err = on_source_changed()) != srs_success) { + return srs_error_wrap(err, "source id change"); + } + // TODO: FIXME: Handle by statistic. return err; From db7e820f046846830ad0d750ec310d864875f845 Mon Sep 17 00:00:00 2001 From: winlin Date: Mon, 5 Apr 2021 08:28:16 +0800 Subject: [PATCH 048/563] RTC: Fix DTLS warnings for HTTP api --- trunk/src/app/srs_app_rtc_dtls.cpp | 55 +++++++++++++++++++++++++++++- trunk/src/app/srs_app_rtc_dtls.hpp | 19 +++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/trunk/src/app/srs_app_rtc_dtls.cpp b/trunk/src/app/srs_app_rtc_dtls.cpp index a528f2631..dbb17a685 100644 --- a/trunk/src/app/srs_app_rtc_dtls.cpp +++ b/trunk/src/app/srs_app_rtc_dtls.cpp @@ -961,10 +961,63 @@ bool SrsDtlsServerImpl::is_dtls_client() return false; } +SrsDtlsEmptyImpl::SrsDtlsEmptyImpl() : SrsDtlsImpl(NULL) +{ + handshake_done_for_us = true; +} + +SrsDtlsEmptyImpl::~SrsDtlsEmptyImpl() +{ +} + +srs_error_t SrsDtlsEmptyImpl::initialize(std::string version, std::string role) +{ + return srs_success; +} + +srs_error_t SrsDtlsEmptyImpl::start_active_handshake() +{ + return srs_success; +} + +bool SrsDtlsEmptyImpl::should_reset_timer() +{ + return false; +} + +srs_error_t SrsDtlsEmptyImpl::on_dtls(char* data, int nb_data) +{ + return srs_success; +} + +srs_error_t SrsDtlsEmptyImpl::get_srtp_key(std::string& recv_key, std::string& send_key) +{ + return srs_success; +} + +void SrsDtlsEmptyImpl::callback_by_ssl(std::string type, std::string desc) +{ +} + +srs_error_t SrsDtlsEmptyImpl::on_final_out_data(uint8_t* data, int size) +{ + return srs_success; +} + +srs_error_t SrsDtlsEmptyImpl::on_handshake_done() +{ + return srs_success; +} + +bool SrsDtlsEmptyImpl::is_dtls_client() +{ + return false; +} + SrsDtls::SrsDtls(ISrsDtlsCallback* callback) { callback_ = callback; - impl = new SrsDtlsServerImpl(callback); + impl = new SrsDtlsEmptyImpl(); } SrsDtls::~SrsDtls() diff --git a/trunk/src/app/srs_app_rtc_dtls.hpp b/trunk/src/app/srs_app_rtc_dtls.hpp index 61916a72f..56a6d8459 100644 --- a/trunk/src/app/srs_app_rtc_dtls.hpp +++ b/trunk/src/app/srs_app_rtc_dtls.hpp @@ -190,6 +190,25 @@ protected: virtual bool is_dtls_client(); }; +class SrsDtlsEmptyImpl : public SrsDtlsImpl +{ +public: + SrsDtlsEmptyImpl(); + virtual ~SrsDtlsEmptyImpl(); +public: + virtual srs_error_t initialize(std::string version, std::string role); + virtual srs_error_t start_active_handshake(); + virtual bool should_reset_timer(); + virtual srs_error_t on_dtls(char* data, int nb_data); +public: + srs_error_t get_srtp_key(std::string& recv_key, std::string& send_key); + void callback_by_ssl(std::string type, std::string desc); +protected: + virtual srs_error_t on_final_out_data(uint8_t* data, int size); + virtual srs_error_t on_handshake_done(); + virtual bool is_dtls_client(); +}; + class SrsDtls { private: From 061f367a8294349699e0f4dd51fb4a379770662c Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 6 Apr 2021 10:50:23 +0800 Subject: [PATCH 049/563] RTC: Fix RTC connection dispose bug --- trunk/src/app/srs_app_rtc_conn.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index c312af7d3..f33393172 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -2139,6 +2139,11 @@ srs_error_t SrsRtcConnection::on_connection_established() { srs_error_t err = srs_success; + // Ignore if disposing. + if (disposing_) { + return err; + } + // If DTLS done packet received many times, such as ARQ, ignore. if(ESTABLISHED == state_) { return err; From aae0f611652bac09d029c0aba4f9378d24c1fc91 Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 6 Apr 2021 10:56:40 +0800 Subject: [PATCH 050/563] RTC: Eliminate the dup code --- trunk/src/app/srs_app_rtc_conn.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index f33393172..0f2b6de9a 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -480,7 +480,7 @@ void SrsRtcPlayStream::on_stream_change(SrsRtcStreamDescription* desc) // Refresh the relation for video. // TODO: FIMXE: Match by label? - if (desc && desc->video_track_descs_.size() == 1 && desc->video_track_descs_.size() == 1) { + if (desc && desc->video_track_descs_.size() == 1) { SrsRtcTrackDescription* vdesc = desc->video_track_descs_.at(0); uint32_t ssrc = vdesc->ssrc_; SrsRtcVideoSendTrack* track = video_tracks_.begin()->second; From 6da91f7deabdd1cba846798545e144d0d35e3fff Mon Sep 17 00:00:00 2001 From: stone Date: Fri, 9 Apr 2021 07:19:00 +0800 Subject: [PATCH 051/563] For #2275, fix bug for transcode engine config param. --- AUTHORS.txt | 1 + trunk/src/app/srs_app_config.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index 0ca801957..d4ac9381d 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -79,3 +79,4 @@ CONTRIBUTORS ordered by first contribution. * ghostsf * xbpeng121<53243357+xbpeng121@users.noreply.github.com> * johzzy +* stone diff --git a/trunk/src/app/srs_app_config.cpp b/trunk/src/app/srs_app_config.cpp index a0e78dec6..c7d0e3d83 100644 --- a/trunk/src/app/srs_app_config.cpp +++ b/trunk/src/app/srs_app_config.cpp @@ -3925,7 +3925,7 @@ srs_error_t SrsConfig::check_normal_config() && e != "acodec" && e != "abitrate" && e != "asample_rate" && e != "achannels" && e != "aparams" && e != "output" && e != "perfile" && e != "iformat" && e != "oformat") { - return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal vhost.transcode.engine.%s of %s", m.c_str(), vhost->arg0().c_str()); + return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal vhost.transcode.engine.%s of %s", e.c_str(), vhost->arg0().c_str()); } } } From d01e603b25172a416279f7264c41b829b62977de Mon Sep 17 00:00:00 2001 From: winlin Date: Fri, 16 Apr 2021 09:25:55 +0800 Subject: [PATCH 052/563] Happy 2021 --- LICENSE | 2 +- trunk/src/app/srs_app_async_call.cpp | 2 +- trunk/src/app/srs_app_async_call.hpp | 2 +- trunk/src/app/srs_app_bandwidth.cpp | 2 +- trunk/src/app/srs_app_bandwidth.hpp | 2 +- trunk/src/app/srs_app_caster_flv.cpp | 2 +- trunk/src/app/srs_app_caster_flv.hpp | 2 +- trunk/src/app/srs_app_config.cpp | 2 +- trunk/src/app/srs_app_config.hpp | 2 +- trunk/src/app/srs_app_conn.cpp | 2 +- trunk/src/app/srs_app_conn.hpp | 2 +- trunk/src/app/srs_app_coworkers.cpp | 2 +- trunk/src/app/srs_app_coworkers.hpp | 2 +- trunk/src/app/srs_app_dash.cpp | 2 +- trunk/src/app/srs_app_dash.hpp | 2 +- trunk/src/app/srs_app_dvr.cpp | 2 +- trunk/src/app/srs_app_dvr.hpp | 2 +- trunk/src/app/srs_app_edge.cpp | 2 +- trunk/src/app/srs_app_edge.hpp | 2 +- trunk/src/app/srs_app_empty.cpp | 2 +- trunk/src/app/srs_app_empty.hpp | 2 +- trunk/src/app/srs_app_encoder.cpp | 2 +- trunk/src/app/srs_app_encoder.hpp | 2 +- trunk/src/app/srs_app_ffmpeg.cpp | 2 +- trunk/src/app/srs_app_ffmpeg.hpp | 2 +- trunk/src/app/srs_app_forward.cpp | 2 +- trunk/src/app/srs_app_forward.hpp | 2 +- trunk/src/app/srs_app_fragment.cpp | 2 +- trunk/src/app/srs_app_fragment.hpp | 2 +- trunk/src/app/srs_app_hds.cpp | 2 +- trunk/src/app/srs_app_hds.hpp | 2 +- trunk/src/app/srs_app_heartbeat.cpp | 2 +- trunk/src/app/srs_app_heartbeat.hpp | 2 +- trunk/src/app/srs_app_hls.cpp | 2 +- trunk/src/app/srs_app_hls.hpp | 2 +- trunk/src/app/srs_app_hourglass.cpp | 2 +- trunk/src/app/srs_app_hourglass.hpp | 2 +- trunk/src/app/srs_app_http_api.cpp | 2 +- trunk/src/app/srs_app_http_api.hpp | 2 +- trunk/src/app/srs_app_http_client.cpp | 2 +- trunk/src/app/srs_app_http_client.hpp | 2 +- trunk/src/app/srs_app_http_conn.cpp | 2 +- trunk/src/app/srs_app_http_conn.hpp | 2 +- trunk/src/app/srs_app_http_hooks.cpp | 2 +- trunk/src/app/srs_app_http_hooks.hpp | 2 +- trunk/src/app/srs_app_http_static.cpp | 2 +- trunk/src/app/srs_app_http_static.hpp | 2 +- trunk/src/app/srs_app_http_stream.cpp | 2 +- trunk/src/app/srs_app_http_stream.hpp | 2 +- trunk/src/app/srs_app_hybrid.cpp | 2 +- trunk/src/app/srs_app_hybrid.hpp | 2 +- trunk/src/app/srs_app_ingest.cpp | 2 +- trunk/src/app/srs_app_ingest.hpp | 2 +- trunk/src/app/srs_app_listener.cpp | 2 +- trunk/src/app/srs_app_listener.hpp | 2 +- trunk/src/app/srs_app_log.cpp | 2 +- trunk/src/app/srs_app_log.hpp | 2 +- trunk/src/app/srs_app_mpegts_udp.cpp | 2 +- trunk/src/app/srs_app_mpegts_udp.hpp | 2 +- trunk/src/app/srs_app_ng_exec.cpp | 2 +- trunk/src/app/srs_app_ng_exec.hpp | 2 +- trunk/src/app/srs_app_pithy_print.cpp | 2 +- trunk/src/app/srs_app_pithy_print.hpp | 2 +- trunk/src/app/srs_app_process.cpp | 2 +- trunk/src/app/srs_app_process.hpp | 2 +- trunk/src/app/srs_app_recv_thread.cpp | 2 +- trunk/src/app/srs_app_recv_thread.hpp | 2 +- trunk/src/app/srs_app_refer.cpp | 2 +- trunk/src/app/srs_app_refer.hpp | 2 +- trunk/src/app/srs_app_reload.cpp | 2 +- trunk/src/app/srs_app_reload.hpp | 2 +- trunk/src/app/srs_app_rtc_api.cpp | 2 +- trunk/src/app/srs_app_rtc_api.hpp | 2 +- trunk/src/app/srs_app_rtc_dtls.cpp | 2 +- trunk/src/app/srs_app_rtc_dtls.hpp | 2 +- trunk/src/app/srs_app_rtmp_conn.cpp | 2 +- trunk/src/app/srs_app_rtmp_conn.hpp | 2 +- trunk/src/app/srs_app_rtsp.cpp | 2 +- trunk/src/app/srs_app_rtsp.hpp | 2 +- trunk/src/app/srs_app_security.cpp | 2 +- trunk/src/app/srs_app_security.hpp | 2 +- trunk/src/app/srs_app_server.cpp | 2 +- trunk/src/app/srs_app_server.hpp | 2 +- trunk/src/app/srs_app_source.cpp | 2 +- trunk/src/app/srs_app_source.hpp | 2 +- trunk/src/app/srs_app_st.cpp | 2 +- trunk/src/app/srs_app_st.hpp | 2 +- trunk/src/app/srs_app_statistic.cpp | 2 +- trunk/src/app/srs_app_statistic.hpp | 2 +- trunk/src/app/srs_app_utility.cpp | 2 +- trunk/src/app/srs_app_utility.hpp | 2 +- trunk/src/core/srs_core.cpp | 2 +- trunk/src/core/srs_core.hpp | 2 +- trunk/src/core/srs_core_autofree.cpp | 2 +- trunk/src/core/srs_core_autofree.hpp | 2 +- trunk/src/core/srs_core_performance.cpp | 2 +- trunk/src/core/srs_core_performance.hpp | 2 +- trunk/src/core/srs_core_time.cpp | 2 +- trunk/src/core/srs_core_time.hpp | 2 +- trunk/src/core/srs_core_version3.cpp | 2 +- trunk/src/core/srs_core_version3.hpp | 2 +- trunk/src/core/srs_core_version4.cpp | 2 +- trunk/src/core/srs_core_version4.hpp | 2 +- trunk/src/kernel/srs_kernel_aac.cpp | 2 +- trunk/src/kernel/srs_kernel_aac.hpp | 2 +- trunk/src/kernel/srs_kernel_balance.cpp | 2 +- trunk/src/kernel/srs_kernel_balance.hpp | 2 +- trunk/src/kernel/srs_kernel_buffer.cpp | 2 +- trunk/src/kernel/srs_kernel_buffer.hpp | 2 +- trunk/src/kernel/srs_kernel_codec.cpp | 2 +- trunk/src/kernel/srs_kernel_codec.hpp | 2 +- trunk/src/kernel/srs_kernel_consts.cpp | 2 +- trunk/src/kernel/srs_kernel_consts.hpp | 2 +- trunk/src/kernel/srs_kernel_error.cpp | 2 +- trunk/src/kernel/srs_kernel_error.hpp | 2 +- trunk/src/kernel/srs_kernel_file.cpp | 2 +- trunk/src/kernel/srs_kernel_file.hpp | 2 +- trunk/src/kernel/srs_kernel_flv.cpp | 2 +- trunk/src/kernel/srs_kernel_flv.hpp | 2 +- trunk/src/kernel/srs_kernel_io.cpp | 2 +- trunk/src/kernel/srs_kernel_io.hpp | 2 +- trunk/src/kernel/srs_kernel_kbps.cpp | 2 +- trunk/src/kernel/srs_kernel_kbps.hpp | 2 +- trunk/src/kernel/srs_kernel_log.cpp | 2 +- trunk/src/kernel/srs_kernel_log.hpp | 2 +- trunk/src/kernel/srs_kernel_mp3.cpp | 2 +- trunk/src/kernel/srs_kernel_mp3.hpp | 2 +- trunk/src/kernel/srs_kernel_mp4.cpp | 2 +- trunk/src/kernel/srs_kernel_mp4.hpp | 2 +- trunk/src/kernel/srs_kernel_rtc_rtp.cpp | 2 +- trunk/src/kernel/srs_kernel_rtc_rtp.hpp | 2 +- trunk/src/kernel/srs_kernel_stream.cpp | 2 +- trunk/src/kernel/srs_kernel_stream.hpp | 2 +- trunk/src/kernel/srs_kernel_ts.cpp | 2 +- trunk/src/kernel/srs_kernel_ts.hpp | 2 +- trunk/src/kernel/srs_kernel_utility.cpp | 2 +- trunk/src/kernel/srs_kernel_utility.hpp | 2 +- trunk/src/main/srs_main_ingest_hls.cpp | 2 +- trunk/src/main/srs_main_mp4_parser.cpp | 2 +- trunk/src/main/srs_main_server.cpp | 2 +- trunk/src/protocol/srs_http_stack.cpp | 2 +- trunk/src/protocol/srs_http_stack.hpp | 2 +- trunk/src/protocol/srs_protocol_amf0.cpp | 2 +- trunk/src/protocol/srs_protocol_amf0.hpp | 2 +- trunk/src/protocol/srs_protocol_format.cpp | 2 +- trunk/src/protocol/srs_protocol_format.hpp | 2 +- trunk/src/protocol/srs_protocol_io.cpp | 2 +- trunk/src/protocol/srs_protocol_io.hpp | 2 +- trunk/src/protocol/srs_protocol_json.cpp | 4 ++-- trunk/src/protocol/srs_protocol_json.hpp | 2 +- trunk/src/protocol/srs_protocol_kbps.cpp | 2 +- trunk/src/protocol/srs_protocol_kbps.hpp | 2 +- trunk/src/protocol/srs_protocol_stream.cpp | 2 +- trunk/src/protocol/srs_protocol_stream.hpp | 2 +- trunk/src/protocol/srs_protocol_utility.cpp | 2 +- trunk/src/protocol/srs_protocol_utility.hpp | 2 +- trunk/src/protocol/srs_raw_avc.cpp | 2 +- trunk/src/protocol/srs_raw_avc.hpp | 2 +- trunk/src/protocol/srs_rtmp_handshake.cpp | 2 +- trunk/src/protocol/srs_rtmp_handshake.hpp | 2 +- trunk/src/protocol/srs_rtmp_msg_array.cpp | 2 +- trunk/src/protocol/srs_rtmp_msg_array.hpp | 2 +- trunk/src/protocol/srs_rtmp_stack.cpp | 2 +- trunk/src/protocol/srs_rtmp_stack.hpp | 2 +- trunk/src/protocol/srs_rtsp_stack.cpp | 2 +- trunk/src/protocol/srs_rtsp_stack.hpp | 2 +- trunk/src/protocol/srs_service_conn.cpp | 2 +- trunk/src/protocol/srs_service_conn.hpp | 2 +- trunk/src/protocol/srs_service_http_client.cpp | 2 +- trunk/src/protocol/srs_service_http_client.hpp | 2 +- trunk/src/protocol/srs_service_http_conn.cpp | 2 +- trunk/src/protocol/srs_service_http_conn.hpp | 2 +- trunk/src/protocol/srs_service_log.cpp | 2 +- trunk/src/protocol/srs_service_log.hpp | 2 +- trunk/src/protocol/srs_service_rtmp_conn.cpp | 2 +- trunk/src/protocol/srs_service_rtmp_conn.hpp | 2 +- trunk/src/protocol/srs_service_st.cpp | 2 +- trunk/src/protocol/srs_service_st.hpp | 2 +- trunk/src/protocol/srs_service_utility.cpp | 4 ++-- trunk/src/protocol/srs_service_utility.hpp | 2 +- trunk/src/srt/srt_conn.cpp | 2 +- trunk/src/srt/srt_conn.hpp | 2 +- trunk/src/srt/srt_data.hpp | 2 +- trunk/src/srt/srt_handle.cpp | 2 +- trunk/src/srt/srt_handle.hpp | 2 +- trunk/src/srt/srt_server.hpp | 2 +- trunk/src/srt/srt_to_rtmp.cpp | 2 +- trunk/src/srt/srt_to_rtmp.hpp | 2 +- trunk/src/srt/stringex.hpp | 2 +- trunk/src/srt/ts_demux.cpp | 2 +- trunk/src/srt/ts_demux.hpp | 2 +- trunk/src/srt/ts_demux_test.cpp | 2 +- trunk/src/utest/srs_utest.cpp | 2 +- trunk/src/utest/srs_utest.hpp | 2 +- trunk/src/utest/srs_utest_amf0.cpp | 2 +- trunk/src/utest/srs_utest_amf0.hpp | 2 +- trunk/src/utest/srs_utest_app.cpp | 2 +- trunk/src/utest/srs_utest_app.hpp | 2 +- trunk/src/utest/srs_utest_avc.cpp | 2 +- trunk/src/utest/srs_utest_avc.hpp | 2 +- trunk/src/utest/srs_utest_config.cpp | 2 +- trunk/src/utest/srs_utest_config.hpp | 2 +- trunk/src/utest/srs_utest_core.cpp | 2 +- trunk/src/utest/srs_utest_core.hpp | 2 +- trunk/src/utest/srs_utest_http.cpp | 2 +- trunk/src/utest/srs_utest_http.hpp | 2 +- trunk/src/utest/srs_utest_kernel.cpp | 2 +- trunk/src/utest/srs_utest_kernel.hpp | 2 +- trunk/src/utest/srs_utest_mp4.cpp | 2 +- trunk/src/utest/srs_utest_mp4.hpp | 2 +- trunk/src/utest/srs_utest_protocol.cpp | 2 +- trunk/src/utest/srs_utest_protocol.hpp | 2 +- trunk/src/utest/srs_utest_reload.cpp | 2 +- trunk/src/utest/srs_utest_reload.hpp | 2 +- trunk/src/utest/srs_utest_rtc.cpp | 2 +- trunk/src/utest/srs_utest_rtc.hpp | 2 +- trunk/src/utest/srs_utest_rtmp.cpp | 2 +- trunk/src/utest/srs_utest_rtmp.hpp | 2 +- trunk/src/utest/srs_utest_service.cpp | 2 +- trunk/src/utest/srs_utest_service.hpp | 2 +- 220 files changed, 222 insertions(+), 222 deletions(-) diff --git a/LICENSE b/LICENSE index ed59f8cfc..abd7f983c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_async_call.cpp b/trunk/src/app/srs_app_async_call.cpp index 271964d99..e4cead1ee 100644 --- a/trunk/src/app/srs_app_async_call.cpp +++ b/trunk/src/app/srs_app_async_call.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_async_call.hpp b/trunk/src/app/srs_app_async_call.hpp index b05fda21a..fb2875a7a 100644 --- a/trunk/src/app/srs_app_async_call.hpp +++ b/trunk/src/app/srs_app_async_call.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_bandwidth.cpp b/trunk/src/app/srs_app_bandwidth.cpp index 5fd77c7db..38a85faae 100644 --- a/trunk/src/app/srs_app_bandwidth.cpp +++ b/trunk/src/app/srs_app_bandwidth.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_bandwidth.hpp b/trunk/src/app/srs_app_bandwidth.hpp index 641d416eb..76d035237 100644 --- a/trunk/src/app/srs_app_bandwidth.hpp +++ b/trunk/src/app/srs_app_bandwidth.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_caster_flv.cpp b/trunk/src/app/srs_app_caster_flv.cpp index 7d86fe434..1cb912241 100644 --- a/trunk/src/app/srs_app_caster_flv.cpp +++ b/trunk/src/app/srs_app_caster_flv.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_caster_flv.hpp b/trunk/src/app/srs_app_caster_flv.hpp index 1a964ca58..5042ebf6e 100644 --- a/trunk/src/app/srs_app_caster_flv.hpp +++ b/trunk/src/app/srs_app_caster_flv.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_config.cpp b/trunk/src/app/srs_app_config.cpp index c7d0e3d83..e69f0772e 100644 --- a/trunk/src/app/srs_app_config.cpp +++ b/trunk/src/app/srs_app_config.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_config.hpp b/trunk/src/app/srs_app_config.hpp index 31e174dc2..0e31bcb6d 100644 --- a/trunk/src/app/srs_app_config.hpp +++ b/trunk/src/app/srs_app_config.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_conn.cpp b/trunk/src/app/srs_app_conn.cpp index 9851313ea..42534c023 100644 --- a/trunk/src/app/srs_app_conn.cpp +++ b/trunk/src/app/srs_app_conn.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_conn.hpp b/trunk/src/app/srs_app_conn.hpp index ac95ca52d..c992c3fc2 100644 --- a/trunk/src/app/srs_app_conn.hpp +++ b/trunk/src/app/srs_app_conn.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_coworkers.cpp b/trunk/src/app/srs_app_coworkers.cpp index 16b2dae8d..71431d1ac 100644 --- a/trunk/src/app/srs_app_coworkers.cpp +++ b/trunk/src/app/srs_app_coworkers.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_coworkers.hpp b/trunk/src/app/srs_app_coworkers.hpp index 6b640bf45..43fd69656 100644 --- a/trunk/src/app/srs_app_coworkers.hpp +++ b/trunk/src/app/srs_app_coworkers.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_dash.cpp b/trunk/src/app/srs_app_dash.cpp index bf67014be..1fd0ff087 100644 --- a/trunk/src/app/srs_app_dash.cpp +++ b/trunk/src/app/srs_app_dash.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_dash.hpp b/trunk/src/app/srs_app_dash.hpp index 775ae9d87..460b28928 100644 --- a/trunk/src/app/srs_app_dash.hpp +++ b/trunk/src/app/srs_app_dash.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_dvr.cpp b/trunk/src/app/srs_app_dvr.cpp index 2dc7b57dc..49d63f921 100644 --- a/trunk/src/app/srs_app_dvr.cpp +++ b/trunk/src/app/srs_app_dvr.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_dvr.hpp b/trunk/src/app/srs_app_dvr.hpp index 03b04e746..579d5db30 100644 --- a/trunk/src/app/srs_app_dvr.hpp +++ b/trunk/src/app/srs_app_dvr.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_edge.cpp b/trunk/src/app/srs_app_edge.cpp index 13885d1ac..4de21e115 100644 --- a/trunk/src/app/srs_app_edge.cpp +++ b/trunk/src/app/srs_app_edge.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_edge.hpp b/trunk/src/app/srs_app_edge.hpp index 4a673c926..39eda1777 100644 --- a/trunk/src/app/srs_app_edge.hpp +++ b/trunk/src/app/srs_app_edge.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_empty.cpp b/trunk/src/app/srs_app_empty.cpp index a89c4e966..1c00704ee 100644 --- a/trunk/src/app/srs_app_empty.cpp +++ b/trunk/src/app/srs_app_empty.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_empty.hpp b/trunk/src/app/srs_app_empty.hpp index 33bf116cd..e5e1db2e7 100644 --- a/trunk/src/app/srs_app_empty.hpp +++ b/trunk/src/app/srs_app_empty.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_encoder.cpp b/trunk/src/app/srs_app_encoder.cpp index 1f4aae4ee..dac0b417d 100644 --- a/trunk/src/app/srs_app_encoder.cpp +++ b/trunk/src/app/srs_app_encoder.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_encoder.hpp b/trunk/src/app/srs_app_encoder.hpp index 2beb64612..dc090c73f 100644 --- a/trunk/src/app/srs_app_encoder.hpp +++ b/trunk/src/app/srs_app_encoder.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_ffmpeg.cpp b/trunk/src/app/srs_app_ffmpeg.cpp index 3acb1708d..9ebe70db9 100644 --- a/trunk/src/app/srs_app_ffmpeg.cpp +++ b/trunk/src/app/srs_app_ffmpeg.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_ffmpeg.hpp b/trunk/src/app/srs_app_ffmpeg.hpp index 9e068acbb..b52172f63 100644 --- a/trunk/src/app/srs_app_ffmpeg.hpp +++ b/trunk/src/app/srs_app_ffmpeg.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_forward.cpp b/trunk/src/app/srs_app_forward.cpp index 957ea7ccb..25090ebfa 100755 --- a/trunk/src/app/srs_app_forward.cpp +++ b/trunk/src/app/srs_app_forward.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_forward.hpp b/trunk/src/app/srs_app_forward.hpp index a4adcfc15..4f69edc40 100644 --- a/trunk/src/app/srs_app_forward.hpp +++ b/trunk/src/app/srs_app_forward.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_fragment.cpp b/trunk/src/app/srs_app_fragment.cpp index a6075b22d..d2c9e7f8e 100644 --- a/trunk/src/app/srs_app_fragment.cpp +++ b/trunk/src/app/srs_app_fragment.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_fragment.hpp b/trunk/src/app/srs_app_fragment.hpp index 22d938c9e..95d23fec4 100644 --- a/trunk/src/app/srs_app_fragment.hpp +++ b/trunk/src/app/srs_app_fragment.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_hds.cpp b/trunk/src/app/srs_app_hds.cpp index 75f028933..7e6cbb4c9 100644 --- a/trunk/src/app/srs_app_hds.cpp +++ b/trunk/src/app/srs_app_hds.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_hds.hpp b/trunk/src/app/srs_app_hds.hpp index 58236cea4..513b7e9a6 100644 --- a/trunk/src/app/srs_app_hds.hpp +++ b/trunk/src/app/srs_app_hds.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_heartbeat.cpp b/trunk/src/app/srs_app_heartbeat.cpp index f193d71b0..14c214f90 100644 --- a/trunk/src/app/srs_app_heartbeat.cpp +++ b/trunk/src/app/srs_app_heartbeat.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_heartbeat.hpp b/trunk/src/app/srs_app_heartbeat.hpp index 30f6ec332..d9c6de4ae 100644 --- a/trunk/src/app/srs_app_heartbeat.hpp +++ b/trunk/src/app/srs_app_heartbeat.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_hls.cpp b/trunk/src/app/srs_app_hls.cpp index 81c7d7a79..8a6b25871 100644 --- a/trunk/src/app/srs_app_hls.cpp +++ b/trunk/src/app/srs_app_hls.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_hls.hpp b/trunk/src/app/srs_app_hls.hpp index 2d4481708..3d00672a5 100644 --- a/trunk/src/app/srs_app_hls.hpp +++ b/trunk/src/app/srs_app_hls.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_hourglass.cpp b/trunk/src/app/srs_app_hourglass.cpp index b5c84ed9a..58d7dee72 100644 --- a/trunk/src/app/srs_app_hourglass.cpp +++ b/trunk/src/app/srs_app_hourglass.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_hourglass.hpp b/trunk/src/app/srs_app_hourglass.hpp index a9f781268..5404f89aa 100644 --- a/trunk/src/app/srs_app_hourglass.hpp +++ b/trunk/src/app/srs_app_hourglass.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_api.cpp b/trunk/src/app/srs_app_http_api.cpp index 449146aac..1a720d0f6 100644 --- a/trunk/src/app/srs_app_http_api.cpp +++ b/trunk/src/app/srs_app_http_api.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_api.hpp b/trunk/src/app/srs_app_http_api.hpp index dbdeafbed..a7479e2c6 100644 --- a/trunk/src/app/srs_app_http_api.hpp +++ b/trunk/src/app/srs_app_http_api.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_client.cpp b/trunk/src/app/srs_app_http_client.cpp index c661a5a81..bb9a01063 100644 --- a/trunk/src/app/srs_app_http_client.cpp +++ b/trunk/src/app/srs_app_http_client.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_client.hpp b/trunk/src/app/srs_app_http_client.hpp index 5407987ab..f4532ea2e 100644 --- a/trunk/src/app/srs_app_http_client.hpp +++ b/trunk/src/app/srs_app_http_client.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_conn.cpp b/trunk/src/app/srs_app_http_conn.cpp index 05d8cada3..729ce2171 100644 --- a/trunk/src/app/srs_app_http_conn.cpp +++ b/trunk/src/app/srs_app_http_conn.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_conn.hpp b/trunk/src/app/srs_app_http_conn.hpp index 8b97e2dec..b7c2b7fb3 100644 --- a/trunk/src/app/srs_app_http_conn.hpp +++ b/trunk/src/app/srs_app_http_conn.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_hooks.cpp b/trunk/src/app/srs_app_http_hooks.cpp index d899570e2..da84f45f7 100644 --- a/trunk/src/app/srs_app_http_hooks.cpp +++ b/trunk/src/app/srs_app_http_hooks.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_hooks.hpp b/trunk/src/app/srs_app_http_hooks.hpp index f9588f647..a6324bf90 100644 --- a/trunk/src/app/srs_app_http_hooks.hpp +++ b/trunk/src/app/srs_app_http_hooks.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_static.cpp b/trunk/src/app/srs_app_http_static.cpp index a20fea72f..7b17a1366 100644 --- a/trunk/src/app/srs_app_http_static.cpp +++ b/trunk/src/app/srs_app_http_static.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_static.hpp b/trunk/src/app/srs_app_http_static.hpp index acc0512af..6852c6588 100644 --- a/trunk/src/app/srs_app_http_static.hpp +++ b/trunk/src/app/srs_app_http_static.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_stream.cpp b/trunk/src/app/srs_app_http_stream.cpp index 943fdf3d3..5b32215ec 100755 --- a/trunk/src/app/srs_app_http_stream.cpp +++ b/trunk/src/app/srs_app_http_stream.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_http_stream.hpp b/trunk/src/app/srs_app_http_stream.hpp index a82e2a5a0..0e56a63c1 100755 --- a/trunk/src/app/srs_app_http_stream.hpp +++ b/trunk/src/app/srs_app_http_stream.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_hybrid.cpp b/trunk/src/app/srs_app_hybrid.cpp index f9365f60a..bedd5b816 100644 --- a/trunk/src/app/srs_app_hybrid.cpp +++ b/trunk/src/app/srs_app_hybrid.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_hybrid.hpp b/trunk/src/app/srs_app_hybrid.hpp index 7e988fe26..b13c381e6 100644 --- a/trunk/src/app/srs_app_hybrid.hpp +++ b/trunk/src/app/srs_app_hybrid.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_ingest.cpp b/trunk/src/app/srs_app_ingest.cpp index a8c7130e2..67ef1f03e 100644 --- a/trunk/src/app/srs_app_ingest.cpp +++ b/trunk/src/app/srs_app_ingest.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_ingest.hpp b/trunk/src/app/srs_app_ingest.hpp index d392a24e3..d6aa82045 100644 --- a/trunk/src/app/srs_app_ingest.hpp +++ b/trunk/src/app/srs_app_ingest.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_listener.cpp b/trunk/src/app/srs_app_listener.cpp index ec131230b..c38609c9f 100755 --- a/trunk/src/app/srs_app_listener.cpp +++ b/trunk/src/app/srs_app_listener.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_listener.hpp b/trunk/src/app/srs_app_listener.hpp index a42fbefd5..e3ba3e975 100644 --- a/trunk/src/app/srs_app_listener.hpp +++ b/trunk/src/app/srs_app_listener.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_log.cpp b/trunk/src/app/srs_app_log.cpp index 9ef9f5571..28ff755ef 100644 --- a/trunk/src/app/srs_app_log.cpp +++ b/trunk/src/app/srs_app_log.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_log.hpp b/trunk/src/app/srs_app_log.hpp index e6b82c4eb..85907e29a 100644 --- a/trunk/src/app/srs_app_log.hpp +++ b/trunk/src/app/srs_app_log.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_mpegts_udp.cpp b/trunk/src/app/srs_app_mpegts_udp.cpp index 8b81f14aa..c8b6ca641 100644 --- a/trunk/src/app/srs_app_mpegts_udp.cpp +++ b/trunk/src/app/srs_app_mpegts_udp.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_mpegts_udp.hpp b/trunk/src/app/srs_app_mpegts_udp.hpp index e0b2ad517..c6aeaa27a 100644 --- a/trunk/src/app/srs_app_mpegts_udp.hpp +++ b/trunk/src/app/srs_app_mpegts_udp.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_ng_exec.cpp b/trunk/src/app/srs_app_ng_exec.cpp index 30ebc5ecb..6bc1506b5 100644 --- a/trunk/src/app/srs_app_ng_exec.cpp +++ b/trunk/src/app/srs_app_ng_exec.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_ng_exec.hpp b/trunk/src/app/srs_app_ng_exec.hpp index c00fcdece..95109dfeb 100644 --- a/trunk/src/app/srs_app_ng_exec.hpp +++ b/trunk/src/app/srs_app_ng_exec.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_pithy_print.cpp b/trunk/src/app/srs_app_pithy_print.cpp index ebf70def8..40c1a9b4b 100644 --- a/trunk/src/app/srs_app_pithy_print.cpp +++ b/trunk/src/app/srs_app_pithy_print.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_pithy_print.hpp b/trunk/src/app/srs_app_pithy_print.hpp index bc966db98..d1b044553 100644 --- a/trunk/src/app/srs_app_pithy_print.hpp +++ b/trunk/src/app/srs_app_pithy_print.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_process.cpp b/trunk/src/app/srs_app_process.cpp index 0ef31197a..71d7db05b 100644 --- a/trunk/src/app/srs_app_process.cpp +++ b/trunk/src/app/srs_app_process.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_process.hpp b/trunk/src/app/srs_app_process.hpp index 13990e430..beeef691e 100644 --- a/trunk/src/app/srs_app_process.hpp +++ b/trunk/src/app/srs_app_process.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_recv_thread.cpp b/trunk/src/app/srs_app_recv_thread.cpp index 5887b3710..55046ea90 100644 --- a/trunk/src/app/srs_app_recv_thread.cpp +++ b/trunk/src/app/srs_app_recv_thread.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_recv_thread.hpp b/trunk/src/app/srs_app_recv_thread.hpp index 13e55fe79..d6fb7f8db 100644 --- a/trunk/src/app/srs_app_recv_thread.hpp +++ b/trunk/src/app/srs_app_recv_thread.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_refer.cpp b/trunk/src/app/srs_app_refer.cpp index ff4e4be28..f66adebf8 100644 --- a/trunk/src/app/srs_app_refer.cpp +++ b/trunk/src/app/srs_app_refer.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_refer.hpp b/trunk/src/app/srs_app_refer.hpp index 0e0967971..6a89a3bda 100644 --- a/trunk/src/app/srs_app_refer.hpp +++ b/trunk/src/app/srs_app_refer.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_reload.cpp b/trunk/src/app/srs_app_reload.cpp index 5203617fb..28c4dfc85 100644 --- a/trunk/src/app/srs_app_reload.cpp +++ b/trunk/src/app/srs_app_reload.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_reload.hpp b/trunk/src/app/srs_app_reload.hpp index bf277f9dd..9a1668d8e 100644 --- a/trunk/src/app/srs_app_reload.hpp +++ b/trunk/src/app/srs_app_reload.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_api.cpp b/trunk/src/app/srs_app_rtc_api.cpp index b35340b0c..fbe278b88 100644 --- a/trunk/src/app/srs_app_rtc_api.cpp +++ b/trunk/src/app/srs_app_rtc_api.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_api.hpp b/trunk/src/app/srs_app_rtc_api.hpp index 56fab8322..c7bc6596b 100644 --- a/trunk/src/app/srs_app_rtc_api.hpp +++ b/trunk/src/app/srs_app_rtc_api.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_dtls.cpp b/trunk/src/app/srs_app_rtc_dtls.cpp index dbb17a685..74a511183 100644 --- a/trunk/src/app/srs_app_rtc_dtls.cpp +++ b/trunk/src/app/srs_app_rtc_dtls.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_dtls.hpp b/trunk/src/app/srs_app_rtc_dtls.hpp index 56a6d8459..0938f30c6 100644 --- a/trunk/src/app/srs_app_rtc_dtls.hpp +++ b/trunk/src/app/srs_app_rtc_dtls.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtmp_conn.cpp b/trunk/src/app/srs_app_rtmp_conn.cpp index 2acec6814..e82ca3fbb 100644 --- a/trunk/src/app/srs_app_rtmp_conn.cpp +++ b/trunk/src/app/srs_app_rtmp_conn.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtmp_conn.hpp b/trunk/src/app/srs_app_rtmp_conn.hpp index 1151378b7..495e2623c 100644 --- a/trunk/src/app/srs_app_rtmp_conn.hpp +++ b/trunk/src/app/srs_app_rtmp_conn.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtsp.cpp b/trunk/src/app/srs_app_rtsp.cpp index 1a936808c..829bd1657 100644 --- a/trunk/src/app/srs_app_rtsp.cpp +++ b/trunk/src/app/srs_app_rtsp.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtsp.hpp b/trunk/src/app/srs_app_rtsp.hpp index ca79a359e..700975184 100644 --- a/trunk/src/app/srs_app_rtsp.hpp +++ b/trunk/src/app/srs_app_rtsp.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_security.cpp b/trunk/src/app/srs_app_security.cpp index 812766f08..085c98c28 100644 --- a/trunk/src/app/srs_app_security.cpp +++ b/trunk/src/app/srs_app_security.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_security.hpp b/trunk/src/app/srs_app_security.hpp index 163b7d3a6..614c87c33 100644 --- a/trunk/src/app/srs_app_security.hpp +++ b/trunk/src/app/srs_app_security.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_server.cpp b/trunk/src/app/srs_app_server.cpp index cf3f553d6..49288d3c9 100644 --- a/trunk/src/app/srs_app_server.cpp +++ b/trunk/src/app/srs_app_server.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_server.hpp b/trunk/src/app/srs_app_server.hpp index e7f562ee4..0b007cea8 100644 --- a/trunk/src/app/srs_app_server.hpp +++ b/trunk/src/app/srs_app_server.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_source.cpp b/trunk/src/app/srs_app_source.cpp index 103d8e20d..1a9553d41 100755 --- a/trunk/src/app/srs_app_source.cpp +++ b/trunk/src/app/srs_app_source.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_source.hpp b/trunk/src/app/srs_app_source.hpp index 9f7740437..8a35b5155 100644 --- a/trunk/src/app/srs_app_source.hpp +++ b/trunk/src/app/srs_app_source.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_st.cpp b/trunk/src/app/srs_app_st.cpp index 911680df7..7cae9e962 100755 --- a/trunk/src/app/srs_app_st.cpp +++ b/trunk/src/app/srs_app_st.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_st.hpp b/trunk/src/app/srs_app_st.hpp index 89d118ce9..e1c45cc88 100644 --- a/trunk/src/app/srs_app_st.hpp +++ b/trunk/src/app/srs_app_st.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_statistic.cpp b/trunk/src/app/srs_app_statistic.cpp index 69d4a5cb5..e637e7199 100644 --- a/trunk/src/app/srs_app_statistic.cpp +++ b/trunk/src/app/srs_app_statistic.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_statistic.hpp b/trunk/src/app/srs_app_statistic.hpp index c62eec78d..7ec90a2ac 100644 --- a/trunk/src/app/srs_app_statistic.hpp +++ b/trunk/src/app/srs_app_statistic.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_utility.cpp b/trunk/src/app/srs_app_utility.cpp index a7251cf22..a221cbbbb 100644 --- a/trunk/src/app/srs_app_utility.cpp +++ b/trunk/src/app/srs_app_utility.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_utility.hpp b/trunk/src/app/srs_app_utility.hpp index 117f7e67d..1c343746f 100644 --- a/trunk/src/app/srs_app_utility.hpp +++ b/trunk/src/app/srs_app_utility.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core.cpp b/trunk/src/core/srs_core.cpp index 29b882ebe..846984ba4 100644 --- a/trunk/src/core/srs_core.cpp +++ b/trunk/src/core/srs_core.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core.hpp b/trunk/src/core/srs_core.hpp index 6eb1e2f2b..c649f76bb 100644 --- a/trunk/src/core/srs_core.hpp +++ b/trunk/src/core/srs_core.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_autofree.cpp b/trunk/src/core/srs_core_autofree.cpp index 8e2583294..9be4928d2 100644 --- a/trunk/src/core/srs_core_autofree.cpp +++ b/trunk/src/core/srs_core_autofree.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_autofree.hpp b/trunk/src/core/srs_core_autofree.hpp index ac1df6f7d..5f7f65afc 100644 --- a/trunk/src/core/srs_core_autofree.hpp +++ b/trunk/src/core/srs_core_autofree.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_performance.cpp b/trunk/src/core/srs_core_performance.cpp index 80b22b71e..7ace5388f 100644 --- a/trunk/src/core/srs_core_performance.cpp +++ b/trunk/src/core/srs_core_performance.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_performance.hpp b/trunk/src/core/srs_core_performance.hpp index be1554851..47eed5d57 100644 --- a/trunk/src/core/srs_core_performance.hpp +++ b/trunk/src/core/srs_core_performance.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_time.cpp b/trunk/src/core/srs_core_time.cpp index b977a3c4a..69f091992 100644 --- a/trunk/src/core/srs_core_time.cpp +++ b/trunk/src/core/srs_core_time.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_time.hpp b/trunk/src/core/srs_core_time.hpp index df33ef623..827c457e7 100644 --- a/trunk/src/core/srs_core_time.hpp +++ b/trunk/src/core/srs_core_time.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_version3.cpp b/trunk/src/core/srs_core_version3.cpp index 97724f2b1..58634f522 100644 --- a/trunk/src/core/srs_core_version3.cpp +++ b/trunk/src/core/srs_core_version3.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_version3.hpp b/trunk/src/core/srs_core_version3.hpp index b4c8f7273..31113e861 100644 --- a/trunk/src/core/srs_core_version3.hpp +++ b/trunk/src/core/srs_core_version3.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_version4.cpp b/trunk/src/core/srs_core_version4.cpp index e5071d6ef..a8ca9a3a4 100644 --- a/trunk/src/core/srs_core_version4.cpp +++ b/trunk/src/core/srs_core_version4.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/core/srs_core_version4.hpp b/trunk/src/core/srs_core_version4.hpp index 54ba852bf..c9ecf224b 100644 --- a/trunk/src/core/srs_core_version4.hpp +++ b/trunk/src/core/srs_core_version4.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_aac.cpp b/trunk/src/kernel/srs_kernel_aac.cpp index 5a4d42499..89d28a2b1 100644 --- a/trunk/src/kernel/srs_kernel_aac.cpp +++ b/trunk/src/kernel/srs_kernel_aac.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_aac.hpp b/trunk/src/kernel/srs_kernel_aac.hpp index 770d51408..5136b6210 100644 --- a/trunk/src/kernel/srs_kernel_aac.hpp +++ b/trunk/src/kernel/srs_kernel_aac.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_balance.cpp b/trunk/src/kernel/srs_kernel_balance.cpp index 6446abd67..10aa9d457 100644 --- a/trunk/src/kernel/srs_kernel_balance.cpp +++ b/trunk/src/kernel/srs_kernel_balance.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_balance.hpp b/trunk/src/kernel/srs_kernel_balance.hpp index a5066ca8b..779f308d6 100644 --- a/trunk/src/kernel/srs_kernel_balance.hpp +++ b/trunk/src/kernel/srs_kernel_balance.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_buffer.cpp b/trunk/src/kernel/srs_kernel_buffer.cpp index 6e7e45754..51df17fa9 100644 --- a/trunk/src/kernel/srs_kernel_buffer.cpp +++ b/trunk/src/kernel/srs_kernel_buffer.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_buffer.hpp b/trunk/src/kernel/srs_kernel_buffer.hpp index c406d0906..ad3439e00 100644 --- a/trunk/src/kernel/srs_kernel_buffer.hpp +++ b/trunk/src/kernel/srs_kernel_buffer.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_codec.cpp b/trunk/src/kernel/srs_kernel_codec.cpp index a937662f5..acec1003e 100644 --- a/trunk/src/kernel/srs_kernel_codec.cpp +++ b/trunk/src/kernel/srs_kernel_codec.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_codec.hpp b/trunk/src/kernel/srs_kernel_codec.hpp index cc2bcb711..56b0bf1d5 100644 --- a/trunk/src/kernel/srs_kernel_codec.hpp +++ b/trunk/src/kernel/srs_kernel_codec.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_consts.cpp b/trunk/src/kernel/srs_kernel_consts.cpp index 9c2be32af..d5a17b68c 100644 --- a/trunk/src/kernel/srs_kernel_consts.cpp +++ b/trunk/src/kernel/srs_kernel_consts.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_consts.hpp b/trunk/src/kernel/srs_kernel_consts.hpp index acb56b4c6..89c258b12 100644 --- a/trunk/src/kernel/srs_kernel_consts.hpp +++ b/trunk/src/kernel/srs_kernel_consts.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_error.cpp b/trunk/src/kernel/srs_kernel_error.cpp index f6d7340f8..3b3eadc9e 100644 --- a/trunk/src/kernel/srs_kernel_error.cpp +++ b/trunk/src/kernel/srs_kernel_error.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_error.hpp b/trunk/src/kernel/srs_kernel_error.hpp index de3040793..2d97327bf 100644 --- a/trunk/src/kernel/srs_kernel_error.hpp +++ b/trunk/src/kernel/srs_kernel_error.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_file.cpp b/trunk/src/kernel/srs_kernel_file.cpp index e884113b2..24f4ca879 100644 --- a/trunk/src/kernel/srs_kernel_file.cpp +++ b/trunk/src/kernel/srs_kernel_file.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_file.hpp b/trunk/src/kernel/srs_kernel_file.hpp index 46ed18b48..aec063104 100644 --- a/trunk/src/kernel/srs_kernel_file.hpp +++ b/trunk/src/kernel/srs_kernel_file.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_flv.cpp b/trunk/src/kernel/srs_kernel_flv.cpp index 102165ec3..d66816a79 100644 --- a/trunk/src/kernel/srs_kernel_flv.cpp +++ b/trunk/src/kernel/srs_kernel_flv.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_flv.hpp b/trunk/src/kernel/srs_kernel_flv.hpp index 67a7241ae..4fb9400e4 100644 --- a/trunk/src/kernel/srs_kernel_flv.hpp +++ b/trunk/src/kernel/srs_kernel_flv.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_io.cpp b/trunk/src/kernel/srs_kernel_io.cpp index 83bcfe03b..e83a0efbc 100644 --- a/trunk/src/kernel/srs_kernel_io.cpp +++ b/trunk/src/kernel/srs_kernel_io.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_io.hpp b/trunk/src/kernel/srs_kernel_io.hpp index ace2959fa..eaba237dc 100644 --- a/trunk/src/kernel/srs_kernel_io.hpp +++ b/trunk/src/kernel/srs_kernel_io.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_kbps.cpp b/trunk/src/kernel/srs_kernel_kbps.cpp index 6c2e9cb2a..fbaf2074a 100644 --- a/trunk/src/kernel/srs_kernel_kbps.cpp +++ b/trunk/src/kernel/srs_kernel_kbps.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_kbps.hpp b/trunk/src/kernel/srs_kernel_kbps.hpp index 99842a26e..7b90ce171 100644 --- a/trunk/src/kernel/srs_kernel_kbps.hpp +++ b/trunk/src/kernel/srs_kernel_kbps.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_log.cpp b/trunk/src/kernel/srs_kernel_log.cpp index 6cfe61068..9e9d23eda 100644 --- a/trunk/src/kernel/srs_kernel_log.cpp +++ b/trunk/src/kernel/srs_kernel_log.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_log.hpp b/trunk/src/kernel/srs_kernel_log.hpp index ca1cd9cd4..98692afb6 100644 --- a/trunk/src/kernel/srs_kernel_log.hpp +++ b/trunk/src/kernel/srs_kernel_log.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_mp3.cpp b/trunk/src/kernel/srs_kernel_mp3.cpp index 52e555090..6b7d9229d 100644 --- a/trunk/src/kernel/srs_kernel_mp3.cpp +++ b/trunk/src/kernel/srs_kernel_mp3.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_mp3.hpp b/trunk/src/kernel/srs_kernel_mp3.hpp index 66f32751e..f2e9c6055 100644 --- a/trunk/src/kernel/srs_kernel_mp3.hpp +++ b/trunk/src/kernel/srs_kernel_mp3.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_mp4.cpp b/trunk/src/kernel/srs_kernel_mp4.cpp index d3d5a7731..85a55f038 100644 --- a/trunk/src/kernel/srs_kernel_mp4.cpp +++ b/trunk/src/kernel/srs_kernel_mp4.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_mp4.hpp b/trunk/src/kernel/srs_kernel_mp4.hpp index 4e856df23..e0043faa5 100644 --- a/trunk/src/kernel/srs_kernel_mp4.hpp +++ b/trunk/src/kernel/srs_kernel_mp4.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp index 6dc5e41c6..bcc04d766 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp index 2433e92cf..c0da94f21 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_stream.cpp b/trunk/src/kernel/srs_kernel_stream.cpp index 3a32bd159..032c4bdc2 100755 --- a/trunk/src/kernel/srs_kernel_stream.cpp +++ b/trunk/src/kernel/srs_kernel_stream.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_stream.hpp b/trunk/src/kernel/srs_kernel_stream.hpp index eb786559e..4d8453f97 100644 --- a/trunk/src/kernel/srs_kernel_stream.hpp +++ b/trunk/src/kernel/srs_kernel_stream.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_ts.cpp b/trunk/src/kernel/srs_kernel_ts.cpp index d0e0d44a0..8bce1e9c4 100644 --- a/trunk/src/kernel/srs_kernel_ts.cpp +++ b/trunk/src/kernel/srs_kernel_ts.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_ts.hpp b/trunk/src/kernel/srs_kernel_ts.hpp index 0f505206a..303a5262b 100644 --- a/trunk/src/kernel/srs_kernel_ts.hpp +++ b/trunk/src/kernel/srs_kernel_ts.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_utility.cpp b/trunk/src/kernel/srs_kernel_utility.cpp index 2cd9d88f1..328267512 100644 --- a/trunk/src/kernel/srs_kernel_utility.cpp +++ b/trunk/src/kernel/srs_kernel_utility.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_utility.hpp b/trunk/src/kernel/srs_kernel_utility.hpp index 5a388d129..2df02dfe9 100644 --- a/trunk/src/kernel/srs_kernel_utility.hpp +++ b/trunk/src/kernel/srs_kernel_utility.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/main/srs_main_ingest_hls.cpp b/trunk/src/main/srs_main_ingest_hls.cpp index aabae1635..8bacabb45 100644 --- a/trunk/src/main/srs_main_ingest_hls.cpp +++ b/trunk/src/main/srs_main_ingest_hls.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/main/srs_main_mp4_parser.cpp b/trunk/src/main/srs_main_mp4_parser.cpp index c3ccfec14..5ce205fb5 100644 --- a/trunk/src/main/srs_main_mp4_parser.cpp +++ b/trunk/src/main/srs_main_mp4_parser.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/main/srs_main_server.cpp b/trunk/src/main/srs_main_server.cpp index d1657d2bc..9325e6e5e 100644 --- a/trunk/src/main/srs_main_server.cpp +++ b/trunk/src/main/srs_main_server.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_http_stack.cpp b/trunk/src/protocol/srs_http_stack.cpp index fa2f36367..440a3de1f 100644 --- a/trunk/src/protocol/srs_http_stack.cpp +++ b/trunk/src/protocol/srs_http_stack.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_http_stack.hpp b/trunk/src/protocol/srs_http_stack.hpp index 7e4c0da0e..736b81800 100644 --- a/trunk/src/protocol/srs_http_stack.hpp +++ b/trunk/src/protocol/srs_http_stack.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_amf0.cpp b/trunk/src/protocol/srs_protocol_amf0.cpp index 96b95f862..d1b63b290 100644 --- a/trunk/src/protocol/srs_protocol_amf0.cpp +++ b/trunk/src/protocol/srs_protocol_amf0.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_amf0.hpp b/trunk/src/protocol/srs_protocol_amf0.hpp index 7ab16fc3c..5f4d332fd 100644 --- a/trunk/src/protocol/srs_protocol_amf0.hpp +++ b/trunk/src/protocol/srs_protocol_amf0.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_format.cpp b/trunk/src/protocol/srs_protocol_format.cpp index 96c08c926..d7a966b22 100644 --- a/trunk/src/protocol/srs_protocol_format.cpp +++ b/trunk/src/protocol/srs_protocol_format.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_format.hpp b/trunk/src/protocol/srs_protocol_format.hpp index 1804256b3..fc3a9d947 100644 --- a/trunk/src/protocol/srs_protocol_format.hpp +++ b/trunk/src/protocol/srs_protocol_format.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_io.cpp b/trunk/src/protocol/srs_protocol_io.cpp index 52f3d0c06..1223d3750 100644 --- a/trunk/src/protocol/srs_protocol_io.cpp +++ b/trunk/src/protocol/srs_protocol_io.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_io.hpp b/trunk/src/protocol/srs_protocol_io.hpp index 756e5667d..02204c942 100644 --- a/trunk/src/protocol/srs_protocol_io.hpp +++ b/trunk/src/protocol/srs_protocol_io.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_json.cpp b/trunk/src/protocol/srs_protocol_json.cpp index 2a2117c0c..fc2b3092b 100644 --- a/trunk/src/protocol/srs_protocol_json.cpp +++ b/trunk/src/protocol/srs_protocol_json.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -1320,7 +1320,7 @@ void json_value_free (json_value * value) /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_json.hpp b/trunk/src/protocol/srs_protocol_json.hpp index 273b237dc..05c753bd2 100644 --- a/trunk/src/protocol/srs_protocol_json.hpp +++ b/trunk/src/protocol/srs_protocol_json.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_kbps.cpp b/trunk/src/protocol/srs_protocol_kbps.cpp index f0af4b210..fc6a93f43 100644 --- a/trunk/src/protocol/srs_protocol_kbps.cpp +++ b/trunk/src/protocol/srs_protocol_kbps.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_kbps.hpp b/trunk/src/protocol/srs_protocol_kbps.hpp index 429f7375b..d473a9794 100644 --- a/trunk/src/protocol/srs_protocol_kbps.hpp +++ b/trunk/src/protocol/srs_protocol_kbps.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_stream.cpp b/trunk/src/protocol/srs_protocol_stream.cpp index 6fa9cfb1a..c6f6b826c 100755 --- a/trunk/src/protocol/srs_protocol_stream.cpp +++ b/trunk/src/protocol/srs_protocol_stream.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_stream.hpp b/trunk/src/protocol/srs_protocol_stream.hpp index 237cf00fc..4320ebaa3 100644 --- a/trunk/src/protocol/srs_protocol_stream.hpp +++ b/trunk/src/protocol/srs_protocol_stream.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_utility.cpp b/trunk/src/protocol/srs_protocol_utility.cpp index ece4ef0bc..2ca056636 100644 --- a/trunk/src/protocol/srs_protocol_utility.cpp +++ b/trunk/src/protocol/srs_protocol_utility.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_protocol_utility.hpp b/trunk/src/protocol/srs_protocol_utility.hpp index eda484e36..1e8082cfd 100644 --- a/trunk/src/protocol/srs_protocol_utility.hpp +++ b/trunk/src/protocol/srs_protocol_utility.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_raw_avc.cpp b/trunk/src/protocol/srs_raw_avc.cpp index da98c6c67..f962fb6e5 100644 --- a/trunk/src/protocol/srs_raw_avc.cpp +++ b/trunk/src/protocol/srs_raw_avc.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_raw_avc.hpp b/trunk/src/protocol/srs_raw_avc.hpp index 944fac560..b7655689f 100644 --- a/trunk/src/protocol/srs_raw_avc.hpp +++ b/trunk/src/protocol/srs_raw_avc.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtmp_handshake.cpp b/trunk/src/protocol/srs_rtmp_handshake.cpp index 6736cf64f..056a715ce 100644 --- a/trunk/src/protocol/srs_rtmp_handshake.cpp +++ b/trunk/src/protocol/srs_rtmp_handshake.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtmp_handshake.hpp b/trunk/src/protocol/srs_rtmp_handshake.hpp index 75eef9305..6a6ed8ed1 100644 --- a/trunk/src/protocol/srs_rtmp_handshake.hpp +++ b/trunk/src/protocol/srs_rtmp_handshake.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtmp_msg_array.cpp b/trunk/src/protocol/srs_rtmp_msg_array.cpp index b7f86f436..9f6ef1a3b 100644 --- a/trunk/src/protocol/srs_rtmp_msg_array.cpp +++ b/trunk/src/protocol/srs_rtmp_msg_array.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtmp_msg_array.hpp b/trunk/src/protocol/srs_rtmp_msg_array.hpp index cdfdae209..ad6782372 100644 --- a/trunk/src/protocol/srs_rtmp_msg_array.hpp +++ b/trunk/src/protocol/srs_rtmp_msg_array.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtmp_stack.cpp b/trunk/src/protocol/srs_rtmp_stack.cpp index f80cf77cd..60668e747 100644 --- a/trunk/src/protocol/srs_rtmp_stack.cpp +++ b/trunk/src/protocol/srs_rtmp_stack.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtmp_stack.hpp b/trunk/src/protocol/srs_rtmp_stack.hpp index 6f311383f..e5c7b5a7d 100644 --- a/trunk/src/protocol/srs_rtmp_stack.hpp +++ b/trunk/src/protocol/srs_rtmp_stack.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtsp_stack.cpp b/trunk/src/protocol/srs_rtsp_stack.cpp index 1237f6454..8bc1937d5 100644 --- a/trunk/src/protocol/srs_rtsp_stack.cpp +++ b/trunk/src/protocol/srs_rtsp_stack.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtsp_stack.hpp b/trunk/src/protocol/srs_rtsp_stack.hpp index 87c07ce55..1000793f5 100644 --- a/trunk/src/protocol/srs_rtsp_stack.hpp +++ b/trunk/src/protocol/srs_rtsp_stack.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_conn.cpp b/trunk/src/protocol/srs_service_conn.cpp index 66b23a49e..48cc0f146 100644 --- a/trunk/src/protocol/srs_service_conn.cpp +++ b/trunk/src/protocol/srs_service_conn.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_conn.hpp b/trunk/src/protocol/srs_service_conn.hpp index c4c2e2602..2fb2b7d8d 100644 --- a/trunk/src/protocol/srs_service_conn.hpp +++ b/trunk/src/protocol/srs_service_conn.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_http_client.cpp b/trunk/src/protocol/srs_service_http_client.cpp index 6aabf7656..0c95f983b 100644 --- a/trunk/src/protocol/srs_service_http_client.cpp +++ b/trunk/src/protocol/srs_service_http_client.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_http_client.hpp b/trunk/src/protocol/srs_service_http_client.hpp index 351735f22..93e2a05b1 100644 --- a/trunk/src/protocol/srs_service_http_client.hpp +++ b/trunk/src/protocol/srs_service_http_client.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_http_conn.cpp b/trunk/src/protocol/srs_service_http_conn.cpp index d239972b2..2b8a5ef16 100644 --- a/trunk/src/protocol/srs_service_http_conn.cpp +++ b/trunk/src/protocol/srs_service_http_conn.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_http_conn.hpp b/trunk/src/protocol/srs_service_http_conn.hpp index 0181f9294..a1b3e381c 100644 --- a/trunk/src/protocol/srs_service_http_conn.hpp +++ b/trunk/src/protocol/srs_service_http_conn.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_log.cpp b/trunk/src/protocol/srs_service_log.cpp index 8c3be6eda..4c30d9450 100644 --- a/trunk/src/protocol/srs_service_log.cpp +++ b/trunk/src/protocol/srs_service_log.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_log.hpp b/trunk/src/protocol/srs_service_log.hpp index 6a4e14568..0a11979d7 100644 --- a/trunk/src/protocol/srs_service_log.hpp +++ b/trunk/src/protocol/srs_service_log.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_rtmp_conn.cpp b/trunk/src/protocol/srs_service_rtmp_conn.cpp index 84662fd3b..57584fd24 100644 --- a/trunk/src/protocol/srs_service_rtmp_conn.cpp +++ b/trunk/src/protocol/srs_service_rtmp_conn.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_rtmp_conn.hpp b/trunk/src/protocol/srs_service_rtmp_conn.hpp index 316b22403..55a8d44e1 100644 --- a/trunk/src/protocol/srs_service_rtmp_conn.hpp +++ b/trunk/src/protocol/srs_service_rtmp_conn.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_st.cpp b/trunk/src/protocol/srs_service_st.cpp index 6c55c586b..444e0ffba 100644 --- a/trunk/src/protocol/srs_service_st.cpp +++ b/trunk/src/protocol/srs_service_st.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_st.hpp b/trunk/src/protocol/srs_service_st.hpp index 3d51675e8..4f243b5e6 100644 --- a/trunk/src/protocol/srs_service_st.hpp +++ b/trunk/src/protocol/srs_service_st.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_service_utility.cpp b/trunk/src/protocol/srs_service_utility.cpp index 8cdb738ff..0069a64bc 100644 --- a/trunk/src/protocol/srs_service_utility.cpp +++ b/trunk/src/protocol/srs_service_utility.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -399,4 +399,4 @@ string srs_get_system_hostname() _srs_system_hostname = std::string(buf); return _srs_system_hostname; -} \ No newline at end of file +} diff --git a/trunk/src/protocol/srs_service_utility.hpp b/trunk/src/protocol/srs_service_utility.hpp index 8b35016fc..fc334a7af 100644 --- a/trunk/src/protocol/srs_service_utility.hpp +++ b/trunk/src/protocol/srs_service_utility.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Winlin + * Copyright (c) 2013-2021 Winlin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_conn.cpp b/trunk/src/srt/srt_conn.cpp index f2564aefe..aca43b494 100644 --- a/trunk/src/srt/srt_conn.cpp +++ b/trunk/src/srt/srt_conn.cpp @@ -214,4 +214,4 @@ int srt_conn::write(unsigned char* data, int len) { return ret; } return ret; -} \ No newline at end of file +} diff --git a/trunk/src/srt/srt_conn.hpp b/trunk/src/srt/srt_conn.hpp index beca33bcf..3bc2a9a4b 100644 --- a/trunk/src/srt/srt_conn.hpp +++ b/trunk/src/srt/srt_conn.hpp @@ -73,4 +73,4 @@ private: typedef std::shared_ptr SRT_CONN_PTR; -#endif //SRT_CONN_H \ No newline at end of file +#endif //SRT_CONN_H diff --git a/trunk/src/srt/srt_data.hpp b/trunk/src/srt/srt_data.hpp index 1d9e284dc..cb51d154a 100644 --- a/trunk/src/srt/srt_data.hpp +++ b/trunk/src/srt/srt_data.hpp @@ -53,4 +53,4 @@ private: typedef std::shared_ptr SRT_DATA_MSG_PTR; -#endif \ No newline at end of file +#endif diff --git a/trunk/src/srt/srt_handle.cpp b/trunk/src/srt/srt_handle.cpp index b4830ee49..c86bef6b5 100644 --- a/trunk/src/srt/srt_handle.cpp +++ b/trunk/src/srt/srt_handle.cpp @@ -398,4 +398,4 @@ void srt_handle::handle_srt_socket(SRT_SOCKSTATUS status, SRTSOCKET conn_fd) assert(0); } return; -} \ No newline at end of file +} diff --git a/trunk/src/srt/srt_handle.hpp b/trunk/src/srt/srt_handle.hpp index 54cece147..c177268c7 100644 --- a/trunk/src/srt/srt_handle.hpp +++ b/trunk/src/srt/srt_handle.hpp @@ -84,4 +84,4 @@ private: long long _last_check_alive_ts; }; -#endif //SRT_HANDLE_H \ No newline at end of file +#endif //SRT_HANDLE_H diff --git a/trunk/src/srt/srt_server.hpp b/trunk/src/srt/srt_server.hpp index ecea91858..f48238176 100644 --- a/trunk/src/srt/srt_server.hpp +++ b/trunk/src/srt/srt_server.hpp @@ -79,4 +79,4 @@ public: virtual void stop(); }; -#endif//SRT_SERVER_H \ No newline at end of file +#endif//SRT_SERVER_H diff --git a/trunk/src/srt/srt_to_rtmp.cpp b/trunk/src/srt/srt_to_rtmp.cpp index d4772784b..b04723def 100644 --- a/trunk/src/srt/srt_to_rtmp.cpp +++ b/trunk/src/srt/srt_to_rtmp.cpp @@ -716,4 +716,4 @@ bool rtmp_packet_queue::get_rtmp_data(rtmp_packet_info_s& packet_info) { _send_map.erase(iter); return true; -} \ No newline at end of file +} diff --git a/trunk/src/srt/srt_to_rtmp.hpp b/trunk/src/srt/srt_to_rtmp.hpp index 0fe07f878..e4dd69a2c 100644 --- a/trunk/src/srt/srt_to_rtmp.hpp +++ b/trunk/src/srt/srt_to_rtmp.hpp @@ -170,4 +170,4 @@ private: int64_t _lastcheck_ts; }; -#endif \ No newline at end of file +#endif diff --git a/trunk/src/srt/stringex.hpp b/trunk/src/srt/stringex.hpp index 54b42b068..0c0d9ea95 100644 --- a/trunk/src/srt/stringex.hpp +++ b/trunk/src/srt/stringex.hpp @@ -62,4 +62,4 @@ inline std::string string_lower(const std::string input_str) { return output_str; } -#endif//STRING_EX_H \ No newline at end of file +#endif//STRING_EX_H diff --git a/trunk/src/srt/ts_demux.cpp b/trunk/src/srt/ts_demux.cpp index 4666215ad..0e0ab68da 100644 --- a/trunk/src/srt/ts_demux.cpp +++ b/trunk/src/srt/ts_demux.cpp @@ -610,4 +610,4 @@ int ts_demux::pes_parse(unsigned char* p, size_t npos, } return pos; -} \ No newline at end of file +} diff --git a/trunk/src/srt/ts_demux.hpp b/trunk/src/srt/ts_demux.hpp index 662f726ce..64556b934 100644 --- a/trunk/src/srt/ts_demux.hpp +++ b/trunk/src/srt/ts_demux.hpp @@ -260,4 +260,4 @@ private: typedef std::shared_ptr TS_DEMUX_PTR; -#endif \ No newline at end of file +#endif diff --git a/trunk/src/srt/ts_demux_test.cpp b/trunk/src/srt/ts_demux_test.cpp index e7af92261..ec65ce182 100644 --- a/trunk/src/srt/ts_demux_test.cpp +++ b/trunk/src/srt/ts_demux_test.cpp @@ -75,4 +75,4 @@ int main(int argn, char** argv) { flen -= TS_MAX; } while(flen > 0); return 1; -} \ No newline at end of file +} diff --git a/trunk/src/utest/srs_utest.cpp b/trunk/src/utest/srs_utest.cpp index 8e400f1f0..4110aa609 100644 --- a/trunk/src/utest/srs_utest.cpp +++ b/trunk/src/utest/srs_utest.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest.hpp b/trunk/src/utest/srs_utest.hpp index 5f30930b3..2e17d8c05 100644 --- a/trunk/src/utest/srs_utest.hpp +++ b/trunk/src/utest/srs_utest.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_amf0.cpp b/trunk/src/utest/srs_utest_amf0.cpp index 9c322d227..c2f1a679c 100644 --- a/trunk/src/utest/srs_utest_amf0.cpp +++ b/trunk/src/utest/srs_utest_amf0.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_amf0.hpp b/trunk/src/utest/srs_utest_amf0.hpp index 6e7f205d9..f0afc7fd0 100644 --- a/trunk/src/utest/srs_utest_amf0.hpp +++ b/trunk/src/utest/srs_utest_amf0.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_app.cpp b/trunk/src/utest/srs_utest_app.cpp index a0ad704f7..a1f80b55e 100644 --- a/trunk/src/utest/srs_utest_app.cpp +++ b/trunk/src/utest/srs_utest_app.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_app.hpp b/trunk/src/utest/srs_utest_app.hpp index 45b3dadf0..fac3aa1be 100644 --- a/trunk/src/utest/srs_utest_app.hpp +++ b/trunk/src/utest/srs_utest_app.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_avc.cpp b/trunk/src/utest/srs_utest_avc.cpp index 60b4bb29d..e5b0c4f98 100644 --- a/trunk/src/utest/srs_utest_avc.cpp +++ b/trunk/src/utest/srs_utest_avc.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_avc.hpp b/trunk/src/utest/srs_utest_avc.hpp index 1a77a1fb4..36d1e0332 100644 --- a/trunk/src/utest/srs_utest_avc.hpp +++ b/trunk/src/utest/srs_utest_avc.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_config.cpp b/trunk/src/utest/srs_utest_config.cpp index 6f75d5eb9..5441e9123 100644 --- a/trunk/src/utest/srs_utest_config.cpp +++ b/trunk/src/utest/srs_utest_config.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_config.hpp b/trunk/src/utest/srs_utest_config.hpp index fc07fb97a..215ed29be 100644 --- a/trunk/src/utest/srs_utest_config.hpp +++ b/trunk/src/utest/srs_utest_config.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_core.cpp b/trunk/src/utest/srs_utest_core.cpp index 40ed33243..493ce6c9d 100644 --- a/trunk/src/utest/srs_utest_core.cpp +++ b/trunk/src/utest/srs_utest_core.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_core.hpp b/trunk/src/utest/srs_utest_core.hpp index 02c86a186..3f478ff8b 100644 --- a/trunk/src/utest/srs_utest_core.hpp +++ b/trunk/src/utest/srs_utest_core.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_http.cpp b/trunk/src/utest/srs_utest_http.cpp index c5dfea841..3f53723f4 100644 --- a/trunk/src/utest/srs_utest_http.cpp +++ b/trunk/src/utest/srs_utest_http.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_http.hpp b/trunk/src/utest/srs_utest_http.hpp index bb7f576a0..71df3aa45 100644 --- a/trunk/src/utest/srs_utest_http.hpp +++ b/trunk/src/utest/srs_utest_http.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_kernel.cpp b/trunk/src/utest/srs_utest_kernel.cpp index f246bd99d..0ad93b1ed 100644 --- a/trunk/src/utest/srs_utest_kernel.cpp +++ b/trunk/src/utest/srs_utest_kernel.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_kernel.hpp b/trunk/src/utest/srs_utest_kernel.hpp index f32d1f063..97fe3d275 100644 --- a/trunk/src/utest/srs_utest_kernel.hpp +++ b/trunk/src/utest/srs_utest_kernel.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_mp4.cpp b/trunk/src/utest/srs_utest_mp4.cpp index c5861b88b..1252cfe42 100644 --- a/trunk/src/utest/srs_utest_mp4.cpp +++ b/trunk/src/utest/srs_utest_mp4.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_mp4.hpp b/trunk/src/utest/srs_utest_mp4.hpp index e5ba3d2fa..0b5015c47 100644 --- a/trunk/src/utest/srs_utest_mp4.hpp +++ b/trunk/src/utest/srs_utest_mp4.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_protocol.cpp b/trunk/src/utest/srs_utest_protocol.cpp index faaff3b20..728903856 100644 --- a/trunk/src/utest/srs_utest_protocol.cpp +++ b/trunk/src/utest/srs_utest_protocol.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_protocol.hpp b/trunk/src/utest/srs_utest_protocol.hpp index f81b3eb3b..3e0091f50 100644 --- a/trunk/src/utest/srs_utest_protocol.hpp +++ b/trunk/src/utest/srs_utest_protocol.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_reload.cpp b/trunk/src/utest/srs_utest_reload.cpp index 115951755..08c5dada9 100644 --- a/trunk/src/utest/srs_utest_reload.cpp +++ b/trunk/src/utest/srs_utest_reload.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_reload.hpp b/trunk/src/utest/srs_utest_reload.hpp index ff8d7894f..71e14ca4c 100644 --- a/trunk/src/utest/srs_utest_reload.hpp +++ b/trunk/src/utest/srs_utest_reload.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_rtc.cpp b/trunk/src/utest/srs_utest_rtc.cpp index beabf73bf..125705870 100644 --- a/trunk/src/utest/srs_utest_rtc.cpp +++ b/trunk/src/utest/srs_utest_rtc.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_rtc.hpp b/trunk/src/utest/srs_utest_rtc.hpp index 4fa39fbb8..4b740eff4 100644 --- a/trunk/src/utest/srs_utest_rtc.hpp +++ b/trunk/src/utest/srs_utest_rtc.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_rtmp.cpp b/trunk/src/utest/srs_utest_rtmp.cpp index 3450d5ba8..7d6526a79 100644 --- a/trunk/src/utest/srs_utest_rtmp.cpp +++ b/trunk/src/utest/srs_utest_rtmp.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_rtmp.hpp b/trunk/src/utest/srs_utest_rtmp.hpp index 9a850d261..43c8d219d 100644 --- a/trunk/src/utest/srs_utest_rtmp.hpp +++ b/trunk/src/utest/srs_utest_rtmp.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_service.cpp b/trunk/src/utest/srs_utest_service.cpp index 4562d27bf..c5eb50f6e 100644 --- a/trunk/src/utest/srs_utest_service.cpp +++ b/trunk/src/utest/srs_utest_service.cpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/utest/srs_utest_service.hpp b/trunk/src/utest/srs_utest_service.hpp index ab4ed8ea3..f9de671f0 100644 --- a/trunk/src/utest/srs_utest_service.hpp +++ b/trunk/src/utest/srs_utest_service.hpp @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2013-2020 Winlin +Copyright (c) 2013-2021 Winlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 124455be09dab9f1ed06f3cfebe6394e84b57719 Mon Sep 17 00:00:00 2001 From: winlin Date: Fri, 16 Apr 2021 09:27:47 +0800 Subject: [PATCH 053/563] Update script --- trunk/scripts/git.commit.sh | 83 +------------------------------------ 1 file changed, 1 insertion(+), 82 deletions(-) diff --git a/trunk/scripts/git.commit.sh b/trunk/scripts/git.commit.sh index bed252315..803b2dd33 100755 --- a/trunk/scripts/git.commit.sh +++ b/trunk/scripts/git.commit.sh @@ -1,85 +1,4 @@ #!/bin/bash -cat <>/dev/null -touch git-ensure-commit && -echo "cd `pwd` &&" >git-ensure-commit && -echo "bash `pwd`/git.commit.sh" >>git-ensure-commit && -chmod +x git-ensure-commit && -sudo rm -f /bin/git-ensure-commit && -sudo mv git-ensure-commit /usr/local/bin/git-ensure-commit -END +for file in $(git remote); do echo ""; git push $file; done -echo "submit code to github.com" - -echo "argv[0]=$0" -if [[ ! -f $0 ]]; then - echo "directly execute the scripts on shell."; - work_dir=`pwd` -else - echo "execute scripts in file: $0"; - work_dir=`dirname $0`; work_dir=`(cd ${work_dir} && pwd)` -fi -work_dir=`(cd ${work_dir}/.. && pwd)` -product_dir=$work_dir - -# allow start script from any dir -cd $work_dir && work_branch=`git branch|grep "*"|awk '{print $2}'` && commit_branch=2.0release && git checkout $commit_branch -ret=$ret; if [[ $ret -ne 0 ]]; then echo "switch branch failed. ret=$ret"; exit $ret; fi -echo "work branch is $work_branch" -echo "commit branch is $commit_branch" - -. ${product_dir}/scripts/_log.sh -ret=$?; if [[ $ret -ne 0 ]]; then exit $ret; fi -ok_msg "导入脚本成功" - -function remote_check() -{ - remote=$1 - url=$2 - git remote -v| grep "$url" >/dev/null 2>&1 - ret=$?; if [[ 0 -ne $ret ]]; then - echo "remote $remote not found, add by:" - echo " git remote add $remote $url" - exit -1 - fi - ok_msg "remote $remote ok, url is $url" -} -remote_check origin git@github.com:ossrs/srs.git -remote_check srs.winlin git@github.com:winlinvip/simple-rtmp-server.git -remote_check srs.csdn git@code.csdn.net:winlinvip/srs-csdn.git -remote_check srs.oschina git@git.oschina.net:winlinvip/srs.oschina.git -remote_check srs.gitlab git@gitlab.com:winlinvip/srs-gitlab.git -ok_msg "remote check ok" - -function sync_push() -{ - for ((;;)); do - git push $* - ret=$?; if [[ 0 -ne $ret ]]; then - failed_msg "Retry for failed: git push $*" - sleep 3 - continue - else - ok_msg "Success: git push $*" - fi - break - done -} - -sync_push origin -sync_push srs.winlin -sync_push srs.csdn -sync_push srs.oschina -sync_push srs.gitlab -ok_msg "push branches ok" - -sync_push --tags srs.winlin -sync_push --tags srs.csdn -sync_push --tags srs.oschina -sync_push --tags srs.gitlab -ok_msg "push tags ok" - -git checkout $work_branch -ok_msg "switch to work branch $work_branch" - -exit 0 From 650fa0af0c751ab1120f783af8d53448a2e6c019 Mon Sep 17 00:00:00 2001 From: winlin Date: Fri, 16 Apr 2021 11:09:23 +0800 Subject: [PATCH 054/563] SquashSRS3: Update description --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 81ba1ebd0..7ed08a22f 100755 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ [![](https://codecov.io/gh/ossrs/srs/branch/4.0release/graph/badge.svg)](https://codecov.io/gh/ossrs/srs/branch/4.0release) [![](https://cloud.githubusercontent.com/assets/2777660/22814959/c51cbe72-ef92-11e6-81cc-32b657b285d5.png)](https://github.com/ossrs/srs/wiki/v1_CN_Contact#wechat) -SRS/4.0,[Leo][release4],是一个流媒体集群,支持RTMP/HLS/WebRTC/SRT/GB28181,高效、稳定、易用,简单而快乐。
-SRS is a RTMP/HLS/WebRTC/SRT/GB28181 streaming cluster, high efficiency, stable and simple. +SRS/4.0,[Leo][release4],是一个简单高效的实时视频服务器,支持RTMP/WebRTC/HLS/HTTP-FLV/SRT/GB28181/RTSP。
+SRS is a simple, high efficiency and realtime video server, supports RTMP/WebRTC/HLS/HTTP-FLV/SRT/GB28181/RTSP. > Remark: Although SRS is licenced under [MIT][LICENSE], but there are some depended libraries which are distributed using their own licenses, please read [License Mixing][LicenseMixing]. From 23342363a7d0e788549a86e9588a198ad69aa592 Mon Sep 17 00:00:00 2001 From: winlin Date: Fri, 16 Apr 2021 11:13:34 +0800 Subject: [PATCH 055/563] SquashSRS3: Update description --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ed08a22f..526d46006 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ [![](https://codecov.io/gh/ossrs/srs/branch/4.0release/graph/badge.svg)](https://codecov.io/gh/ossrs/srs/branch/4.0release) [![](https://cloud.githubusercontent.com/assets/2777660/22814959/c51cbe72-ef92-11e6-81cc-32b657b285d5.png)](https://github.com/ossrs/srs/wiki/v1_CN_Contact#wechat) -SRS/4.0,[Leo][release4],是一个简单高效的实时视频服务器,支持RTMP/WebRTC/HLS/HTTP-FLV/SRT/GB28181/RTSP。
+SRS/4.0,[Leo][release4],是一个简单高效的实时视频服务器,支持RTMP/WebRTC/HLS/HTTP-FLV/SRT/GB28181/RTSP。 + SRS is a simple, high efficiency and realtime video server, supports RTMP/WebRTC/HLS/HTTP-FLV/SRT/GB28181/RTSP. > Remark: Although SRS is licenced under [MIT][LICENSE], but there are some depended libraries which are distributed using their own licenses, please read [License Mixing][LicenseMixing]. From cec0191b163c6309547e59106b51a5cc690b97e7 Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 20 Apr 2021 19:00:14 +0800 Subject: [PATCH 056/563] Happy 2021 --- trunk/src/app/srs_app_gb28181.cpp | 2 +- trunk/src/app/srs_app_gb28181.hpp | 2 +- trunk/src/app/srs_app_gb28181_sip.cpp | 2 +- trunk/src/app/srs_app_gb28181_sip.hpp | 2 +- trunk/src/app/srs_app_rtc_codec.cpp | 2 +- trunk/src/app/srs_app_rtc_codec.hpp | 2 +- trunk/src/app/srs_app_rtc_conn.cpp | 2 +- trunk/src/app/srs_app_rtc_conn.hpp | 2 +- trunk/src/app/srs_app_rtc_jitbuffer.cpp | 2 +- trunk/src/app/srs_app_rtc_jitbuffer.hpp | 2 +- trunk/src/app/srs_app_rtc_queue.cpp | 2 +- trunk/src/app/srs_app_rtc_queue.hpp | 2 +- trunk/src/app/srs_app_rtc_sdp.cpp | 2 +- trunk/src/app/srs_app_rtc_sdp.hpp | 2 +- trunk/src/app/srs_app_rtc_server.cpp | 2 +- trunk/src/app/srs_app_rtc_server.hpp | 2 +- trunk/src/app/srs_app_rtc_source.cpp | 2 +- trunk/src/app/srs_app_rtc_source.hpp | 2 +- trunk/src/app/srs_app_sip.cpp | 2 +- trunk/src/app/srs_app_sip.hpp | 2 +- trunk/src/kernel/srs_kernel_rtc_rtcp.cpp | 2 +- trunk/src/kernel/srs_kernel_rtc_rtcp.hpp | 2 +- trunk/src/protocol/srs_rtc_stun_stack.cpp | 2 +- trunk/src/protocol/srs_rtc_stun_stack.hpp | 2 +- trunk/src/srt/srt_conn.cpp | 2 +- trunk/src/srt/srt_conn.hpp | 2 +- trunk/src/srt/srt_data.cpp | 2 +- trunk/src/srt/srt_data.hpp | 2 +- trunk/src/srt/srt_handle.cpp | 2 +- trunk/src/srt/srt_handle.hpp | 2 +- trunk/src/srt/srt_server.cpp | 2 +- trunk/src/srt/srt_server.hpp | 2 +- trunk/src/srt/srt_to_rtmp.cpp | 2 +- trunk/src/srt/srt_to_rtmp.hpp | 2 +- trunk/src/srt/stringex.hpp | 2 +- trunk/src/srt/ts_demux.cpp | 2 +- trunk/src/srt/ts_demux.hpp | 2 +- trunk/src/srt/ts_demux_test.cpp | 2 +- 38 files changed, 38 insertions(+), 38 deletions(-) diff --git a/trunk/src/app/srs_app_gb28181.cpp b/trunk/src/app/srs_app_gb28181.cpp index 2106afcde..4663d25f9 100644 --- a/trunk/src/app/srs_app_gb28181.cpp +++ b/trunk/src/app/srs_app_gb28181.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Lixin + * Copyright (c) 2013-2021 Lixin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_gb28181.hpp b/trunk/src/app/srs_app_gb28181.hpp index 7694066c1..fdc421fbe 100644 --- a/trunk/src/app/srs_app_gb28181.hpp +++ b/trunk/src/app/srs_app_gb28181.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Lixin + * Copyright (c) 2013-2021 Lixin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_gb28181_sip.cpp b/trunk/src/app/srs_app_gb28181_sip.cpp index 5a8972397..1ca695c6d 100644 --- a/trunk/src/app/srs_app_gb28181_sip.cpp +++ b/trunk/src/app/srs_app_gb28181_sip.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Lixin + * Copyright (c) 2013-2021 Lixin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_gb28181_sip.hpp b/trunk/src/app/srs_app_gb28181_sip.hpp index 7a37b92d6..23dd99c33 100644 --- a/trunk/src/app/srs_app_gb28181_sip.hpp +++ b/trunk/src/app/srs_app_gb28181_sip.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Lixin + * Copyright (c) 2013-2021 Lixin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_codec.cpp b/trunk/src/app/srs_app_rtc_codec.cpp index 7194f9605..2c9ebd2a9 100644 --- a/trunk/src/app/srs_app_rtc_codec.cpp +++ b/trunk/src/app/srs_app_rtc_codec.cpp @@ -2,7 +2,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Bepartofyou + * Copyright (c) 2013-2021 Bepartofyou * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_codec.hpp b/trunk/src/app/srs_app_rtc_codec.hpp index 23df35414..b9da7daf5 100644 --- a/trunk/src/app/srs_app_rtc_codec.hpp +++ b/trunk/src/app/srs_app_rtc_codec.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Bepartofyou + * Copyright (c) 2013-2021 Bepartofyou * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 0f2b6de9a..297dacf59 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_conn.hpp b/trunk/src/app/srs_app_rtc_conn.hpp index 12187fae2..694f63c73 100644 --- a/trunk/src/app/srs_app_rtc_conn.hpp +++ b/trunk/src/app/srs_app_rtc_conn.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_jitbuffer.cpp b/trunk/src/app/srs_app_rtc_jitbuffer.cpp index f030932d2..40a1b6fbd 100644 --- a/trunk/src/app/srs_app_rtc_jitbuffer.cpp +++ b/trunk/src/app/srs_app_rtc_jitbuffer.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Lixin + * Copyright (c) 2013-2021 Lixin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_jitbuffer.hpp b/trunk/src/app/srs_app_rtc_jitbuffer.hpp index a7682a2b9..d0521c2f8 100644 --- a/trunk/src/app/srs_app_rtc_jitbuffer.hpp +++ b/trunk/src/app/srs_app_rtc_jitbuffer.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Lixin + * Copyright (c) 2013-2021 Lixin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_queue.cpp b/trunk/src/app/srs_app_rtc_queue.cpp index ee166eacd..7a19f2b57 100644 --- a/trunk/src/app/srs_app_rtc_queue.cpp +++ b/trunk/src/app/srs_app_rtc_queue.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_queue.hpp b/trunk/src/app/srs_app_rtc_queue.hpp index b1d2b2cac..5cd644442 100644 --- a/trunk/src/app/srs_app_rtc_queue.hpp +++ b/trunk/src/app/srs_app_rtc_queue.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_sdp.cpp b/trunk/src/app/srs_app_rtc_sdp.cpp index facb8fbd1..fe4e5f412 100644 --- a/trunk/src/app/srs_app_rtc_sdp.cpp +++ b/trunk/src/app/srs_app_rtc_sdp.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_sdp.hpp b/trunk/src/app/srs_app_rtc_sdp.hpp index 16c3f8e01..01d65bd5c 100644 --- a/trunk/src/app/srs_app_rtc_sdp.hpp +++ b/trunk/src/app/srs_app_rtc_sdp.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_server.cpp b/trunk/src/app/srs_app_rtc_server.cpp index 6ca4cfbf4..311da5050 100644 --- a/trunk/src/app/srs_app_rtc_server.cpp +++ b/trunk/src/app/srs_app_rtc_server.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_server.hpp b/trunk/src/app/srs_app_rtc_server.hpp index ecae6d980..16aa12857 100644 --- a/trunk/src/app/srs_app_rtc_server.hpp +++ b/trunk/src/app/srs_app_rtc_server.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index 5d4c333a2..a1a3d699d 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_rtc_source.hpp b/trunk/src/app/srs_app_rtc_source.hpp index 1ea9c5a41..e29711feb 100644 --- a/trunk/src/app/srs_app_rtc_source.hpp +++ b/trunk/src/app/srs_app_rtc_source.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_sip.cpp b/trunk/src/app/srs_app_sip.cpp index bf65481d6..c7ff221f0 100644 --- a/trunk/src/app/srs_app_sip.cpp +++ b/trunk/src/app/srs_app_sip.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Lixin + * Copyright (c) 2013-2021 Lixin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/app/srs_app_sip.hpp b/trunk/src/app/srs_app_sip.hpp index c6d4e1131..eea753797 100644 --- a/trunk/src/app/srs_app_sip.hpp +++ b/trunk/src/app/srs_app_sip.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Lixin + * Copyright (c) 2013-2021 Lixin * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_rtc_rtcp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtcp.cpp index 2552887fe..284053142 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtcp.cpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtcp.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 LiPeng + * Copyright (c) 2013-2021 LiPeng * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/kernel/srs_kernel_rtc_rtcp.hpp b/trunk/src/kernel/srs_kernel_rtc_rtcp.hpp index b469b814a..78df377e1 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtcp.hpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtcp.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 LiPeng + * Copyright (c) 2013-2021 LiPeng * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtc_stun_stack.cpp b/trunk/src/protocol/srs_rtc_stun_stack.cpp index fe7f2385f..9fdfc98e1 100644 --- a/trunk/src/protocol/srs_rtc_stun_stack.cpp +++ b/trunk/src/protocol/srs_rtc_stun_stack.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/protocol/srs_rtc_stun_stack.hpp b/trunk/src/protocol/srs_rtc_stun_stack.hpp index 682a6c84d..fede60b32 100644 --- a/trunk/src/protocol/srs_rtc_stun_stack.hpp +++ b/trunk/src/protocol/srs_rtc_stun_stack.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 John + * Copyright (c) 2013-2021 John * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_conn.cpp b/trunk/src/srt/srt_conn.cpp index aca43b494..ccdaa257b 100644 --- a/trunk/src/srt/srt_conn.cpp +++ b/trunk/src/srt/srt_conn.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_conn.hpp b/trunk/src/srt/srt_conn.hpp index 3bc2a9a4b..df6106235 100644 --- a/trunk/src/srt/srt_conn.hpp +++ b/trunk/src/srt/srt_conn.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_data.cpp b/trunk/src/srt/srt_data.cpp index 5e1b65d54..c8802fd06 100644 --- a/trunk/src/srt/srt_data.cpp +++ b/trunk/src/srt/srt_data.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_data.hpp b/trunk/src/srt/srt_data.hpp index cb51d154a..fb46fccba 100644 --- a/trunk/src/srt/srt_data.hpp +++ b/trunk/src/srt/srt_data.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_handle.cpp b/trunk/src/srt/srt_handle.cpp index c86bef6b5..c947e8288 100644 --- a/trunk/src/srt/srt_handle.cpp +++ b/trunk/src/srt/srt_handle.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_handle.hpp b/trunk/src/srt/srt_handle.hpp index c177268c7..fb67acf97 100644 --- a/trunk/src/srt/srt_handle.hpp +++ b/trunk/src/srt/srt_handle.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_server.cpp b/trunk/src/srt/srt_server.cpp index 76f203749..c2d174030 100644 --- a/trunk/src/srt/srt_server.cpp +++ b/trunk/src/srt/srt_server.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_server.hpp b/trunk/src/srt/srt_server.hpp index f48238176..d2ae413fd 100644 --- a/trunk/src/srt/srt_server.hpp +++ b/trunk/src/srt/srt_server.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_to_rtmp.cpp b/trunk/src/srt/srt_to_rtmp.cpp index b04723def..33657b040 100644 --- a/trunk/src/srt/srt_to_rtmp.cpp +++ b/trunk/src/srt/srt_to_rtmp.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/srt_to_rtmp.hpp b/trunk/src/srt/srt_to_rtmp.hpp index e4dd69a2c..9d4e288e4 100644 --- a/trunk/src/srt/srt_to_rtmp.hpp +++ b/trunk/src/srt/srt_to_rtmp.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/stringex.hpp b/trunk/src/srt/stringex.hpp index 0c0d9ea95..10cbc8915 100644 --- a/trunk/src/srt/stringex.hpp +++ b/trunk/src/srt/stringex.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/ts_demux.cpp b/trunk/src/srt/ts_demux.cpp index 0e0ab68da..6d2a1b27a 100644 --- a/trunk/src/srt/ts_demux.cpp +++ b/trunk/src/srt/ts_demux.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/ts_demux.hpp b/trunk/src/srt/ts_demux.hpp index 64556b934..7dc14b432 100644 --- a/trunk/src/srt/ts_demux.hpp +++ b/trunk/src/srt/ts_demux.hpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in diff --git a/trunk/src/srt/ts_demux_test.cpp b/trunk/src/srt/ts_demux_test.cpp index ec65ce182..bfc81ee8a 100644 --- a/trunk/src/srt/ts_demux_test.cpp +++ b/trunk/src/srt/ts_demux_test.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2020 Runner365 + * Copyright (c) 2013-2021 Runner365 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in From 9ed7565789e34dfe8f0a35b4f8ec480c4a4f8b33 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 24 Apr 2021 17:51:05 +0800 Subject: [PATCH 057/563] SquashSRS3: Package srs-console --- README.md | 4 ++++ trunk/scripts/package.sh | 19 +++++++++++++++++-- trunk/src/core/srs_core_version3.hpp | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 526d46006..3c7bcdc20 100755 --- a/README.md +++ b/README.md @@ -268,6 +268,8 @@ Other documents: ## V3 changes +* v3.0, 2021-04-24, [3.0 release4(3.0.159)][r3.0r4] released. 122750 lines. +* v3.0, 2021-04-24, Package srs-console to zip and docker. 3.0.159 * v3.0, 2021-03-05, Refine usage to docker by default. 3.0.158 * v3.0, 2021-01-07, Change id from int to string for the statistics. 3.0.157 * v3.0, 2021-01-02, [3.0 release3(3.0.156)][r3.0r3] released. 122736 lines. @@ -903,6 +905,7 @@ Other documents: ## Releases +* 2021-04-24, [Release v3.0-r4][r3.0r4], 3.0 release4, 3.0.159, 122750 lines. * 2021-01-02, [Release v3.0-r3][r3.0r3], 3.0 release3, 3.0.156, 122736 lines. * 2020-10-31, [Release v3.0-r2][r3.0r2], 3.0 release2, 3.0.153, 122663 lines. * 2020-10-10, [Release v3.0-r1][r3.0r1], 3.0 release1, 3.0.144, 122674 lines. @@ -1893,6 +1896,7 @@ Winlin [exo #828]: https://github.com/google/ExoPlayer/pull/828 +[r3.0r4]: https://github.com/ossrs/srs/releases/tag/v3.0-r4 [r3.0r3]: https://github.com/ossrs/srs/releases/tag/v3.0-r3 [r3.0r2]: https://github.com/ossrs/srs/releases/tag/v3.0-r2 [r3.0r1]: https://github.com/ossrs/srs/releases/tag/v3.0-r1 diff --git a/trunk/scripts/package.sh b/trunk/scripts/package.sh index ecd233450..d89e04b47 100755 --- a/trunk/scripts/package.sh +++ b/trunk/scripts/package.sh @@ -17,6 +17,8 @@ MIPS=NO # EMBEDED=NO JOBS=1 +# +SRS_CONSOLE= ################################################################################## ################################################################################## @@ -26,8 +28,8 @@ for option do case "$option" in -*=*) - value=`echo "$option" | sed -e 's|[-_a-zA-Z0-9/]*=||'` - option=`echo "$option" | sed -e 's|=[-_a-zA-Z0-9/]*||'` + value=`echo "$option" | sed -e 's|[-_a-zA-Z0-9/]*=||'` + option=`echo "$option" | sed -e 's|=[-_a-zA-Z0-9/~]*||'` ;; *) value="" ;; esac @@ -41,6 +43,7 @@ do --arm) ARM=YES ;; --pi) PI=YES ;; --jobs) JOBS=$value ;; + --console) SRS_CONSOLE=$value ;; *) echo "$0: error: invalid option \"$option\", @see $0 --help" @@ -59,6 +62,8 @@ if [ $help = yes ]; then --pi for pi platform, configure/make/package. --x86-64 alias for --x86-x64. --jobs Set the configure and make jobs. + + --console The path for https://github.com/ossrs/srs-console END exit 0 fi @@ -151,6 +156,16 @@ ok_msg "start install srs" ret=$?; if [[ 0 -ne ${ret} ]]; then failed_msg "install srs failed"; exit $ret; fi ok_msg "install srs success" +# Copy srs-console +if [[ $SRS_CONSOLE != "" ]]; then + HTTP_HOME="objs/nginx/html/" + CONSOLE_HOME="trunk/research/console" + cp -R $SRS_CONSOLE/index.html ${package_dir}/${INSTALL}/${HTTP_HOME} >> $log 2>&1 + cp -R $SRS_CONSOLE/${CONSOLE_HOME} ${package_dir}/${INSTALL}/${HTTP_HOME} >> $log 2>&1 + ret=$?; if [[ 0 -ne ${ret} ]]; then failed_msg "copy console files failed"; exit $ret; fi + ok_msg "copy console files success" +fi + # copy extra files to package. ok_msg "start copy extra files to package" ( diff --git a/trunk/src/core/srs_core_version3.hpp b/trunk/src/core/srs_core_version3.hpp index 31113e861..49ab6fa23 100644 --- a/trunk/src/core/srs_core_version3.hpp +++ b/trunk/src/core/srs_core_version3.hpp @@ -24,6 +24,6 @@ #ifndef SRS_CORE_VERSION3_HPP #define SRS_CORE_VERSION3_HPP -#define SRS_VERSION3_REVISION 158 +#define SRS_VERSION3_REVISION 159 #endif From de1c7522f046dfcd1354faf673b59da15c66032f Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 24 Apr 2021 19:07:37 +0800 Subject: [PATCH 058/563] SquashSRS3: Add console --- README.md | 7 +- trunk/auto/depends.sh | 8 + trunk/configure | 29 +- .../research/api-server/static-dir/index.html | 64 +- trunk/research/console/en_index.html | 68 + trunk/research/console/index.html | 12 + .../console/js/3rdparty/angular-resource.js | 610 + .../js/3rdparty/angular-resource.min.js | 13 + .../js/3rdparty/angular-resource.min.js.map | 8 + .../console/js/3rdparty/angular-route.js | 927 + .../console/js/3rdparty/angular-route.min.js | 14 + .../js/3rdparty/angular-route.min.js.map | 8 + trunk/research/console/js/3rdparty/angular.js | 21674 ++++++++++++++++ .../console/js/3rdparty/angular.min.js | 212 + .../console/js/3rdparty/angular.min.js.map | 8 + .../console/js/3rdparty/bootstrap.css | 6167 +++++ .../research/console/js/3rdparty/bootstrap.js | 2280 ++ .../console/js/3rdparty/bootstrap.min.css | 9 + .../console/js/3rdparty/bootstrap.min.js | 6 + .../research/console/js/bravo_alert/alert.js | 45 + .../console/js/bravo_popover/popover.js | 233 + .../js/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../console/js/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes trunk/research/console/js/srs.cn.js | 106 + trunk/research/console/js/srs.console.css | 23 + trunk/research/console/js/srs.console.js | 1375 + trunk/research/console/js/srs.en.js | 107 + trunk/research/console/js/winlin.utility.js | 685 + trunk/research/console/ng_index.html | 68 + trunk/research/console/views/client.html | 25 + trunk/research/console/views/client_en.html | 25 + trunk/research/console/views/clients.html | 39 + trunk/research/console/views/clients_en.html | 39 + trunk/research/console/views/config.html | 1007 + trunk/research/console/views/config_en.html | 1004 + trunk/research/console/views/configs.html | 452 + trunk/research/console/views/configs_en.html | 452 + trunk/research/console/views/connect.html | 40 + trunk/research/console/views/connect_en.html | 40 + trunk/research/console/views/dvr.html | 62 + trunk/research/console/views/dvr_en.html | 62 + trunk/research/console/views/dvrs.html | 1 + trunk/research/console/views/dvrs_en.html | 1 + trunk/research/console/views/stream.html | 27 + trunk/research/console/views/stream_en.html | 27 + trunk/research/console/views/streams.html | 47 + trunk/research/console/views/streams_en.html | 47 + trunk/research/console/views/summary.html | 137 + trunk/research/console/views/summary_en.html | 137 + trunk/research/console/views/vhost.html | 23 + trunk/research/console/views/vhost_en.html | 23 + trunk/research/console/views/vhosts.html | 37 + trunk/research/console/views/vhosts_en.html | 37 + trunk/scripts/package.sh | 21 +- trunk/src/core/srs_core_version3.hpp | 2 +- 55 files changed, 38494 insertions(+), 86 deletions(-) create mode 100644 trunk/research/console/en_index.html create mode 100644 trunk/research/console/index.html create mode 100644 trunk/research/console/js/3rdparty/angular-resource.js create mode 100644 trunk/research/console/js/3rdparty/angular-resource.min.js create mode 100644 trunk/research/console/js/3rdparty/angular-resource.min.js.map create mode 100644 trunk/research/console/js/3rdparty/angular-route.js create mode 100644 trunk/research/console/js/3rdparty/angular-route.min.js create mode 100644 trunk/research/console/js/3rdparty/angular-route.min.js.map create mode 100644 trunk/research/console/js/3rdparty/angular.js create mode 100644 trunk/research/console/js/3rdparty/angular.min.js create mode 100644 trunk/research/console/js/3rdparty/angular.min.js.map create mode 100644 trunk/research/console/js/3rdparty/bootstrap.css create mode 100644 trunk/research/console/js/3rdparty/bootstrap.js create mode 100644 trunk/research/console/js/3rdparty/bootstrap.min.css create mode 100644 trunk/research/console/js/3rdparty/bootstrap.min.js create mode 100755 trunk/research/console/js/bravo_alert/alert.js create mode 100755 trunk/research/console/js/bravo_popover/popover.js create mode 100644 trunk/research/console/js/img/glyphicons-halflings-white.png create mode 100644 trunk/research/console/js/img/glyphicons-halflings.png create mode 100644 trunk/research/console/js/srs.cn.js create mode 100644 trunk/research/console/js/srs.console.css create mode 100644 trunk/research/console/js/srs.console.js create mode 100644 trunk/research/console/js/srs.en.js create mode 100644 trunk/research/console/js/winlin.utility.js create mode 100644 trunk/research/console/ng_index.html create mode 100644 trunk/research/console/views/client.html create mode 100644 trunk/research/console/views/client_en.html create mode 100644 trunk/research/console/views/clients.html create mode 100644 trunk/research/console/views/clients_en.html create mode 100644 trunk/research/console/views/config.html create mode 100644 trunk/research/console/views/config_en.html create mode 100644 trunk/research/console/views/configs.html create mode 100644 trunk/research/console/views/configs_en.html create mode 100644 trunk/research/console/views/connect.html create mode 100644 trunk/research/console/views/connect_en.html create mode 100644 trunk/research/console/views/dvr.html create mode 100644 trunk/research/console/views/dvr_en.html create mode 100644 trunk/research/console/views/dvrs.html create mode 100644 trunk/research/console/views/dvrs_en.html create mode 100644 trunk/research/console/views/stream.html create mode 100644 trunk/research/console/views/stream_en.html create mode 100644 trunk/research/console/views/streams.html create mode 100644 trunk/research/console/views/streams_en.html create mode 100644 trunk/research/console/views/summary.html create mode 100644 trunk/research/console/views/summary_en.html create mode 100644 trunk/research/console/views/vhost.html create mode 100644 trunk/research/console/views/vhost_en.html create mode 100644 trunk/research/console/views/vhosts.html create mode 100644 trunk/research/console/views/vhosts_en.html diff --git a/README.md b/README.md index 3c7bcdc20..772c74b5b 100755 --- a/README.md +++ b/README.md @@ -268,8 +268,9 @@ Other documents: ## V3 changes -* v3.0, 2021-04-24, [3.0 release4(3.0.159)][r3.0r4] released. 122750 lines. -* v3.0, 2021-04-24, Package srs-console to zip and docker. 3.0.159 +* v3.0, 2021-04-24, [3.0 release4(3.0.160)][r3.0r4] released. 122750 lines. +* v3.0, 2021-04-24, Package players and console to zip and docker. 3.0.160 +* v3.0, 2021-04-24, Add srs-console to research/console. 3.0.159 * v3.0, 2021-03-05, Refine usage to docker by default. 3.0.158 * v3.0, 2021-01-07, Change id from int to string for the statistics. 3.0.157 * v3.0, 2021-01-02, [3.0 release3(3.0.156)][r3.0r3] released. 122736 lines. @@ -905,7 +906,7 @@ Other documents: ## Releases -* 2021-04-24, [Release v3.0-r4][r3.0r4], 3.0 release4, 3.0.159, 122750 lines. +* 2021-04-24, [Release v3.0-r4][r3.0r4], 3.0 release4, 3.0.160, 122750 lines. * 2021-01-02, [Release v3.0-r3][r3.0r3], 3.0 release3, 3.0.156, 122736 lines. * 2020-10-31, [Release v3.0-r2][r3.0r2], 3.0 release2, 3.0.153, 122663 lines. * 2020-10-10, [Release v3.0-r1][r3.0r1], 3.0 release1, 3.0.144, 122674 lines. diff --git a/trunk/auto/depends.sh b/trunk/auto/depends.sh index 9033ef7e3..0f6ee1a8d 100755 --- a/trunk/auto/depends.sh +++ b/trunk/auto/depends.sh @@ -430,6 +430,14 @@ ln -sf `pwd`/research/players/crossdomain.xml ${SRS_OBJS}/nginx/html/crossdomain rm -rf ${SRS_OBJS}/nginx/html/favicon.ico && ln -sf `pwd`/research/api-server/static-dir/favicon.ico ${SRS_OBJS}/nginx/html/favicon.ico +# For srs-console. +rm -rf ${SRS_OBJS}/nginx/html/console && +ln -sf `pwd`/research/console ${SRS_OBJS}/nginx/html/console + +# For home page index.html +rm -rf ${SRS_OBJS}/nginx/html/index.html && +ln -sf `pwd`/research/api-server/static-dir/index.html ${SRS_OBJS}/nginx/html/index.html + # nginx.html to detect whether nginx is alive echo "Nginx is ok." > ${SRS_OBJS}/nginx/html/nginx.html diff --git a/trunk/configure b/trunk/configure index 70a8319f9..76c7cf020 100755 --- a/trunk/configure +++ b/trunk/configure @@ -458,7 +458,7 @@ mv ${SRS_WORKDIR}/${SRS_MAKEFILE} ${SRS_WORKDIR}/${SRS_MAKEFILE}.bk # generate phony header cat << END > ${SRS_WORKDIR}/${SRS_MAKEFILE} -.PHONY: default _default install install-api help clean destroy server srs_ingest_hls utest _prepare_dir $__mphonys +.PHONY: default _default install help clean destroy server srs_ingest_hls utest _prepare_dir $__mphonys .PHONY: clean_srs clean_modules clean_openssl clean_nginx clean_cherrypy clean_srtp2 clean_opus clean_ffmpeg clean_st .PHONY: st ffmpeg @@ -477,14 +477,13 @@ _default: server srs_ingest_hls utest __modules $__mdefaults @bash objs/_srs_build_summary.sh help: - @echo "Usage: make |||||||" + @echo "Usage: make ||||||" @echo " help Display this help menu" @echo " clean Cleanup project and all depends" @echo " destroy Cleanup all files for this platform in ${SRS_OBJS_DIR}/${SRS_PLATFORM}" @echo " server Build the srs and other modules in main" @echo " utest Build the utest for srs" @echo " install Install srs to the prefix path" - @echo " install-api Install srs and api-server to the prefix path" @echo " uninstall Uninstall srs from prefix path" @echo "To rebuild special module:" @echo " st Rebuild st-srs in ${SRS_OBJS_DIR}/${SRS_PLATFORM}/st-srs" @@ -589,30 +588,6 @@ uninstall: @echo "rmdir \$(SRS_PREFIX)" @rm -rf \$(SRS_PREFIX) -install-api: install - @echo "Now mkdir \$(__REAL_INSTALL)" - @mkdir -p \$(__REAL_INSTALL) - @echo "Now copy binary files" - @mkdir -p \$(__REAL_INSTALL)/research/api-server - @cp research/api-server/server.py \$(__REAL_INSTALL)/research/api-server - @mkdir -p \$(__REAL_INSTALL)/objs/ffmpeg/bin - @cp objs/ffmpeg/bin/ffmpeg \$(__REAL_INSTALL)/objs/ffmpeg/bin - @echo "Now copy html files" - @mkdir -p \$(__REAL_INSTALL)/research/api-server/static-dir/players - @cp research/api-server/static-dir/crossdomain.xml \$(__REAL_INSTALL)/research/api-server/static-dir - @cp research/api-server/static-dir/index.html \$(__REAL_INSTALL)/research/api-server/static-dir - @cp -r research/api-server/static-dir/players/* \$(__REAL_INSTALL)/research/api-server/static-dir/players - @echo "Now copy init.d script files" - @mkdir -p \$(__REAL_INSTALL)/etc/init.d - @cp etc/init.d/srs-api \$(__REAL_INSTALL)/etc/init.d - @sed -i "s|^ROOT=.*|ROOT=\"\$(SRS_PREFIX)\"|g" \$(__REAL_INSTALL)/etc/init.d/srs-api - @echo "" - @echo "The api installed, to link and start api:" - @echo " sudo ln -sf \$(SRS_PREFIX)/etc/init.d/srs-api /etc/init.d/srs-api" - @echo " /etc/init.d/srs-api start" - @echo " http://\$(shell bash auto/local_ip.sh):8085" - @echo "@see: https://github.com/ossrs/srs/wiki/v1_CN_LinuxService" - install: @echo "Now mkdir \$(__REAL_INSTALL)" @mkdir -p \$(__REAL_INSTALL) diff --git a/trunk/research/api-server/static-dir/index.html b/trunk/research/api-server/static-dir/index.html index b4b4782c8..b3ecfc979 100755 --- a/trunk/research/api-server/static-dir/index.html +++ b/trunk/research/api-server/static-dir/index.html @@ -1,50 +1,26 @@ - + - SRS + SRS - - - - - - - -

-
-
- -
+

SRS works!

+

+ Click here to enter SRS console.
+ 点击进入SRS控制台 +

+

+ Click here to start SRS player.
+ 点击进入SRS播放器 +

+

SRS Team © 2021

+ diff --git a/trunk/research/console/en_index.html b/trunk/research/console/en_index.html new file mode 100644 index 000000000..c0aa7df67 --- /dev/null +++ b/trunk/research/console/en_index.html @@ -0,0 +1,68 @@ + + + + + SRS控制台 + + + + + + + + + + + + + + + +
+
+
+ + {{log.level}}: {{log.msg}} +
+
+ + + + diff --git a/trunk/research/console/index.html b/trunk/research/console/index.html new file mode 100644 index 000000000..3b90af64e --- /dev/null +++ b/trunk/research/console/index.html @@ -0,0 +1,12 @@ + + + + + SrsConsole + + + + + diff --git a/trunk/research/console/js/3rdparty/angular-resource.js b/trunk/research/console/js/3rdparty/angular-resource.js new file mode 100644 index 000000000..712b32ead --- /dev/null +++ b/trunk/research/console/js/3rdparty/angular-resource.js @@ -0,0 +1,610 @@ +/** + * @license AngularJS v1.2.17 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +var $resourceMinErr = angular.$$minErr('$resource'); + +// Helper functions and regex to lookup a dotted path on an object +// stopping at undefined/null. The path must be composed of ASCII +// identifiers (just like $parse) +var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/; + +function isValidDottedPath(path) { + return (path != null && path !== '' && path !== 'hasOwnProperty' && + MEMBER_NAME_REGEX.test('.' + path)); +} + +function lookupDottedPath(obj, path) { + if (!isValidDottedPath(path)) { + throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); + } + var keys = path.split('.'); + for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) { + var key = keys[i]; + obj = (obj !== null) ? obj[key] : undefined; + } + return obj; +} + +/** + * Create a shallow copy of an object and clear other fields from the destination + */ +function shallowClearAndCopy(src, dst) { + dst = dst || {}; + + angular.forEach(dst, function(value, key){ + delete dst[key]; + }); + + for (var key in src) { + if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + + return dst; +} + +/** + * @ngdoc module + * @name ngResource + * @description + * + * # ngResource + * + * The `ngResource` module provides interaction support with RESTful services + * via the $resource service. + * + * + *
+ * + * See {@link ngResource.$resource `$resource`} for usage. + */ + +/** + * @ngdoc service + * @name $resource + * @requires $http + * + * @description + * A factory which creates a resource object that lets you interact with + * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. + * + * The returned resource object has action methods which provide high-level behaviors without + * the need to interact with the low level {@link ng.$http $http} service. + * + * Requires the {@link ngResource `ngResource`} module to be installed. + * + * @param {string} url A parametrized URL template with parameters prefixed by `:` as in + * `/user/:username`. If you are using a URL with a port number (e.g. + * `http://example.com:8080/api`), it will be respected. + * + * If you are using a url with a suffix, just add the suffix, like this: + * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` + * or even `$resource('http://example.com/resource/:resource_id.:format')` + * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be + * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you + * can escape it with `/\.`. + * + * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in + * `actions` methods. If any of the parameter value is a function, it will be executed every time + * when a param value needs to be obtained for a request (unless the param was overridden). + * + * Each key value in the parameter object is first bound to url template if present and then any + * excess keys are appended to the url search query after the `?`. + * + * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + * URL `/path/greet?salutation=Hello`. + * + * If the parameter value is prefixed with `@` then the value of that parameter will be taken + * from the corresponding key on the data object (useful for non-GET operations). + * + * @param {Object.=} actions Hash with declaration of custom action that should extend + * the default set of resource actions. The declaration should be created in the format of {@link + * ng.$http#usage_parameters $http.config}: + * + * {action1: {method:?, params:?, isArray:?, headers:?, ...}, + * action2: {method:?, params:?, isArray:?, headers:?, ...}, + * ...} + * + * Where: + * + * - **`action`** – {string} – The name of action. This name becomes the name of the method on + * your resource object. + * - **`method`** – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, + * `DELETE`, and `JSONP`. + * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of + * the parameter value is a function, it will be executed every time when a param value needs to + * be obtained for a request (unless the param was overridden). + * - **`url`** – {string} – action specific `url` override. The url templating is supported just + * like for the resource-level urls. + * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, + * see `returns` section. + * - **`transformRequest`** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * request body and headers and returns its transformed (typically serialized) version. + * - **`transformResponse`** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * response body and headers and returns its transformed (typically deserialized) version. + * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the + * GET request, otherwise if a cache instance built with + * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for + * caching. + * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that + * should abort the request when resolved. + * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the + * XHR object. See + * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) + * for more information. + * - **`responseType`** - `{string}` - see + * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). + * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - + * `response` and `responseError`. Both `response` and `responseError` interceptors get called + * with `http response` object. See {@link ng.$http $http interceptors}. + * + * @returns {Object} A resource "class" object with methods for the default set of resource actions + * optionally extended with custom `actions`. The default set contains these actions: + * ```js + * { 'get': {method:'GET'}, + * 'save': {method:'POST'}, + * 'query': {method:'GET', isArray:true}, + * 'remove': {method:'DELETE'}, + * 'delete': {method:'DELETE'} }; + * ``` + * + * Calling these methods invoke an {@link ng.$http} with the specified http method, + * destination and parameters. When the data is returned from the server then the object is an + * instance of the resource class. The actions `save`, `remove` and `delete` are available on it + * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, + * read, update, delete) on server-side data like this: + * ```js + * var User = $resource('/user/:userId', {userId:'@id'}); + * var user = User.get({userId:123}, function() { + * user.abc = true; + * user.$save(); + * }); + * ``` + * + * It is important to realize that invoking a $resource object method immediately returns an + * empty reference (object or array depending on `isArray`). Once the data is returned from the + * server the existing reference is populated with the actual data. This is a useful trick since + * usually the resource is assigned to a model which is then rendered by the view. Having an empty + * object results in no rendering, once the data arrives from the server then the object is + * populated with the data and the view automatically re-renders itself showing the new data. This + * means that in most cases one never has to write a callback function for the action methods. + * + * The action methods on the class object or instance object can be invoked with the following + * parameters: + * + * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` + * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` + * - non-GET instance actions: `instance.$action([parameters], [success], [error])` + * + * Success callback is called with (value, responseHeaders) arguments. Error callback is called + * with (httpResponse) argument. + * + * Class actions return empty instance (with additional properties below). + * Instance actions return promise of the action. + * + * The Resource instances and collection have these additional properties: + * + * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this + * instance or collection. + * + * On success, the promise is resolved with the same resource instance or collection object, + * updated with data from server. This makes it easy to use in + * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view + * rendering until the resource(s) are loaded. + * + * On failure, the promise is resolved with the {@link ng.$http http response} object, without + * the `resource` property. + * + * If an interceptor object was provided, the promise will instead be resolved with the value + * returned by the interceptor. + * + * - `$resolved`: `true` after first server interaction is completed (either with success or + * rejection), `false` before that. Knowing if the Resource has been resolved is useful in + * data-binding. + * + * @example + * + * # Credit card resource + * + * ```js + // Define CreditCard class + var CreditCard = $resource('/user/:userId/card/:cardId', + {userId:123, cardId:'@id'}, { + charge: {method:'POST', params:{charge:true}} + }); + + // We can retrieve a collection from the server + var cards = CreditCard.query(function() { + // GET: /user/123/card + // server returns: [ {id:456, number:'1234', name:'Smith'} ]; + + var card = cards[0]; + // each item is an instance of CreditCard + expect(card instanceof CreditCard).toEqual(true); + card.name = "J. Smith"; + // non GET methods are mapped onto the instances + card.$save(); + // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} + // server returns: {id:456, number:'1234', name: 'J. Smith'}; + + // our custom method is mapped as well. + card.$charge({amount:9.99}); + // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} + }); + + // we can create an instance as well + var newCard = new CreditCard({number:'0123'}); + newCard.name = "Mike Smith"; + newCard.$save(); + // POST: /user/123/card {number:'0123', name:'Mike Smith'} + // server returns: {id:789, number:'0123', name: 'Mike Smith'}; + expect(newCard.id).toEqual(789); + * ``` + * + * The object returned from this function execution is a resource "class" which has "static" method + * for each action in the definition. + * + * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and + * `headers`. + * When the data is returned from the server then the object is an instance of the resource type and + * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD + * operations (create, read, update, delete) on server-side data. + + ```js + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}, function(user) { + user.abc = true; + user.$save(); + }); + ``` + * + * It's worth noting that the success callback for `get`, `query` and other methods gets passed + * in the response that came from the server as well as $http header getter function, so one + * could rewrite the above example and get access to http headers as: + * + ```js + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}, function(u, getResponseHeaders){ + u.abc = true; + u.$save(function(u, putResponseHeaders) { + //u => saved user object + //putResponseHeaders => $http header getter + }); + }); + ``` + * + * You can also access the raw `$http` promise via the `$promise` property on the object returned + * + ``` + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}) + .$promise.then(function(user) { + $scope.user = user; + }); + ``` + + * # Creating a custom 'PUT' request + * In this example we create a custom method on our resource to make a PUT request + * ```js + * var app = angular.module('app', ['ngResource', 'ngRoute']); + * + * // Some APIs expect a PUT request in the format URL/object/ID + * // Here we are creating an 'update' method + * app.factory('Notes', ['$resource', function($resource) { + * return $resource('/notes/:id', null, + * { + * 'update': { method:'PUT' } + * }); + * }]); + * + * // In our controller we get the ID from the URL using ngRoute and $routeParams + * // We pass in $routeParams and our Notes factory along with $scope + * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', + function($scope, $routeParams, Notes) { + * // First get a note object from the factory + * var note = Notes.get({ id:$routeParams.id }); + * $id = note.id; + * + * // Now call update passing in the ID first then the object you are updating + * Notes.update({ id:$id }, note); + * + * // This will PUT /notes/ID with the note object in the request payload + * }]); + * ``` + */ +angular.module('ngResource', ['ng']). + factory('$resource', ['$http', '$q', function($http, $q) { + + var DEFAULT_ACTIONS = { + 'get': {method:'GET'}, + 'save': {method:'POST'}, + 'query': {method:'GET', isArray:true}, + 'remove': {method:'DELETE'}, + 'delete': {method:'DELETE'} + }; + var noop = angular.noop, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + isFunction = angular.isFunction; + + /** + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow + * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path + * segments: + * segment = *pchar + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * pct-encoded = "%" HEXDIG HEXDIG + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriSegment(val) { + return encodeUriQuery(val, true). + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); + } + + + /** + * This method is intended for encoding *key* or *value* parts of query component. We need a + * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't + * have to be encoded per http://tools.ietf.org/html/rfc3986: + * query = *( pchar / "/" / "?" ) + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriQuery(val, pctEncodeSpaces) { + return encodeURIComponent(val). + replace(/%40/gi, '@'). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); + } + + function Route(template, defaults) { + this.template = template; + this.defaults = defaults || {}; + this.urlParams = {}; + } + + Route.prototype = { + setUrlParams: function(config, params, actionUrl) { + var self = this, + url = actionUrl || self.template, + val, + encodedVal; + + var urlParams = self.urlParams = {}; + forEach(url.split(/\W/), function(param){ + if (param === 'hasOwnProperty') { + throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); + } + if (!(new RegExp("^\\d+$").test(param)) && param && + (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { + urlParams[param] = true; + } + }); + url = url.replace(/\\:/g, ':'); + + params = params || {}; + forEach(self.urlParams, function(_, urlParam){ + val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; + if (angular.isDefined(val) && val !== null) { + encodedVal = encodeUriSegment(val); + url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { + return encodedVal + p1; + }); + } else { + url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, + leadingSlashes, tail) { + if (tail.charAt(0) == '/') { + return tail; + } else { + return leadingSlashes + tail; + } + }); + } + }); + + // strip trailing slashes and set the url + url = url.replace(/\/+$/, '') || '/'; + // then replace collapse `/.` if found in the last URL path segment before the query + // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` + url = url.replace(/\/\.(?=\w+($|\?))/, '.'); + // replace escaped `/\.` with `/.` + config.url = url.replace(/\/\\\./, '/.'); + + + // set params - delegate param encoding to $http + forEach(params, function(value, key){ + if (!self.urlParams[key]) { + config.params = config.params || {}; + config.params[key] = value; + } + }); + } + }; + + + function resourceFactory(url, paramDefaults, actions) { + var route = new Route(url); + + actions = extend({}, DEFAULT_ACTIONS, actions); + + function extractParams(data, actionParams){ + var ids = {}; + actionParams = extend({}, paramDefaults, actionParams); + forEach(actionParams, function(value, key){ + if (isFunction(value)) { value = value(); } + ids[key] = value && value.charAt && value.charAt(0) == '@' ? + lookupDottedPath(data, value.substr(1)) : value; + }); + return ids; + } + + function defaultResponseInterceptor(response) { + return response.resource; + } + + function Resource(value){ + shallowClearAndCopy(value || {}, this); + } + + forEach(actions, function(action, name) { + var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); + + Resource[name] = function(a1, a2, a3, a4) { + var params = {}, data, success, error; + + /* jshint -W086 */ /* (purposefully fall through case statements) */ + switch(arguments.length) { + case 4: + error = a4; + success = a3; + //fallthrough + case 3: + case 2: + if (isFunction(a2)) { + if (isFunction(a1)) { + success = a1; + error = a2; + break; + } + + success = a2; + error = a3; + //fallthrough + } else { + params = a1; + data = a2; + success = a3; + break; + } + case 1: + if (isFunction(a1)) success = a1; + else if (hasBody) data = a1; + else params = a1; + break; + case 0: break; + default: + throw $resourceMinErr('badargs', + "Expected up to 4 arguments [params, data, success, error], got {0} arguments", + arguments.length); + } + /* jshint +W086 */ /* (purposefully fall through case statements) */ + + var isInstanceCall = this instanceof Resource; + var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); + var httpConfig = {}; + var responseInterceptor = action.interceptor && action.interceptor.response || + defaultResponseInterceptor; + var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || + undefined; + + forEach(action, function(value, key) { + if (key != 'params' && key != 'isArray' && key != 'interceptor') { + httpConfig[key] = copy(value); + } + }); + + if (hasBody) httpConfig.data = data; + route.setUrlParams(httpConfig, + extend({}, extractParams(data, action.params || {}), params), + action.url); + + var promise = $http(httpConfig).then(function(response) { + var data = response.data, + promise = value.$promise; + + if (data) { + // Need to convert action.isArray to boolean in case it is undefined + // jshint -W018 + if (angular.isArray(data) !== (!!action.isArray)) { + throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected ' + + 'response to contain an {0} but got an {1}', + action.isArray?'array':'object', angular.isArray(data)?'array':'object'); + } + // jshint +W018 + if (action.isArray) { + value.length = 0; + forEach(data, function(item) { + value.push(new Resource(item)); + }); + } else { + shallowClearAndCopy(data, value); + value.$promise = promise; + } + } + + value.$resolved = true; + + response.resource = value; + + return response; + }, function(response) { + value.$resolved = true; + + (error||noop)(response); + + return $q.reject(response); + }); + + promise = promise.then( + function(response) { + var value = responseInterceptor(response); + (success||noop)(value, response.headers); + return value; + }, + responseErrorInterceptor); + + if (!isInstanceCall) { + // we are creating instance / collection + // - set the initial promise + // - return the instance / collection + value.$promise = promise; + value.$resolved = false; + + return value; + } + + // instance call + return promise; + }; + + + Resource.prototype['$' + name] = function(params, success, error) { + if (isFunction(params)) { + error = success; success = params; params = {}; + } + var result = Resource[name].call(this, params, this, success, error); + return result.$promise || result; + }; + }); + + Resource.bind = function(additionalParamDefaults){ + return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); + }; + + return Resource; + } + + return resourceFactory; + }]); + + +})(window, window.angular); diff --git a/trunk/research/console/js/3rdparty/angular-resource.min.js b/trunk/research/console/js/3rdparty/angular-resource.min.js new file mode 100644 index 000000000..badd75458 --- /dev/null +++ b/trunk/research/console/js/3rdparty/angular-resource.min.js @@ -0,0 +1,13 @@ +/* + AngularJS v1.2.17 + (c) 2010-2014 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(H,a,A){'use strict';function D(p,g){g=g||{};a.forEach(g,function(a,c){delete g[c]});for(var c in p)!p.hasOwnProperty(c)||"$"===c.charAt(0)&&"$"===c.charAt(1)||(g[c]=p[c]);return g}var v=a.$$minErr("$resource"),C=/^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;a.module("ngResource",["ng"]).factory("$resource",["$http","$q",function(p,g){function c(a,c){this.template=a;this.defaults=c||{};this.urlParams={}}function t(n,w,l){function r(h,d){var e={};d=x({},w,d);s(d,function(b,d){u(b)&&(b=b());var k;if(b&& +b.charAt&&"@"==b.charAt(0)){k=h;var a=b.substr(1);if(null==a||""===a||"hasOwnProperty"===a||!C.test("."+a))throw v("badmember",a);for(var a=a.split("."),f=0,c=a.length;f + */ + /* global -ngRouteModule */ +var ngRouteModule = angular.module('ngRoute', ['ng']). + provider('$route', $RouteProvider); + +/** + * @ngdoc provider + * @name $routeProvider + * @kind function + * + * @description + * + * Used for configuring routes. + * + * ## Example + * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. + * + * ## Dependencies + * Requires the {@link ngRoute `ngRoute`} module to be installed. + */ +function $RouteProvider(){ + function inherit(parent, extra) { + return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra); + } + + var routes = {}; + + /** + * @ngdoc method + * @name $routeProvider#when + * + * @param {string} path Route path (matched against `$location.path`). If `$location.path` + * contains redundant trailing slash or is missing one, the route will still match and the + * `$location.path` will be updated to add or drop the trailing slash to exactly match the + * route definition. + * + * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up + * to the next slash are matched and stored in `$routeParams` under the given `name` + * when the route matches. + * * `path` can contain named groups starting with a colon and ending with a star: + * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` + * when the route matches. + * * `path` can contain optional named groups with a question mark: e.g.`:name?`. + * + * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match + * `/color/brown/largecode/code/with/slashes/edit` and extract: + * + * * `color: brown` + * * `largecode: code/with/slashes`. + * + * + * @param {Object} route Mapping information to be assigned to `$route.current` on route + * match. + * + * Object properties: + * + * - `controller` – `{(string|function()=}` – Controller fn that should be associated with + * newly created scope or the name of a {@link angular.Module#controller registered + * controller} if passed as a string. + * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be + * published to scope under the `controllerAs` name. + * - `template` – `{string=|function()=}` – html template as a string or a function that + * returns an html template as a string which should be used by {@link + * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. + * This property takes precedence over `templateUrl`. + * + * If `template` is a function, it will be called with the following parameters: + * + * - `{Array.}` - route parameters extracted from the current + * `$location.path()` by applying the current route + * + * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * template that should be used by {@link ngRoute.directive:ngView ngView}. + * + * If `templateUrl` is a function, it will be called with the following parameters: + * + * - `{Array.}` - route parameters extracted from the current + * `$location.path()` by applying the current route + * + * - `resolve` - `{Object.=}` - An optional map of dependencies which should + * be injected into the controller. If any of these dependencies are promises, the router + * will wait for them all to be resolved or one to be rejected before the controller is + * instantiated. + * If all the promises are resolved successfully, the values of the resolved promises are + * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is + * fired. If any of the promises are rejected the + * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object + * is: + * + * - `key` – `{string}`: a name of a dependency to be injected into the controller. + * - `factory` - `{string|function}`: If `string` then it is an alias for a service. + * Otherwise if function, then it is {@link auto.$injector#invoke injected} + * and the return value is treated as the dependency. If the result is a promise, it is + * resolved before its value is injected into the controller. Be aware that + * `ngRoute.$routeParams` will still refer to the previous route within these resolve + * functions. Use `$route.current.params` to access the new route parameters, instead. + * + * - `redirectTo` – {(string|function())=} – value to update + * {@link ng.$location $location} path with and trigger route redirection. + * + * If `redirectTo` is a function, it will be called with the following parameters: + * + * - `{Object.}` - route parameters extracted from the current + * `$location.path()` by applying the current route templateUrl. + * - `{string}` - current `$location.path()` + * - `{Object}` - current `$location.search()` + * + * The custom `redirectTo` function is expected to return a string which will be used + * to update `$location.path()` and `$location.search()`. + * + * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` + * or `$location.hash()` changes. + * + * If the option is set to `false` and url in the browser changes, then + * `$routeUpdate` event is broadcasted on the root scope. + * + * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive + * + * If the option is set to `true`, then the particular route can be matched without being + * case sensitive + * + * @returns {Object} self + * + * @description + * Adds a new route definition to the `$route` service. + */ + this.when = function(path, route) { + routes[path] = angular.extend( + {reloadOnSearch: true}, + route, + path && pathRegExp(path, route) + ); + + // create redirection for trailing slashes + if (path) { + var redirectPath = (path[path.length-1] == '/') + ? path.substr(0, path.length-1) + : path +'/'; + + routes[redirectPath] = angular.extend( + {redirectTo: path}, + pathRegExp(redirectPath, route) + ); + } + + return this; + }; + + /** + * @param path {string} path + * @param opts {Object} options + * @return {?Object} + * + * @description + * Normalizes the given path, returning a regular expression + * and the original path. + * + * Inspired by pathRexp in visionmedia/express/lib/utils.js. + */ + function pathRegExp(path, opts) { + var insensitive = opts.caseInsensitiveMatch, + ret = { + originalPath: path, + regexp: path + }, + keys = ret.keys = []; + + path = path + .replace(/([().])/g, '\\$1') + .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){ + var optional = option === '?' ? option : null; + var star = option === '*' ? option : null; + keys.push({ name: key, optional: !!optional }); + slash = slash || ''; + return '' + + (optional ? '' : slash) + + '(?:' + + (optional ? slash : '') + + (star && '(.+?)' || '([^/]+)') + + (optional || '') + + ')' + + (optional || ''); + }) + .replace(/([\/$\*])/g, '\\$1'); + + ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); + return ret; + } + + /** + * @ngdoc method + * @name $routeProvider#otherwise + * + * @description + * Sets route definition that will be used on route change when no other route definition + * is matched. + * + * @param {Object} params Mapping information to be assigned to `$route.current`. + * @returns {Object} self + */ + this.otherwise = function(params) { + this.when(null, params); + return this; + }; + + + this.$get = ['$rootScope', + '$location', + '$routeParams', + '$q', + '$injector', + '$http', + '$templateCache', + '$sce', + function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { + + /** + * @ngdoc service + * @name $route + * @requires $location + * @requires $routeParams + * + * @property {Object} current Reference to the current route definition. + * The route definition contains: + * + * - `controller`: The controller constructor as define in route definition. + * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for + * controller instantiation. The `locals` contain + * the resolved values of the `resolve` map. Additionally the `locals` also contain: + * + * - `$scope` - The current route scope. + * - `$template` - The current route template HTML. + * + * @property {Object} routes Object with all route configuration Objects as its properties. + * + * @description + * `$route` is used for deep-linking URLs to controllers and views (HTML partials). + * It watches `$location.url()` and tries to map the path to an existing route definition. + * + * Requires the {@link ngRoute `ngRoute`} module to be installed. + * + * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. + * + * The `$route` service is typically used in conjunction with the + * {@link ngRoute.directive:ngView `ngView`} directive and the + * {@link ngRoute.$routeParams `$routeParams`} service. + * + * @example + * This example shows how changing the URL hash causes the `$route` to match a route against the + * URL, and the `ngView` pulls in the partial. + * + * Note that this example is using {@link ng.directive:script inlined templates} + * to get it working on jsfiddle as well. + * + * + * + *
+ * Choose: + * Moby | + * Moby: Ch1 | + * Gatsby | + * Gatsby: Ch4 | + * Scarlet Letter
+ * + *
+ * + *
+ * + *
$location.path() = {{$location.path()}}
+ *
$route.current.templateUrl = {{$route.current.templateUrl}}
+ *
$route.current.params = {{$route.current.params}}
+ *
$route.current.scope.name = {{$route.current.scope.name}}
+ *
$routeParams = {{$routeParams}}
+ *
+ *
+ * + * + * controller: {{name}}
+ * Book Id: {{params.bookId}}
+ *
+ * + * + * controller: {{name}}
+ * Book Id: {{params.bookId}}
+ * Chapter Id: {{params.chapterId}} + *
+ * + * + * angular.module('ngRouteExample', ['ngRoute']) + * + * .controller('MainController', function($scope, $route, $routeParams, $location) { + * $scope.$route = $route; + * $scope.$location = $location; + * $scope.$routeParams = $routeParams; + * }) + * + * .controller('BookController', function($scope, $routeParams) { + * $scope.name = "BookController"; + * $scope.params = $routeParams; + * }) + * + * .controller('ChapterController', function($scope, $routeParams) { + * $scope.name = "ChapterController"; + * $scope.params = $routeParams; + * }) + * + * .config(function($routeProvider, $locationProvider) { + * $routeProvider + * .when('/Book/:bookId', { + * templateUrl: 'book.html', + * controller: 'BookController', + * resolve: { + * // I will cause a 1 second delay + * delay: function($q, $timeout) { + * var delay = $q.defer(); + * $timeout(delay.resolve, 1000); + * return delay.promise; + * } + * } + * }) + * .when('/Book/:bookId/ch/:chapterId', { + * templateUrl: 'chapter.html', + * controller: 'ChapterController' + * }); + * + * // configure html5 to get links working on jsfiddle + * $locationProvider.html5Mode(true); + * }); + * + * + * + * + * it('should load and compile correct template', function() { + * element(by.linkText('Moby: Ch1')).click(); + * var content = element(by.css('[ng-view]')).getText(); + * expect(content).toMatch(/controller\: ChapterController/); + * expect(content).toMatch(/Book Id\: Moby/); + * expect(content).toMatch(/Chapter Id\: 1/); + * + * element(by.partialLinkText('Scarlet')).click(); + * + * content = element(by.css('[ng-view]')).getText(); + * expect(content).toMatch(/controller\: BookController/); + * expect(content).toMatch(/Book Id\: Scarlet/); + * }); + * + *
+ */ + + /** + * @ngdoc event + * @name $route#$routeChangeStart + * @eventType broadcast on root scope + * @description + * Broadcasted before a route change. At this point the route services starts + * resolving all of the dependencies needed for the route change to occur. + * Typically this involves fetching the view template as well as any dependencies + * defined in `resolve` route property. Once all of the dependencies are resolved + * `$routeChangeSuccess` is fired. + * + * @param {Object} angularEvent Synthetic event object. + * @param {Route} next Future route information. + * @param {Route} current Current route information. + */ + + /** + * @ngdoc event + * @name $route#$routeChangeSuccess + * @eventType broadcast on root scope + * @description + * Broadcasted after a route dependencies are resolved. + * {@link ngRoute.directive:ngView ngView} listens for the directive + * to instantiate the controller and render the view. + * + * @param {Object} angularEvent Synthetic event object. + * @param {Route} current Current route information. + * @param {Route|Undefined} previous Previous route information, or undefined if current is + * first route entered. + */ + + /** + * @ngdoc event + * @name $route#$routeChangeError + * @eventType broadcast on root scope + * @description + * Broadcasted if any of the resolve promises are rejected. + * + * @param {Object} angularEvent Synthetic event object + * @param {Route} current Current route information. + * @param {Route} previous Previous route information. + * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. + */ + + /** + * @ngdoc event + * @name $route#$routeUpdate + * @eventType broadcast on root scope + * @description + * + * The `reloadOnSearch` property has been set to false, and we are reusing the same + * instance of the Controller. + */ + + var forceReload = false, + $route = { + routes: routes, + + /** + * @ngdoc method + * @name $route#reload + * + * @description + * Causes `$route` service to reload the current route even if + * {@link ng.$location $location} hasn't changed. + * + * As a result of that, {@link ngRoute.directive:ngView ngView} + * creates new scope, reinstantiates the controller. + */ + reload: function() { + forceReload = true; + $rootScope.$evalAsync(updateRoute); + } + }; + + $rootScope.$on('$locationChangeSuccess', updateRoute); + + return $route; + + ///////////////////////////////////////////////////// + + /** + * @param on {string} current url + * @param route {Object} route regexp to match the url against + * @return {?Object} + * + * @description + * Check if the route matches the current url. + * + * Inspired by match in + * visionmedia/express/lib/router/router.js. + */ + function switchRouteMatcher(on, route) { + var keys = route.keys, + params = {}; + + if (!route.regexp) return null; + + var m = route.regexp.exec(on); + if (!m) return null; + + for (var i = 1, len = m.length; i < len; ++i) { + var key = keys[i - 1]; + + var val = 'string' == typeof m[i] + ? decodeURIComponent(m[i]) + : m[i]; + + if (key && val) { + params[key.name] = val; + } + } + return params; + } + + function updateRoute() { + var next = parseRoute(), + last = $route.current; + + if (next && last && next.$$route === last.$$route + && angular.equals(next.pathParams, last.pathParams) + && !next.reloadOnSearch && !forceReload) { + last.params = next.params; + angular.copy(last.params, $routeParams); + $rootScope.$broadcast('$routeUpdate', last); + } else if (next || last) { + forceReload = false; + $rootScope.$broadcast('$routeChangeStart', next, last); + $route.current = next; + if (next) { + if (next.redirectTo) { + if (angular.isString(next.redirectTo)) { + $location.path(interpolate(next.redirectTo, next.params)).search(next.params) + .replace(); + } else { + $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) + .replace(); + } + } + } + + $q.when(next). + then(function() { + if (next) { + var locals = angular.extend({}, next.resolve), + template, templateUrl; + + angular.forEach(locals, function(value, key) { + locals[key] = angular.isString(value) ? + $injector.get(value) : $injector.invoke(value); + }); + + if (angular.isDefined(template = next.template)) { + if (angular.isFunction(template)) { + template = template(next.params); + } + } else if (angular.isDefined(templateUrl = next.templateUrl)) { + if (angular.isFunction(templateUrl)) { + templateUrl = templateUrl(next.params); + } + templateUrl = $sce.getTrustedResourceUrl(templateUrl); + if (angular.isDefined(templateUrl)) { + next.loadedTemplateUrl = templateUrl; + template = $http.get(templateUrl, {cache: $templateCache}). + then(function(response) { return response.data; }); + } + } + if (angular.isDefined(template)) { + locals['$template'] = template; + } + return $q.all(locals); + } + }). + // after route change + then(function(locals) { + if (next == $route.current) { + if (next) { + next.locals = locals; + angular.copy(next.params, $routeParams); + } + $rootScope.$broadcast('$routeChangeSuccess', next, last); + } + }, function(error) { + if (next == $route.current) { + $rootScope.$broadcast('$routeChangeError', next, last, error); + } + }); + } + } + + + /** + * @returns {Object} the current active route, by matching it against the URL + */ + function parseRoute() { + // Match a route + var params, match; + angular.forEach(routes, function(route, path) { + if (!match && (params = switchRouteMatcher($location.path(), route))) { + match = inherit(route, { + params: angular.extend({}, $location.search(), params), + pathParams: params}); + match.$$route = route; + } + }); + // No route matched; fallback to "otherwise" route + return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); + } + + /** + * @returns {string} interpolation of the redirect path with the parameters + */ + function interpolate(string, params) { + var result = []; + angular.forEach((string||'').split(':'), function(segment, i) { + if (i === 0) { + result.push(segment); + } else { + var segmentMatch = segment.match(/(\w+)(.*)/); + var key = segmentMatch[1]; + result.push(params[key]); + result.push(segmentMatch[2] || ''); + delete params[key]; + } + }); + return result.join(''); + } + }]; +} + +ngRouteModule.provider('$routeParams', $RouteParamsProvider); + + +/** + * @ngdoc service + * @name $routeParams + * @requires $route + * + * @description + * The `$routeParams` service allows you to retrieve the current set of route parameters. + * + * Requires the {@link ngRoute `ngRoute`} module to be installed. + * + * The route parameters are a combination of {@link ng.$location `$location`}'s + * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}. + * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. + * + * In case of parameter name collision, `path` params take precedence over `search` params. + * + * The service guarantees that the identity of the `$routeParams` object will remain unchanged + * (but its properties will likely change) even when a route change occurs. + * + * Note that the `$routeParams` are only updated *after* a route change completes successfully. + * This means that you cannot rely on `$routeParams` being correct in route resolve functions. + * Instead you can use `$route.current.params` to access the new route's parameters. + * + * @example + * ```js + * // Given: + * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby + * // Route: /Chapter/:chapterId/Section/:sectionId + * // + * // Then + * $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'} + * ``` + */ +function $RouteParamsProvider() { + this.$get = function() { return {}; }; +} + +ngRouteModule.directive('ngView', ngViewFactory); +ngRouteModule.directive('ngView', ngViewFillContentFactory); + + +/** + * @ngdoc directive + * @name ngView + * @restrict ECA + * + * @description + * # Overview + * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by + * including the rendered template of the current route into the main layout (`index.html`) file. + * Every time the current route changes, the included view changes with it according to the + * configuration of the `$route` service. + * + * Requires the {@link ngRoute `ngRoute`} module to be installed. + * + * @animations + * enter - animation is used to bring new content into the browser. + * leave - animation is used to animate existing content away. + * + * The enter and leave animation occur concurrently. + * + * @scope + * @priority 400 + * @param {string=} onload Expression to evaluate whenever the view updates. + * + * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll + * $anchorScroll} to scroll the viewport after the view is updated. + * + * - If the attribute is not set, disable scrolling. + * - If the attribute is set without value, enable scrolling. + * - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated + * as an expression yields a truthy value. + * @example + + +
+ Choose: + Moby | + Moby: Ch1 | + Gatsby | + Gatsby: Ch4 | + Scarlet Letter
+ +
+
+
+
+ +
$location.path() = {{main.$location.path()}}
+
$route.current.templateUrl = {{main.$route.current.templateUrl}}
+
$route.current.params = {{main.$route.current.params}}
+
$route.current.scope.name = {{main.$route.current.scope.name}}
+
$routeParams = {{main.$routeParams}}
+
+
+ + +
+ controller: {{book.name}}
+ Book Id: {{book.params.bookId}}
+
+
+ + +
+ controller: {{chapter.name}}
+ Book Id: {{chapter.params.bookId}}
+ Chapter Id: {{chapter.params.chapterId}} +
+
+ + + .view-animate-container { + position:relative; + height:100px!important; + position:relative; + background:white; + border:1px solid black; + height:40px; + overflow:hidden; + } + + .view-animate { + padding:10px; + } + + .view-animate.ng-enter, .view-animate.ng-leave { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + + display:block; + width:100%; + border-left:1px solid black; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + padding:10px; + } + + .view-animate.ng-enter { + left:100%; + } + .view-animate.ng-enter.ng-enter-active { + left:0; + } + .view-animate.ng-leave.ng-leave-active { + left:-100%; + } + + + + angular.module('ngViewExample', ['ngRoute', 'ngAnimate']) + .config(['$routeProvider', '$locationProvider', + function($routeProvider, $locationProvider) { + $routeProvider + .when('/Book/:bookId', { + templateUrl: 'book.html', + controller: 'BookCtrl', + controllerAs: 'book' + }) + .when('/Book/:bookId/ch/:chapterId', { + templateUrl: 'chapter.html', + controller: 'ChapterCtrl', + controllerAs: 'chapter' + }); + + // configure html5 to get links working on jsfiddle + $locationProvider.html5Mode(true); + }]) + .controller('MainCtrl', ['$route', '$routeParams', '$location', + function($route, $routeParams, $location) { + this.$route = $route; + this.$location = $location; + this.$routeParams = $routeParams; + }]) + .controller('BookCtrl', ['$routeParams', function($routeParams) { + this.name = "BookCtrl"; + this.params = $routeParams; + }]) + .controller('ChapterCtrl', ['$routeParams', function($routeParams) { + this.name = "ChapterCtrl"; + this.params = $routeParams; + }]); + + + + + it('should load and compile correct template', function() { + element(by.linkText('Moby: Ch1')).click(); + var content = element(by.css('[ng-view]')).getText(); + expect(content).toMatch(/controller\: ChapterCtrl/); + expect(content).toMatch(/Book Id\: Moby/); + expect(content).toMatch(/Chapter Id\: 1/); + + element(by.partialLinkText('Scarlet')).click(); + + content = element(by.css('[ng-view]')).getText(); + expect(content).toMatch(/controller\: BookCtrl/); + expect(content).toMatch(/Book Id\: Scarlet/); + }); + +
+ */ + + +/** + * @ngdoc event + * @name ngView#$viewContentLoaded + * @eventType emit on the current ngView scope + * @description + * Emitted every time the ngView content is reloaded. + */ +ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; +function ngViewFactory( $route, $anchorScroll, $animate) { + return { + restrict: 'ECA', + terminal: true, + priority: 400, + transclude: 'element', + link: function(scope, $element, attr, ctrl, $transclude) { + var currentScope, + currentElement, + previousElement, + autoScrollExp = attr.autoscroll, + onloadExp = attr.onload || ''; + + scope.$on('$routeChangeSuccess', update); + update(); + + function cleanupLastView() { + if(previousElement) { + previousElement.remove(); + previousElement = null; + } + if(currentScope) { + currentScope.$destroy(); + currentScope = null; + } + if(currentElement) { + $animate.leave(currentElement, function() { + previousElement = null; + }); + previousElement = currentElement; + currentElement = null; + } + } + + function update() { + var locals = $route.current && $route.current.locals, + template = locals && locals.$template; + + if (angular.isDefined(template)) { + var newScope = scope.$new(); + var current = $route.current; + + // Note: This will also link all children of ng-view that were contained in the original + // html. If that content contains controllers, ... they could pollute/change the scope. + // However, using ng-view on an element with additional content does not make sense... + // Note: We can't remove them in the cloneAttchFn of $transclude as that + // function is called before linking the content, which would apply child + // directives to non existing elements. + var clone = $transclude(newScope, function(clone) { + $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () { + if (angular.isDefined(autoScrollExp) + && (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); + } + }); + cleanupLastView(); + }); + + currentElement = clone; + currentScope = current.scope = newScope; + currentScope.$emit('$viewContentLoaded'); + currentScope.$eval(onloadExp); + } else { + cleanupLastView(); + } + } + } + }; +} + +// This directive is called during the $transclude call of the first `ngView` directive. +// It will replace and compile the content of the element with the loaded template. +// We need this directive so that the element content is already filled when +// the link function of another directive on the same element as ngView +// is called. +ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; +function ngViewFillContentFactory($compile, $controller, $route) { + return { + restrict: 'ECA', + priority: -400, + link: function(scope, $element) { + var current = $route.current, + locals = current.locals; + + $element.html(locals.$template); + + var link = $compile($element.contents()); + + if (current.controller) { + locals.$scope = scope; + var controller = $controller(current.controller, locals); + if (current.controllerAs) { + scope[current.controllerAs] = controller; + } + $element.data('$ngControllerController', controller); + $element.children().data('$ngControllerController', controller); + } + + link(scope); + } + }; +} + + +})(window, window.angular); diff --git a/trunk/research/console/js/3rdparty/angular-route.min.js b/trunk/research/console/js/3rdparty/angular-route.min.js new file mode 100644 index 000000000..bf7ff1a0e --- /dev/null +++ b/trunk/research/console/js/3rdparty/angular-route.min.js @@ -0,0 +1,14 @@ +/* + AngularJS v1.2.17 + (c) 2010-2014 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(n,e,A){'use strict';function x(s,g,k){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,c,b,f,w){function y(){p&&(p.remove(),p=null);h&&(h.$destroy(),h=null);l&&(k.leave(l,function(){p=null}),p=l,l=null)}function v(){var b=s.current&&s.current.locals;if(e.isDefined(b&&b.$template)){var b=a.$new(),d=s.current;l=w(b,function(d){k.enter(d,null,l||c,function(){!e.isDefined(t)||t&&!a.$eval(t)||g()});y()});h=d.scope=b;h.$emit("$viewContentLoaded");h.$eval(u)}else y()} +var h,l,p,t=b.autoscroll,u=b.onload||"";a.$on("$routeChangeSuccess",v);v()}}}function z(e,g,k){return{restrict:"ECA",priority:-400,link:function(a,c){var b=k.current,f=b.locals;c.html(f.$template);var w=e(c.contents());b.controller&&(f.$scope=a,f=g(b.controller,f),b.controllerAs&&(a[b.controllerAs]=f),c.data("$ngControllerController",f),c.children().data("$ngControllerController",f));w(a)}}}n=e.module("ngRoute",["ng"]).provider("$route",function(){function s(a,c){return e.extend(new (e.extend(function(){}, +{prototype:a})),c)}function g(a,e){var b=e.caseInsensitiveMatch,f={originalPath:a,regexp:a},k=f.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,e,b,c){a="?"===c?c:null;c="*"===c?c:null;k.push({name:b,optional:!!a});e=e||"";return""+(a?"":e)+"(?:"+(a?e:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");f.regexp=RegExp("^"+a+"$",b?"i":"");return f}var k={};this.when=function(a,c){k[a]=e.extend({reloadOnSearch:!0},c,a&&g(a,c));if(a){var b= +"/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";k[b]=e.extend({redirectTo:a},g(b,c))}return this};this.otherwise=function(a){this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$http","$templateCache","$sce",function(a,c,b,f,g,n,v,h){function l(){var d=p(),m=r.current;if(d&&m&&d.$$route===m.$$route&&e.equals(d.pathParams,m.pathParams)&&!d.reloadOnSearch&&!u)m.params=d.params,e.copy(m.params,b),a.$broadcast("$routeUpdate",m);else if(d||m)u=!1,a.$broadcast("$routeChangeStart", +d,m),(r.current=d)&&d.redirectTo&&(e.isString(d.redirectTo)?c.path(t(d.redirectTo,d.params)).search(d.params).replace():c.url(d.redirectTo(d.pathParams,c.path(),c.search())).replace()),f.when(d).then(function(){if(d){var a=e.extend({},d.resolve),c,b;e.forEach(a,function(d,c){a[c]=e.isString(d)?g.get(d):g.invoke(d)});e.isDefined(c=d.template)?e.isFunction(c)&&(c=c(d.params)):e.isDefined(b=d.templateUrl)&&(e.isFunction(b)&&(b=b(d.params)),b=h.getTrustedResourceUrl(b),e.isDefined(b)&&(d.loadedTemplateUrl= +b,c=n.get(b,{cache:v}).then(function(a){return a.data})));e.isDefined(c)&&(a.$template=c);return f.all(a)}}).then(function(c){d==r.current&&(d&&(d.locals=c,e.copy(d.params,b)),a.$broadcast("$routeChangeSuccess",d,m))},function(c){d==r.current&&a.$broadcast("$routeChangeError",d,m,c)})}function p(){var a,b;e.forEach(k,function(f,k){var q;if(q=!b){var g=c.path();q=f.keys;var l={};if(f.regexp)if(g=f.regexp.exec(g)){for(var h=1,p=g.length;h 0 && (length - 1) in obj; +} + +/** + * @ngdoc function + * @name angular.forEach + * @module ng + * @kind function + * + * @description + * Invokes the `iterator` function once for each item in `obj` collection, which can be either an + * object or an array. The `iterator` function is invoked with `iterator(value, key)`, where `value` + * is the value of an object property or an array element and `key` is the object property key or + * array element index. Specifying a `context` for the function is optional. + * + * It is worth noting that `.forEach` does not iterate over inherited properties because it filters + * using the `hasOwnProperty` method. + * + ```js + var values = {name: 'misko', gender: 'male'}; + var log = []; + angular.forEach(values, function(value, key) { + this.push(key + ': ' + value); + }, log); + expect(log).toEqual(['name: misko', 'gender: male']); + ``` + * + * @param {Object|Array} obj Object to iterate over. + * @param {Function} iterator Iterator function. + * @param {Object=} context Object to become context (`this`) for the iterator function. + * @returns {Object|Array} Reference to `obj`. + */ +function forEach(obj, iterator, context) { + var key; + if (obj) { + if (isFunction(obj)) { + for (key in obj) { + // Need to check if hasOwnProperty exists, + // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function + if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { + iterator.call(context, obj[key], key); + } + } + } else if (obj.forEach && obj.forEach !== forEach) { + obj.forEach(iterator, context); + } else if (isArrayLike(obj)) { + for (key = 0; key < obj.length; key++) + iterator.call(context, obj[key], key); + } else { + for (key in obj) { + if (obj.hasOwnProperty(key)) { + iterator.call(context, obj[key], key); + } + } + } + } + return obj; +} + +function sortedKeys(obj) { + var keys = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + keys.push(key); + } + } + return keys.sort(); +} + +function forEachSorted(obj, iterator, context) { + var keys = sortedKeys(obj); + for ( var i = 0; i < keys.length; i++) { + iterator.call(context, obj[keys[i]], keys[i]); + } + return keys; +} + + +/** + * when using forEach the params are value, key, but it is often useful to have key, value. + * @param {function(string, *)} iteratorFn + * @returns {function(*, string)} + */ +function reverseParams(iteratorFn) { + return function(value, key) { iteratorFn(key, value); }; +} + +/** + * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric + * characters such as '012ABC'. The reason why we are not using simply a number counter is that + * the number string gets longer over time, and it can also overflow, where as the nextId + * will grow much slower, it is a string, and it will never overflow. + * + * @returns {string} an unique alpha-numeric string + */ +function nextUid() { + var index = uid.length; + var digit; + + while(index) { + index--; + digit = uid[index].charCodeAt(0); + if (digit == 57 /*'9'*/) { + uid[index] = 'A'; + return uid.join(''); + } + if (digit == 90 /*'Z'*/) { + uid[index] = '0'; + } else { + uid[index] = String.fromCharCode(digit + 1); + return uid.join(''); + } + } + uid.unshift('0'); + return uid.join(''); +} + + +/** + * Set or clear the hashkey for an object. + * @param obj object + * @param h the hashkey (!truthy to delete the hashkey) + */ +function setHashKey(obj, h) { + if (h) { + obj.$$hashKey = h; + } + else { + delete obj.$$hashKey; + } +} + +/** + * @ngdoc function + * @name angular.extend + * @module ng + * @kind function + * + * @description + * Extends the destination object `dst` by copying all of the properties from the `src` object(s) + * to `dst`. You can specify multiple `src` objects. + * + * @param {Object} dst Destination object. + * @param {...Object} src Source object(s). + * @returns {Object} Reference to `dst`. + */ +function extend(dst) { + var h = dst.$$hashKey; + forEach(arguments, function(obj) { + if (obj !== dst) { + forEach(obj, function(value, key) { + dst[key] = value; + }); + } + }); + + setHashKey(dst,h); + return dst; +} + +function int(str) { + return parseInt(str, 10); +} + + +function inherit(parent, extra) { + return extend(new (extend(function() {}, {prototype:parent}))(), extra); +} + +/** + * @ngdoc function + * @name angular.noop + * @module ng + * @kind function + * + * @description + * A function that performs no operations. This function can be useful when writing code in the + * functional style. + ```js + function foo(callback) { + var result = calculateResult(); + (callback || angular.noop)(result); + } + ``` + */ +function noop() {} +noop.$inject = []; + + +/** + * @ngdoc function + * @name angular.identity + * @module ng + * @kind function + * + * @description + * A function that returns its first argument. This function is useful when writing code in the + * functional style. + * + ```js + function transformer(transformationFn, value) { + return (transformationFn || angular.identity)(value); + }; + ``` + */ +function identity($) {return $;} +identity.$inject = []; + + +function valueFn(value) {return function() {return value;};} + +/** + * @ngdoc function + * @name angular.isUndefined + * @module ng + * @kind function + * + * @description + * Determines if a reference is undefined. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is undefined. + */ +function isUndefined(value){return typeof value === 'undefined';} + + +/** + * @ngdoc function + * @name angular.isDefined + * @module ng + * @kind function + * + * @description + * Determines if a reference is defined. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is defined. + */ +function isDefined(value){return typeof value !== 'undefined';} + + +/** + * @ngdoc function + * @name angular.isObject + * @module ng + * @kind function + * + * @description + * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not + * considered to be objects. Note that JavaScript arrays are objects. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Object` but not `null`. + */ +function isObject(value){return value != null && typeof value === 'object';} + + +/** + * @ngdoc function + * @name angular.isString + * @module ng + * @kind function + * + * @description + * Determines if a reference is a `String`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `String`. + */ +function isString(value){return typeof value === 'string';} + + +/** + * @ngdoc function + * @name angular.isNumber + * @module ng + * @kind function + * + * @description + * Determines if a reference is a `Number`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Number`. + */ +function isNumber(value){return typeof value === 'number';} + + +/** + * @ngdoc function + * @name angular.isDate + * @module ng + * @kind function + * + * @description + * Determines if a value is a date. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Date`. + */ +function isDate(value) { + return toString.call(value) === '[object Date]'; +} + + +/** + * @ngdoc function + * @name angular.isArray + * @module ng + * @kind function + * + * @description + * Determines if a reference is an `Array`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Array`. + */ +function isArray(value) { + return toString.call(value) === '[object Array]'; +} + + +/** + * @ngdoc function + * @name angular.isFunction + * @module ng + * @kind function + * + * @description + * Determines if a reference is a `Function`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Function`. + */ +function isFunction(value){return typeof value === 'function';} + + +/** + * Determines if a value is a regular expression object. + * + * @private + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `RegExp`. + */ +function isRegExp(value) { + return toString.call(value) === '[object RegExp]'; +} + + +/** + * Checks if `obj` is a window object. + * + * @private + * @param {*} obj Object to check + * @returns {boolean} True if `obj` is a window obj. + */ +function isWindow(obj) { + return obj && obj.document && obj.location && obj.alert && obj.setInterval; +} + + +function isScope(obj) { + return obj && obj.$evalAsync && obj.$watch; +} + + +function isFile(obj) { + return toString.call(obj) === '[object File]'; +} + + +function isBlob(obj) { + return toString.call(obj) === '[object Blob]'; +} + + +function isBoolean(value) { + return typeof value === 'boolean'; +} + + +var trim = (function() { + // native trim is way faster: http://jsperf.com/angular-trim-test + // but IE doesn't have it... :-( + // TODO: we should move this into IE/ES5 polyfill + if (!String.prototype.trim) { + return function(value) { + return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value; + }; + } + return function(value) { + return isString(value) ? value.trim() : value; + }; +})(); + + +/** + * @ngdoc function + * @name angular.isElement + * @module ng + * @kind function + * + * @description + * Determines if a reference is a DOM element (or wrapped jQuery element). + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element). + */ +function isElement(node) { + return !!(node && + (node.nodeName // we are a direct element + || (node.prop && node.attr && node.find))); // we have an on and find method part of jQuery API +} + +/** + * @param str 'key1,key2,...' + * @returns {object} in the form of {key1:true, key2:true, ...} + */ +function makeMap(str) { + var obj = {}, items = str.split(","), i; + for ( i = 0; i < items.length; i++ ) + obj[ items[i] ] = true; + return obj; +} + + +if (msie < 9) { + nodeName_ = function(element) { + element = element.nodeName ? element : element[0]; + return (element.scopeName && element.scopeName != 'HTML') + ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; + }; +} else { + nodeName_ = function(element) { + return element.nodeName ? element.nodeName : element[0].nodeName; + }; +} + + +function map(obj, iterator, context) { + var results = []; + forEach(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; +} + + +/** + * @description + * Determines the number of elements in an array, the number of properties an object has, or + * the length of a string. + * + * Note: This function is used to augment the Object type in Angular expressions. See + * {@link angular.Object} for more information about Angular arrays. + * + * @param {Object|Array|string} obj Object, array, or string to inspect. + * @param {boolean} [ownPropsOnly=false] Count only "own" properties in an object + * @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array. + */ +function size(obj, ownPropsOnly) { + var count = 0, key; + + if (isArray(obj) || isString(obj)) { + return obj.length; + } else if (isObject(obj)) { + for (key in obj) + if (!ownPropsOnly || obj.hasOwnProperty(key)) + count++; + } + + return count; +} + + +function includes(array, obj) { + return indexOf(array, obj) != -1; +} + +function indexOf(array, obj) { + if (array.indexOf) return array.indexOf(obj); + + for (var i = 0; i < array.length; i++) { + if (obj === array[i]) return i; + } + return -1; +} + +function arrayRemove(array, value) { + var index = indexOf(array, value); + if (index >=0) + array.splice(index, 1); + return value; +} + +function isLeafNode (node) { + if (node) { + switch (node.nodeName) { + case "OPTION": + case "PRE": + case "TITLE": + return true; + } + } + return false; +} + +/** + * @ngdoc function + * @name angular.copy + * @module ng + * @kind function + * + * @description + * Creates a deep copy of `source`, which should be an object or an array. + * + * * If no destination is supplied, a copy of the object or array is created. + * * If a destination is provided, all of its elements (for array) or properties (for objects) + * are deleted and then all elements/properties from the source are copied to it. + * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. + * * If `source` is identical to 'destination' an exception will be thrown. + * + * @param {*} source The source that will be used to make a copy. + * Can be any type, including primitives, `null`, and `undefined`. + * @param {(Object|Array)=} destination Destination into which the source is copied. If + * provided, must be of the same type as `source`. + * @returns {*} The copy or updated `destination`, if `destination` was specified. + * + * @example + + +
+
+ Name:
+ E-mail:
+ Gender: male + female
+ + +
+
form = {{user | json}}
+
master = {{master | json}}
+
+ + +
+
+ */ +function copy(source, destination, stackSource, stackDest) { + if (isWindow(source) || isScope(source)) { + throw ngMinErr('cpws', + "Can't copy! Making copies of Window or Scope instances is not supported."); + } + + if (!destination) { + destination = source; + if (source) { + if (isArray(source)) { + destination = copy(source, [], stackSource, stackDest); + } else if (isDate(source)) { + destination = new Date(source.getTime()); + } else if (isRegExp(source)) { + destination = new RegExp(source.source); + } else if (isObject(source)) { + destination = copy(source, {}, stackSource, stackDest); + } + } + } else { + if (source === destination) throw ngMinErr('cpi', + "Can't copy! Source and destination are identical."); + + stackSource = stackSource || []; + stackDest = stackDest || []; + + if (isObject(source)) { + var index = indexOf(stackSource, source); + if (index !== -1) return stackDest[index]; + + stackSource.push(source); + stackDest.push(destination); + } + + var result; + if (isArray(source)) { + destination.length = 0; + for ( var i = 0; i < source.length; i++) { + result = copy(source[i], null, stackSource, stackDest); + if (isObject(source[i])) { + stackSource.push(source[i]); + stackDest.push(result); + } + destination.push(result); + } + } else { + var h = destination.$$hashKey; + forEach(destination, function(value, key) { + delete destination[key]; + }); + for ( var key in source) { + result = copy(source[key], null, stackSource, stackDest); + if (isObject(source[key])) { + stackSource.push(source[key]); + stackDest.push(result); + } + destination[key] = result; + } + setHashKey(destination,h); + } + + } + return destination; +} + +/** + * Creates a shallow copy of an object, an array or a primitive + */ +function shallowCopy(src, dst) { + if (isArray(src)) { + dst = dst || []; + + for ( var i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + } else if (isObject(src)) { + dst = dst || {}; + + for (var key in src) { + if (hasOwnProperty.call(src, key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + } + + return dst || src; +} + + +/** + * @ngdoc function + * @name angular.equals + * @module ng + * @kind function + * + * @description + * Determines if two objects or two values are equivalent. Supports value types, regular + * expressions, arrays and objects. + * + * Two objects or values are considered equivalent if at least one of the following is true: + * + * * Both objects or values pass `===` comparison. + * * Both objects or values are of the same type and all of their properties are equal by + * comparing them with `angular.equals`. + * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) + * * Both values represent the same regular expression (In JavaScript, + * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual + * representation matches). + * + * During a property comparison, properties of `function` type and properties with names + * that begin with `$` are ignored. + * + * Scope and DOMWindow objects are being compared only by identify (`===`). + * + * @param {*} o1 Object or value to compare. + * @param {*} o2 Object or value to compare. + * @returns {boolean} True if arguments are equal. + */ +function equals(o1, o2) { + if (o1 === o2) return true; + if (o1 === null || o2 === null) return false; + if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN + var t1 = typeof o1, t2 = typeof o2, length, key, keySet; + if (t1 == t2) { + if (t1 == 'object') { + if (isArray(o1)) { + if (!isArray(o2)) return false; + if ((length = o1.length) == o2.length) { + for(key=0; key 2 ? sliceArgs(arguments, 2) : []; + if (isFunction(fn) && !(fn instanceof RegExp)) { + return curryArgs.length + ? function() { + return arguments.length + ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0))) + : fn.apply(self, curryArgs); + } + : function() { + return arguments.length + ? fn.apply(self, arguments) + : fn.call(self); + }; + } else { + // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) + return fn; + } +} + + +function toJsonReplacer(key, value) { + var val = value; + + if (typeof key === 'string' && key.charAt(0) === '$') { + val = undefined; + } else if (isWindow(value)) { + val = '$WINDOW'; + } else if (value && document === value) { + val = '$DOCUMENT'; + } else if (isScope(value)) { + val = '$SCOPE'; + } + + return val; +} + + +/** + * @ngdoc function + * @name angular.toJson + * @module ng + * @kind function + * + * @description + * Serializes input into a JSON-formatted string. Properties with leading $ characters will be + * stripped since angular uses this notation internally. + * + * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. + * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. + * @returns {string|undefined} JSON-ified string representing `obj`. + */ +function toJson(obj, pretty) { + if (typeof obj === 'undefined') return undefined; + return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); +} + + +/** + * @ngdoc function + * @name angular.fromJson + * @module ng + * @kind function + * + * @description + * Deserializes a JSON string. + * + * @param {string} json JSON string to deserialize. + * @returns {Object|Array|string|number} Deserialized thingy. + */ +function fromJson(json) { + return isString(json) + ? JSON.parse(json) + : json; +} + + +function toBoolean(value) { + if (typeof value === 'function') { + value = true; + } else if (value && value.length !== 0) { + var v = lowercase("" + value); + value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); + } else { + value = false; + } + return value; +} + +/** + * @returns {string} Returns the string representation of the element. + */ +function startingTag(element) { + element = jqLite(element).clone(); + try { + // turns out IE does not let you set .html() on elements which + // are not allowed to have children. So we just ignore it. + element.empty(); + } catch(e) {} + // As Per DOM Standards + var TEXT_NODE = 3; + var elemHtml = jqLite('
').append(element).html(); + try { + return element[0].nodeType === TEXT_NODE ? lowercase(elemHtml) : + elemHtml. + match(/^(<[^>]+>)/)[1]. + replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); + } catch(e) { + return lowercase(elemHtml); + } + +} + + +///////////////////////////////////////////////// + +/** + * Tries to decode the URI component without throwing an exception. + * + * @private + * @param str value potential URI component to check. + * @returns {boolean} True if `value` can be decoded + * with the decodeURIComponent function. + */ +function tryDecodeURIComponent(value) { + try { + return decodeURIComponent(value); + } catch(e) { + // Ignore any invalid uri component + } +} + + +/** + * Parses an escaped url query string into key-value pairs. + * @returns {Object.} + */ +function parseKeyValue(/**string*/keyValue) { + var obj = {}, key_value, key; + forEach((keyValue || "").split('&'), function(keyValue) { + if ( keyValue ) { + key_value = keyValue.split('='); + key = tryDecodeURIComponent(key_value[0]); + if ( isDefined(key) ) { + var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; + if (!obj[key]) { + obj[key] = val; + } else if(isArray(obj[key])) { + obj[key].push(val); + } else { + obj[key] = [obj[key],val]; + } + } + } + }); + return obj; +} + +function toKeyValue(obj) { + var parts = []; + forEach(obj, function(value, key) { + if (isArray(value)) { + forEach(value, function(arrayValue) { + parts.push(encodeUriQuery(key, true) + + (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); + }); + } else { + parts.push(encodeUriQuery(key, true) + + (value === true ? '' : '=' + encodeUriQuery(value, true))); + } + }); + return parts.length ? parts.join('&') : ''; +} + + +/** + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow + * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path + * segments: + * segment = *pchar + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * pct-encoded = "%" HEXDIG HEXDIG + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ +function encodeUriSegment(val) { + return encodeUriQuery(val, true). + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); +} + + +/** + * This method is intended for encoding *key* or *value* parts of query component. We need a custom + * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be + * encoded per http://tools.ietf.org/html/rfc3986: + * query = *( pchar / "/" / "?" ) + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ +function encodeUriQuery(val, pctEncodeSpaces) { + return encodeURIComponent(val). + replace(/%40/gi, '@'). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); +} + + +/** + * @ngdoc directive + * @name ngApp + * @module ng + * + * @element ANY + * @param {angular.Module} ngApp an optional application + * {@link angular.module module} name to load. + * + * @description + * + * Use this directive to **auto-bootstrap** an AngularJS application. The `ngApp` directive + * designates the **root element** of the application and is typically placed near the root element + * of the page - e.g. on the `` or `` tags. + * + * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` + * found in the document will be used to define the root element to auto-bootstrap as an + * application. To run multiple applications in an HTML document you must manually bootstrap them using + * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. + * + * You can specify an **AngularJS module** to be used as the root module for the application. This + * module will be loaded into the {@link auto.$injector} when the application is bootstrapped and + * should contain the application code needed or have dependencies on other modules that will + * contain the code. See {@link angular.module} for more information. + * + * In the example below if the `ngApp` directive were not placed on the `html` element then the + * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` + * would not be resolved to `3`. + * + * `ngApp` is the easiest, and most common, way to bootstrap an application. + * + + +
+ I can add: {{a}} + {{b}} = {{ a+b }} +
+
+ + angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { + $scope.a = 1; + $scope.b = 2; + }); + +
+ * + */ +function angularInit(element, bootstrap) { + var elements = [element], + appElement, + module, + names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], + NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; + + function append(element) { + element && elements.push(element); + } + + forEach(names, function(name) { + names[name] = true; + append(document.getElementById(name)); + name = name.replace(':', '\\:'); + if (element.querySelectorAll) { + forEach(element.querySelectorAll('.' + name), append); + forEach(element.querySelectorAll('.' + name + '\\:'), append); + forEach(element.querySelectorAll('[' + name + ']'), append); + } + }); + + forEach(elements, function(element) { + if (!appElement) { + var className = ' ' + element.className + ' '; + var match = NG_APP_CLASS_REGEXP.exec(className); + if (match) { + appElement = element; + module = (match[2] || '').replace(/\s+/g, ','); + } else { + forEach(element.attributes, function(attr) { + if (!appElement && names[attr.name]) { + appElement = element; + module = attr.value; + } + }); + } + } + }); + if (appElement) { + bootstrap(appElement, module ? [module] : []); + } +} + +/** + * @ngdoc function + * @name angular.bootstrap + * @module ng + * @description + * Use this function to manually start up angular application. + * + * See: {@link guide/bootstrap Bootstrap} + * + * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. + * They must use {@link ng.directive:ngApp ngApp}. + * + * Angular will detect if it has been loaded into the browser more than once and only allow the + * first loaded script to be bootstrapped and will report a warning to the browser console for + * each of the subsequent scripts. This prevents strange results in applications, where otherwise + * multiple instances of Angular try to work on the DOM. + * + * + * + * + *
+ * + * + * + * + * + * + * + *
{{heading}}
{{fill}}
+ *
+ *
+ * + * var app = angular.module('multi-bootstrap', []) + * + * .controller('BrokenTable', function($scope) { + * $scope.headings = ['One', 'Two', 'Three']; + * $scope.fillings = [[1, 2, 3], ['A', 'B', 'C'], [7, 8, 9]]; + * }); + * + * + * it('should only insert one table cell for each item in $scope.fillings', function() { + * expect(element.all(by.css('td')).count()) + * .toBe(9); + * }); + * + *
+ * + * @param {DOMElement} element DOM element which is the root of angular application. + * @param {Array=} modules an array of modules to load into the application. + * Each item in the array should be the name of a predefined module or a (DI annotated) + * function that will be invoked by the injector as a run block. + * See: {@link angular.module modules} + * @returns {auto.$injector} Returns the newly created injector for this app. + */ +function bootstrap(element, modules) { + var doBootstrap = function() { + element = jqLite(element); + + if (element.injector()) { + var tag = (element[0] === document) ? 'document' : startingTag(element); + throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); + } + + modules = modules || []; + modules.unshift(['$provide', function($provide) { + $provide.value('$rootElement', element); + }]); + modules.unshift('ng'); + var injector = createInjector(modules); + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', + function(scope, element, compile, injector, animate) { + scope.$apply(function() { + element.data('$injector', injector); + compile(element)(scope); + }); + }] + ); + return injector; + }; + + var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; + + if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { + return doBootstrap(); + } + + window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); + angular.resumeBootstrap = function(extraModules) { + forEach(extraModules, function(module) { + modules.push(module); + }); + doBootstrap(); + }; +} + +var SNAKE_CASE_REGEXP = /[A-Z]/g; +function snake_case(name, separator) { + separator = separator || '_'; + return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); +} + +function bindJQuery() { + // bind to jQuery if present; + jQuery = window.jQuery; + // Use jQuery if it exists with proper functionality, otherwise default to us. + // Angular 1.2+ requires jQuery 1.7.1+ for on()/off() support. + if (jQuery && jQuery.fn.on) { + jqLite = jQuery; + extend(jQuery.fn, { + scope: JQLitePrototype.scope, + isolateScope: JQLitePrototype.isolateScope, + controller: JQLitePrototype.controller, + injector: JQLitePrototype.injector, + inheritedData: JQLitePrototype.inheritedData + }); + // Method signature: + // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) + jqLitePatchJQueryRemove('remove', true, true, false); + jqLitePatchJQueryRemove('empty', false, false, false); + jqLitePatchJQueryRemove('html', false, false, true); + } else { + jqLite = JQLite; + } + angular.element = jqLite; +} + +/** + * throw error if the argument is falsy. + */ +function assertArg(arg, name, reason) { + if (!arg) { + throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + } + return arg; +} + +function assertArgFn(arg, name, acceptArrayAnnotation) { + if (acceptArrayAnnotation && isArray(arg)) { + arg = arg[arg.length - 1]; + } + + assertArg(isFunction(arg), name, 'not a function, got ' + + (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); + return arg; +} + +/** + * throw error if the name given is hasOwnProperty + * @param {String} name the name to test + * @param {String} context the context in which the name is used, such as module or directive + */ +function assertNotHasOwnProperty(name, context) { + if (name === 'hasOwnProperty') { + throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); + } +} + +/** + * Return the value accessible from the object by path. Any undefined traversals are ignored + * @param {Object} obj starting object + * @param {String} path path to traverse + * @param {boolean} [bindFnToScope=true] + * @returns {Object} value as accessible by path + */ +//TODO(misko): this function needs to be removed +function getter(obj, path, bindFnToScope) { + if (!path) return obj; + var keys = path.split('.'); + var key; + var lastInstance = obj; + var len = keys.length; + + for (var i = 0; i < len; i++) { + key = keys[i]; + if (obj) { + obj = (lastInstance = obj)[key]; + } + } + if (!bindFnToScope && isFunction(obj)) { + return bind(lastInstance, obj); + } + return obj; +} + +/** + * Return the DOM siblings between the first and last node in the given array. + * @param {Array} array like object + * @returns {DOMElement} object containing the elements + */ +function getBlockElements(nodes) { + var startNode = nodes[0], + endNode = nodes[nodes.length - 1]; + if (startNode === endNode) { + return jqLite(startNode); + } + + var element = startNode; + var elements = [element]; + + do { + element = element.nextSibling; + if (!element) break; + elements.push(element); + } while (element !== endNode); + + return jqLite(elements); +} + +/** + * @ngdoc type + * @name angular.Module + * @module ng + * @description + * + * Interface for configuring angular {@link angular.module modules}. + */ + +function setupModuleLoader(window) { + + var $injectorMinErr = minErr('$injector'); + var ngMinErr = minErr('ng'); + + function ensure(obj, name, factory) { + return obj[name] || (obj[name] = factory()); + } + + var angular = ensure(window, 'angular', Object); + + // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap + angular.$$minErr = angular.$$minErr || minErr; + + return ensure(angular, 'module', function() { + /** @type {Object.} */ + var modules = {}; + + /** + * @ngdoc function + * @name angular.module + * @module ng + * @description + * + * The `angular.module` is a global place for creating, registering and retrieving Angular + * modules. + * All modules (angular core or 3rd party) that should be available to an application must be + * registered using this mechanism. + * + * When passed two or more arguments, a new module is created. If passed only one argument, an + * existing module (the name passed as the first argument to `module`) is retrieved. + * + * + * # Module + * + * A module is a collection of services, directives, filters, and configuration information. + * `angular.module` is used to configure the {@link auto.$injector $injector}. + * + * ```js + * // Create a new module + * var myModule = angular.module('myModule', []); + * + * // register a new service + * myModule.value('appName', 'MyCoolApp'); + * + * // configure existing services inside initialization blocks. + * myModule.config(['$locationProvider', function($locationProvider) { + * // Configure existing providers + * $locationProvider.hashPrefix('!'); + * }]); + * ``` + * + * Then you can create an injector and load your modules like this: + * + * ```js + * var injector = angular.injector(['ng', 'myModule']) + * ``` + * + * However it's more likely that you'll just use + * {@link ng.directive:ngApp ngApp} or + * {@link angular.bootstrap} to simplify this process for you. + * + * @param {!string} name The name of the module to create or retrieve. +<<<<<* @param {!Array.=} requires If specified then new module is being created. If +>>>>>* unspecified then the module is being retrieved for further configuration. + * @param {Function} configFn Optional configuration function for the module. Same as + * {@link angular.Module#config Module#config()}. + * @returns {module} new module with the {@link angular.Module} api. + */ + return function module(name, requires, configFn) { + var assertNotHasOwnProperty = function(name, context) { + if (name === 'hasOwnProperty') { + throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); + } + }; + + assertNotHasOwnProperty(name, 'module'); + if (requires && modules.hasOwnProperty(name)) { + modules[name] = null; + } + return ensure(modules, name, function() { + if (!requires) { + throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + + "the module name or forgot to load it. If registering a module ensure that you " + + "specify the dependencies as the second argument.", name); + } + + /** @type {!Array.>} */ + var invokeQueue = []; + + /** @type {!Array.} */ + var runBlocks = []; + + var config = invokeLater('$injector', 'invoke'); + + /** @type {angular.Module} */ + var moduleInstance = { + // Private state + _invokeQueue: invokeQueue, + _runBlocks: runBlocks, + + /** + * @ngdoc property + * @name angular.Module#requires + * @module ng + * @returns {Array.} List of module names which must be loaded before this module. + * @description + * Holds the list of modules which the injector will load before the current module is + * loaded. + */ + requires: requires, + + /** + * @ngdoc property + * @name angular.Module#name + * @module ng + * @returns {string} Name of the module. + * @description + */ + name: name, + + + /** + * @ngdoc method + * @name angular.Module#provider + * @module ng + * @param {string} name service name + * @param {Function} providerType Construction function for creating new instance of the + * service. + * @description + * See {@link auto.$provide#provider $provide.provider()}. + */ + provider: invokeLater('$provide', 'provider'), + + /** + * @ngdoc method + * @name angular.Module#factory + * @module ng + * @param {string} name service name + * @param {Function} providerFunction Function for creating new instance of the service. + * @description + * See {@link auto.$provide#factory $provide.factory()}. + */ + factory: invokeLater('$provide', 'factory'), + + /** + * @ngdoc method + * @name angular.Module#service + * @module ng + * @param {string} name service name + * @param {Function} constructor A constructor function that will be instantiated. + * @description + * See {@link auto.$provide#service $provide.service()}. + */ + service: invokeLater('$provide', 'service'), + + /** + * @ngdoc method + * @name angular.Module#value + * @module ng + * @param {string} name service name + * @param {*} object Service instance object. + * @description + * See {@link auto.$provide#value $provide.value()}. + */ + value: invokeLater('$provide', 'value'), + + /** + * @ngdoc method + * @name angular.Module#constant + * @module ng + * @param {string} name constant name + * @param {*} object Constant value. + * @description + * Because the constant are fixed, they get applied before other provide methods. + * See {@link auto.$provide#constant $provide.constant()}. + */ + constant: invokeLater('$provide', 'constant', 'unshift'), + + /** + * @ngdoc method + * @name angular.Module#animation + * @module ng + * @param {string} name animation name + * @param {Function} animationFactory Factory function for creating new instance of an + * animation. + * @description + * + * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. + * + * + * Defines an animation hook that can be later used with + * {@link ngAnimate.$animate $animate} service and directives that use this service. + * + * ```js + * module.animation('.animation-name', function($inject1, $inject2) { + * return { + * eventName : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction(element) { + * //code to cancel the animation + * } + * } + * } + * }) + * ``` + * + * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and + * {@link ngAnimate ngAnimate module} for more information. + */ + animation: invokeLater('$animateProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#filter + * @module ng + * @param {string} name Filter name. + * @param {Function} filterFactory Factory function for creating new instance of filter. + * @description + * See {@link ng.$filterProvider#register $filterProvider.register()}. + */ + filter: invokeLater('$filterProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#controller + * @module ng + * @param {string|Object} name Controller name, or an object map of controllers where the + * keys are the names and the values are the constructors. + * @param {Function} constructor Controller constructor function. + * @description + * See {@link ng.$controllerProvider#register $controllerProvider.register()}. + */ + controller: invokeLater('$controllerProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#directive + * @module ng + * @param {string|Object} name Directive name, or an object map of directives where the + * keys are the names and the values are the factories. + * @param {Function} directiveFactory Factory function for creating new instance of + * directives. + * @description + * See {@link ng.$compileProvider#directive $compileProvider.directive()}. + */ + directive: invokeLater('$compileProvider', 'directive'), + + /** + * @ngdoc method + * @name angular.Module#config + * @module ng + * @param {Function} configFn Execute this function on module load. Useful for service + * configuration. + * @description + * Use this method to register work which needs to be performed on module loading. + * For more about how to configure services, see + * {@link providers#providers_provider-recipe Provider Recipe}. + */ + config: config, + + /** + * @ngdoc method + * @name angular.Module#run + * @module ng + * @param {Function} initializationFn Execute this function after injector creation. + * Useful for application initialization. + * @description + * Use this method to register work which should be performed when the injector is done + * loading all modules. + */ + run: function(block) { + runBlocks.push(block); + return this; + } + }; + + if (configFn) { + config(configFn); + } + + return moduleInstance; + + /** + * @param {string} provider + * @param {string} method + * @param {String=} insertMethod + * @returns {angular.Module} + */ + function invokeLater(provider, method, insertMethod) { + return function() { + invokeQueue[insertMethod || 'push']([provider, method, arguments]); + return moduleInstance; + }; + } + }); + }; + }); + +} + +/* global + angularModule: true, + version: true, + + $LocaleProvider, + $CompileProvider, + + htmlAnchorDirective, + inputDirective, + inputDirective, + formDirective, + scriptDirective, + selectDirective, + styleDirective, + optionDirective, + ngBindDirective, + ngBindHtmlDirective, + ngBindTemplateDirective, + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective, + ngCspDirective, + ngCloakDirective, + ngControllerDirective, + ngFormDirective, + ngHideDirective, + ngIfDirective, + ngIncludeDirective, + ngIncludeFillContentDirective, + ngInitDirective, + ngNonBindableDirective, + ngPluralizeDirective, + ngRepeatDirective, + ngShowDirective, + ngStyleDirective, + ngSwitchDirective, + ngSwitchWhenDirective, + ngSwitchDefaultDirective, + ngOptionsDirective, + ngTranscludeDirective, + ngModelDirective, + ngListDirective, + ngChangeDirective, + requiredDirective, + requiredDirective, + ngValueDirective, + ngAttributeAliasDirectives, + ngEventDirectives, + + $AnchorScrollProvider, + $AnimateProvider, + $BrowserProvider, + $CacheFactoryProvider, + $ControllerProvider, + $DocumentProvider, + $ExceptionHandlerProvider, + $FilterProvider, + $InterpolateProvider, + $IntervalProvider, + $HttpProvider, + $HttpBackendProvider, + $LocationProvider, + $LogProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TimeoutProvider, + $$RAFProvider, + $$AsyncCallbackProvider, + $WindowProvider +*/ + + +/** + * @ngdoc object + * @name angular.version + * @module ng + * @description + * An object that contains information about the current AngularJS version. This object has the + * following properties: + * + * - `full` – `{string}` – Full version string, such as "0.9.18". + * - `major` – `{number}` – Major version number, such as "0". + * - `minor` – `{number}` – Minor version number, such as "9". + * - `dot` – `{number}` – Dot version number, such as "18". + * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". + */ +var version = { + full: '1.2.17', // all of these placeholder strings will be replaced by grunt's + major: 1, // package task + minor: 2, + dot: 17, + codeName: 'quantum-disentanglement' +}; + + +function publishExternalAPI(angular){ + extend(angular, { + 'bootstrap': bootstrap, + 'copy': copy, + 'extend': extend, + 'equals': equals, + 'element': jqLite, + 'forEach': forEach, + 'injector': createInjector, + 'noop':noop, + 'bind':bind, + 'toJson': toJson, + 'fromJson': fromJson, + 'identity':identity, + 'isUndefined': isUndefined, + 'isDefined': isDefined, + 'isString': isString, + 'isFunction': isFunction, + 'isObject': isObject, + 'isNumber': isNumber, + 'isElement': isElement, + 'isArray': isArray, + 'version': version, + 'isDate': isDate, + 'lowercase': lowercase, + 'uppercase': uppercase, + 'callbacks': {counter: 0}, + '$$minErr': minErr, + '$$csp': csp + }); + + angularModule = setupModuleLoader(window); + try { + angularModule('ngLocale'); + } catch (e) { + angularModule('ngLocale', []).provider('$locale', $LocaleProvider); + } + + angularModule('ng', ['ngLocale'], ['$provide', + function ngModule($provide) { + // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it. + $provide.provider({ + $$sanitizeUri: $$SanitizeUriProvider + }); + $provide.provider('$compile', $CompileProvider). + directive({ + a: htmlAnchorDirective, + input: inputDirective, + textarea: inputDirective, + form: formDirective, + script: scriptDirective, + select: selectDirective, + style: styleDirective, + option: optionDirective, + ngBind: ngBindDirective, + ngBindHtml: ngBindHtmlDirective, + ngBindTemplate: ngBindTemplateDirective, + ngClass: ngClassDirective, + ngClassEven: ngClassEvenDirective, + ngClassOdd: ngClassOddDirective, + ngCloak: ngCloakDirective, + ngController: ngControllerDirective, + ngForm: ngFormDirective, + ngHide: ngHideDirective, + ngIf: ngIfDirective, + ngInclude: ngIncludeDirective, + ngInit: ngInitDirective, + ngNonBindable: ngNonBindableDirective, + ngPluralize: ngPluralizeDirective, + ngRepeat: ngRepeatDirective, + ngShow: ngShowDirective, + ngStyle: ngStyleDirective, + ngSwitch: ngSwitchDirective, + ngSwitchWhen: ngSwitchWhenDirective, + ngSwitchDefault: ngSwitchDefaultDirective, + ngOptions: ngOptionsDirective, + ngTransclude: ngTranscludeDirective, + ngModel: ngModelDirective, + ngList: ngListDirective, + ngChange: ngChangeDirective, + required: requiredDirective, + ngRequired: requiredDirective, + ngValue: ngValueDirective + }). + directive({ + ngInclude: ngIncludeFillContentDirective + }). + directive(ngAttributeAliasDirectives). + directive(ngEventDirectives); + $provide.provider({ + $anchorScroll: $AnchorScrollProvider, + $animate: $AnimateProvider, + $browser: $BrowserProvider, + $cacheFactory: $CacheFactoryProvider, + $controller: $ControllerProvider, + $document: $DocumentProvider, + $exceptionHandler: $ExceptionHandlerProvider, + $filter: $FilterProvider, + $interpolate: $InterpolateProvider, + $interval: $IntervalProvider, + $http: $HttpProvider, + $httpBackend: $HttpBackendProvider, + $location: $LocationProvider, + $log: $LogProvider, + $parse: $ParseProvider, + $rootScope: $RootScopeProvider, + $q: $QProvider, + $sce: $SceProvider, + $sceDelegate: $SceDelegateProvider, + $sniffer: $SnifferProvider, + $templateCache: $TemplateCacheProvider, + $timeout: $TimeoutProvider, + $window: $WindowProvider, + $$rAF: $$RAFProvider, + $$asyncCallback : $$AsyncCallbackProvider + }); + } + ]); +} + +/* global + + -JQLitePrototype, + -addEventListenerFn, + -removeEventListenerFn, + -BOOLEAN_ATTR +*/ + +////////////////////////////////// +//JQLite +////////////////////////////////// + +/** + * @ngdoc function + * @name angular.element + * @module ng + * @kind function + * + * @description + * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. + * + * If jQuery is available, `angular.element` is an alias for the + * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` + * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." + * + *
jqLite is a tiny, API-compatible subset of jQuery that allows + * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most + * commonly needed functionality with the goal of having a very small footprint.
+ * + * To use jQuery, simply load it before `DOMContentLoaded` event fired. + * + *
**Note:** all element references in Angular are always wrapped with jQuery or + * jqLite; they are never raw DOM references.
+ * + * ## Angular's jqLite + * jqLite provides only the following jQuery methods: + * + * - [`addClass()`](http://api.jquery.com/addClass/) + * - [`after()`](http://api.jquery.com/after/) + * - [`append()`](http://api.jquery.com/append/) + * - [`attr()`](http://api.jquery.com/attr/) + * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData + * - [`children()`](http://api.jquery.com/children/) - Does not support selectors + * - [`clone()`](http://api.jquery.com/clone/) + * - [`contents()`](http://api.jquery.com/contents/) + * - [`css()`](http://api.jquery.com/css/) + * - [`data()`](http://api.jquery.com/data/) + * - [`empty()`](http://api.jquery.com/empty/) + * - [`eq()`](http://api.jquery.com/eq/) + * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name + * - [`hasClass()`](http://api.jquery.com/hasClass/) + * - [`html()`](http://api.jquery.com/html/) + * - [`next()`](http://api.jquery.com/next/) - Does not support selectors + * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData + * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors + * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors + * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors + * - [`prepend()`](http://api.jquery.com/prepend/) + * - [`prop()`](http://api.jquery.com/prop/) + * - [`ready()`](http://api.jquery.com/ready/) + * - [`remove()`](http://api.jquery.com/remove/) + * - [`removeAttr()`](http://api.jquery.com/removeAttr/) + * - [`removeClass()`](http://api.jquery.com/removeClass/) + * - [`removeData()`](http://api.jquery.com/removeData/) + * - [`replaceWith()`](http://api.jquery.com/replaceWith/) + * - [`text()`](http://api.jquery.com/text/) + * - [`toggleClass()`](http://api.jquery.com/toggleClass/) + * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. + * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces + * - [`val()`](http://api.jquery.com/val/) + * - [`wrap()`](http://api.jquery.com/wrap/) + * + * ## jQuery/jqLite Extras + * Angular also provides the following additional methods and events to both jQuery and jqLite: + * + * ### Events + * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event + * on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM + * element before it is removed. + * + * ### Methods + * - `controller(name)` - retrieves the controller of the current element or its parent. By default + * retrieves controller associated with the `ngController` directive. If `name` is provided as + * camelCase directive name, then the controller for this directive will be retrieved (e.g. + * `'ngModel'`). + * - `injector()` - retrieves the injector of the current element or its parent. + * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current + * element or its parent. + * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the + * current element. This getter should be used only on elements that contain a directive which starts a new isolate + * scope. Calling `scope()` on this element always returns the original non-isolate scope. + * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top + * parent element is reached. + * + * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. + * @returns {Object} jQuery object. + */ + +var jqCache = JQLite.cache = {}, + jqName = JQLite.expando = 'ng' + new Date().getTime(), + jqId = 1, + addEventListenerFn = (window.document.addEventListener + ? function(element, type, fn) {element.addEventListener(type, fn, false);} + : function(element, type, fn) {element.attachEvent('on' + type, fn);}), + removeEventListenerFn = (window.document.removeEventListener + ? function(element, type, fn) {element.removeEventListener(type, fn, false); } + : function(element, type, fn) {element.detachEvent('on' + type, fn); }); + +/* + * !!! This is an undocumented "private" function !!! + */ +var jqData = JQLite._data = function(node) { + //jQuery always returns an object on cache miss + return this.cache[node[this.expando]] || {}; +}; + +function jqNextId() { return ++jqId; } + + +var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; +var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var jqLiteMinErr = minErr('jqLite'); + +/** + * Converts snake_case to camelCase. + * Also there is special case for Moz prefix starting with upper case letter. + * @param name Name to normalize + */ +function camelCase(name) { + return name. + replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }). + replace(MOZ_HACK_REGEXP, 'Moz$1'); +} + +///////////////////////////////////////////// +// jQuery mutation patch +// +// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a +// $destroy event on all DOM nodes being removed. +// +///////////////////////////////////////////// + +function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { + var originalJqFn = jQuery.fn[name]; + originalJqFn = originalJqFn.$original || originalJqFn; + removePatch.$original = originalJqFn; + jQuery.fn[name] = removePatch; + + function removePatch(param) { + // jshint -W040 + var list = filterElems && param ? [this.filter(param)] : [this], + fireEvent = dispatchThis, + set, setIndex, setLength, + element, childIndex, childLength, children; + + if (!getterIfNoArguments || param != null) { + while(list.length) { + set = list.shift(); + for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { + element = jqLite(set[setIndex]); + if (fireEvent) { + element.triggerHandler('$destroy'); + } else { + fireEvent = !fireEvent; + } + for(childIndex = 0, childLength = (children = element.children()).length; + childIndex < childLength; + childIndex++) { + list.push(jQuery(children[childIndex])); + } + } + } + } + return originalJqFn.apply(this, arguments); + } +} + +var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; +var HTML_REGEXP = /<|&#?\w+;/; +var TAG_NAME_REGEXP = /<([\w:]+)/; +var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; + +var wrapMap = { + 'option': [1, ''], + + 'thead': [1, '', '
'], + 'col': [2, '', '
'], + 'tr': [2, '', '
'], + 'td': [3, '', '
'], + '_default': [0, "", ""] +}; + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +function jqLiteIsTextNode(html) { + return !HTML_REGEXP.test(html); +} + +function jqLiteBuildFragment(html, context) { + var elem, tmp, tag, wrap, + fragment = context.createDocumentFragment(), + nodes = [], i, j, jj; + + if (jqLiteIsTextNode(html)) { + // Convert non-html into a text node + nodes.push(context.createTextNode(html)); + } else { + tmp = fragment.appendChild(context.createElement('div')); + // Convert html into DOM nodes + tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); + wrap = wrapMap[tag] || wrapMap._default; + tmp.innerHTML = '
 
' + + wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1>") + wrap[2]; + tmp.removeChild(tmp.firstChild); + + // Descend through wrappers to the right content + i = wrap[0]; + while (i--) { + tmp = tmp.lastChild; + } + + for (j=0, jj=tmp.childNodes.length; j -1); +} + +function jqLiteRemoveClass(element, cssClasses) { + if (cssClasses && element.setAttribute) { + forEach(cssClasses.split(' '), function(cssClass) { + element.setAttribute('class', trim( + (" " + (element.getAttribute('class') || '') + " ") + .replace(/[\n\t]/g, " ") + .replace(" " + trim(cssClass) + " ", " ")) + ); + }); + } +} + +function jqLiteAddClass(element, cssClasses) { + if (cssClasses && element.setAttribute) { + var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, " "); + + forEach(cssClasses.split(' '), function(cssClass) { + cssClass = trim(cssClass); + if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { + existingClasses += cssClass + ' '; + } + }); + + element.setAttribute('class', trim(existingClasses)); + } +} + +function jqLiteAddNodes(root, elements) { + if (elements) { + elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) + ? elements + : [ elements ]; + for(var i=0; i < elements.length; i++) { + root.push(elements[i]); + } + } +} + +function jqLiteController(element, name) { + return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); +} + +function jqLiteInheritedData(element, name, value) { + element = jqLite(element); + + // if element is the document object work with the html element instead + // this makes $(document).scope() possible + if(element[0].nodeType == 9) { + element = element.find('html'); + } + var names = isArray(name) ? name : [name]; + + while (element.length) { + var node = element[0]; + for (var i = 0, ii = names.length; i < ii; i++) { + if ((value = element.data(names[i])) !== undefined) return value; + } + + // If dealing with a document fragment node with a host element, and no parent, use the host + // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM + // to lookup parent controllers. + element = jqLite(node.parentNode || (node.nodeType === 11 && node.host)); + } +} + +function jqLiteEmpty(element) { + for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { + jqLiteDealoc(childNodes[i]); + } + while (element.firstChild) { + element.removeChild(element.firstChild); + } +} + +////////////////////////////////////////// +// Functions which are declared directly. +////////////////////////////////////////// +var JQLitePrototype = JQLite.prototype = { + ready: function(fn) { + var fired = false; + + function trigger() { + if (fired) return; + fired = true; + fn(); + } + + // check if document already is loaded + if (document.readyState === 'complete'){ + setTimeout(trigger); + } else { + this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 + // we can not use jqLite since we are not done loading and jQuery could be loaded later. + // jshint -W064 + JQLite(window).on('load', trigger); // fallback to window.onload for others + // jshint +W064 + } + }, + toString: function() { + var value = []; + forEach(this, function(e){ value.push('' + e);}); + return '[' + value.join(', ') + ']'; + }, + + eq: function(index) { + return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]); + }, + + length: 0, + push: push, + sort: [].sort, + splice: [].splice +}; + +////////////////////////////////////////// +// Functions iterating getter/setters. +// these functions return self on setter and +// value on get. +////////////////////////////////////////// +var BOOLEAN_ATTR = {}; +forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { + BOOLEAN_ATTR[lowercase(value)] = value; +}); +var BOOLEAN_ELEMENTS = {}; +forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { + BOOLEAN_ELEMENTS[uppercase(value)] = true; +}); + +function getBooleanAttrName(element, name) { + // check dom last since we will most likely fail on name + var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; + + // booleanAttr is here twice to minimize DOM access + return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; +} + +forEach({ + data: jqLiteData, + inheritedData: jqLiteInheritedData, + + scope: function(element) { + // Can't use jqLiteData here directly so we stay compatible with jQuery! + return jqLite(element).data('$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); + }, + + isolateScope: function(element) { + // Can't use jqLiteData here directly so we stay compatible with jQuery! + return jqLite(element).data('$isolateScope') || jqLite(element).data('$isolateScopeNoTemplate'); + }, + + controller: jqLiteController, + + injector: function(element) { + return jqLiteInheritedData(element, '$injector'); + }, + + removeAttr: function(element,name) { + element.removeAttribute(name); + }, + + hasClass: jqLiteHasClass, + + css: function(element, name, value) { + name = camelCase(name); + + if (isDefined(value)) { + element.style[name] = value; + } else { + var val; + + if (msie <= 8) { + // this is some IE specific weirdness that jQuery 1.6.4 does not sure why + val = element.currentStyle && element.currentStyle[name]; + if (val === '') val = 'auto'; + } + + val = val || element.style[name]; + + if (msie <= 8) { + // jquery weirdness :-/ + val = (val === '') ? undefined : val; + } + + return val; + } + }, + + attr: function(element, name, value){ + var lowercasedName = lowercase(name); + if (BOOLEAN_ATTR[lowercasedName]) { + if (isDefined(value)) { + if (!!value) { + element[name] = true; + element.setAttribute(name, lowercasedName); + } else { + element[name] = false; + element.removeAttribute(lowercasedName); + } + } else { + return (element[name] || + (element.attributes.getNamedItem(name)|| noop).specified) + ? lowercasedName + : undefined; + } + } else if (isDefined(value)) { + element.setAttribute(name, value); + } else if (element.getAttribute) { + // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code + // some elements (e.g. Document) don't have get attribute, so return undefined + var ret = element.getAttribute(name, 2); + // normalize non-existing attributes to undefined (as jQuery) + return ret === null ? undefined : ret; + } + }, + + prop: function(element, name, value) { + if (isDefined(value)) { + element[name] = value; + } else { + return element[name]; + } + }, + + text: (function() { + var NODE_TYPE_TEXT_PROPERTY = []; + if (msie < 9) { + NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ + } else { + NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ + } + getText.$dv = ''; + return getText; + + function getText(element, value) { + var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]; + if (isUndefined(value)) { + return textProp ? element[textProp] : ''; + } + element[textProp] = value; + } + })(), + + val: function(element, value) { + if (isUndefined(value)) { + if (nodeName_(element) === 'SELECT' && element.multiple) { + var result = []; + forEach(element.options, function (option) { + if (option.selected) { + result.push(option.value || option.text); + } + }); + return result.length === 0 ? null : result; + } + return element.value; + } + element.value = value; + }, + + html: function(element, value) { + if (isUndefined(value)) { + return element.innerHTML; + } + for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { + jqLiteDealoc(childNodes[i]); + } + element.innerHTML = value; + }, + + empty: jqLiteEmpty +}, function(fn, name){ + /** + * Properties: writes return selection, reads return first value + */ + JQLite.prototype[name] = function(arg1, arg2) { + var i, key; + + // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it + // in a way that survives minification. + // jqLiteEmpty takes no arguments but is a setter. + if (fn !== jqLiteEmpty && + (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { + if (isObject(arg1)) { + + // we are a write, but the object properties are the key/values + for (i = 0; i < this.length; i++) { + if (fn === jqLiteData) { + // data() takes the whole object in jQuery + fn(this[i], arg1); + } else { + for (key in arg1) { + fn(this[i], key, arg1[key]); + } + } + } + // return self for chaining + return this; + } else { + // we are a read, so read the first child. + var value = fn.$dv; + // Only if we have $dv do we iterate over all, otherwise it is just the first element. + var jj = (value === undefined) ? Math.min(this.length, 1) : this.length; + for (var j = 0; j < jj; j++) { + var nodeValue = fn(this[j], arg1, arg2); + value = value ? value + nodeValue : nodeValue; + } + return value; + } + } else { + // we are a write, so apply to all children + for (i = 0; i < this.length; i++) { + fn(this[i], arg1, arg2); + } + // return self for chaining + return this; + } + }; +}); + +function createEventHandler(element, events) { + var eventHandler = function (event, type) { + if (!event.preventDefault) { + event.preventDefault = function() { + event.returnValue = false; //ie + }; + } + + if (!event.stopPropagation) { + event.stopPropagation = function() { + event.cancelBubble = true; //ie + }; + } + + if (!event.target) { + event.target = event.srcElement || document; + } + + if (isUndefined(event.defaultPrevented)) { + var prevent = event.preventDefault; + event.preventDefault = function() { + event.defaultPrevented = true; + prevent.call(event); + }; + event.defaultPrevented = false; + } + + event.isDefaultPrevented = function() { + return event.defaultPrevented || event.returnValue === false; + }; + + // Copy event handlers in case event handlers array is modified during execution. + var eventHandlersCopy = shallowCopy(events[type || event.type] || []); + + forEach(eventHandlersCopy, function(fn) { + fn.call(element, event); + }); + + // Remove monkey-patched methods (IE), + // as they would cause memory leaks in IE8. + if (msie <= 8) { + // IE7/8 does not allow to delete property on native object + event.preventDefault = null; + event.stopPropagation = null; + event.isDefaultPrevented = null; + } else { + // It shouldn't affect normal browsers (native methods are defined on prototype). + delete event.preventDefault; + delete event.stopPropagation; + delete event.isDefaultPrevented; + } + }; + eventHandler.elem = element; + return eventHandler; +} + +////////////////////////////////////////// +// Functions iterating traversal. +// These functions chain results into a single +// selector. +////////////////////////////////////////// +forEach({ + removeData: jqLiteRemoveData, + + dealoc: jqLiteDealoc, + + on: function onFn(element, type, fn, unsupported){ + if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); + + var events = jqLiteExpandoStore(element, 'events'), + handle = jqLiteExpandoStore(element, 'handle'); + + if (!events) jqLiteExpandoStore(element, 'events', events = {}); + if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); + + forEach(type.split(' '), function(type){ + var eventFns = events[type]; + + if (!eventFns) { + if (type == 'mouseenter' || type == 'mouseleave') { + var contains = document.body.contains || document.body.compareDocumentPosition ? + function( a, b ) { + // jshint bitwise: false + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + events[type] = []; + + // Refer to jQuery's implementation of mouseenter & mouseleave + // Read about mouseenter and mouseleave: + // http://www.quirksmode.org/js/events_mouse.html#link8 + var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; + + onFn(element, eventmap[type], function(event) { + var target = this, related = event.relatedTarget; + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !contains(target, related)) ){ + handle(event, type); + } + }); + + } else { + addEventListenerFn(element, type, handle); + events[type] = []; + } + eventFns = events[type]; + } + eventFns.push(fn); + }); + }, + + off: jqLiteOff, + + one: function(element, type, fn) { + element = jqLite(element); + + //add the listener twice so that when it is called + //you can remove the original function and still be + //able to call element.off(ev, fn) normally + element.on(type, function onFn() { + element.off(type, fn); + element.off(type, onFn); + }); + element.on(type, fn); + }, + + replaceWith: function(element, replaceNode) { + var index, parent = element.parentNode; + jqLiteDealoc(element); + forEach(new JQLite(replaceNode), function(node){ + if (index) { + parent.insertBefore(node, index.nextSibling); + } else { + parent.replaceChild(node, element); + } + index = node; + }); + }, + + children: function(element) { + var children = []; + forEach(element.childNodes, function(element){ + if (element.nodeType === 1) + children.push(element); + }); + return children; + }, + + contents: function(element) { + return element.contentDocument || element.childNodes || []; + }, + + append: function(element, node) { + forEach(new JQLite(node), function(child){ + if (element.nodeType === 1 || element.nodeType === 11) { + element.appendChild(child); + } + }); + }, + + prepend: function(element, node) { + if (element.nodeType === 1) { + var index = element.firstChild; + forEach(new JQLite(node), function(child){ + element.insertBefore(child, index); + }); + } + }, + + wrap: function(element, wrapNode) { + wrapNode = jqLite(wrapNode)[0]; + var parent = element.parentNode; + if (parent) { + parent.replaceChild(wrapNode, element); + } + wrapNode.appendChild(element); + }, + + remove: function(element) { + jqLiteDealoc(element); + var parent = element.parentNode; + if (parent) parent.removeChild(element); + }, + + after: function(element, newElement) { + var index = element, parent = element.parentNode; + forEach(new JQLite(newElement), function(node){ + parent.insertBefore(node, index.nextSibling); + index = node; + }); + }, + + addClass: jqLiteAddClass, + removeClass: jqLiteRemoveClass, + + toggleClass: function(element, selector, condition) { + if (selector) { + forEach(selector.split(' '), function(className){ + var classCondition = condition; + if (isUndefined(classCondition)) { + classCondition = !jqLiteHasClass(element, className); + } + (classCondition ? jqLiteAddClass : jqLiteRemoveClass)(element, className); + }); + } + }, + + parent: function(element) { + var parent = element.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + + next: function(element) { + if (element.nextElementSibling) { + return element.nextElementSibling; + } + + // IE8 doesn't have nextElementSibling + var elm = element.nextSibling; + while (elm != null && elm.nodeType !== 1) { + elm = elm.nextSibling; + } + return elm; + }, + + find: function(element, selector) { + if (element.getElementsByTagName) { + return element.getElementsByTagName(selector); + } else { + return []; + } + }, + + clone: jqLiteClone, + + triggerHandler: function(element, eventName, eventData) { + var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; + + eventData = eventData || []; + + var event = [{ + preventDefault: noop, + stopPropagation: noop + }]; + + forEach(eventFns, function(fn) { + fn.apply(element, event.concat(eventData)); + }); + } +}, function(fn, name){ + /** + * chaining functions + */ + JQLite.prototype[name] = function(arg1, arg2, arg3) { + var value; + for(var i=0; i < this.length; i++) { + if (isUndefined(value)) { + value = fn(this[i], arg1, arg2, arg3); + if (isDefined(value)) { + // any function which returns a value needs to be wrapped + value = jqLite(value); + } + } else { + jqLiteAddNodes(value, fn(this[i], arg1, arg2, arg3)); + } + } + return isDefined(value) ? value : this; + }; + + // bind legacy bind/unbind to on/off + JQLite.prototype.bind = JQLite.prototype.on; + JQLite.prototype.unbind = JQLite.prototype.off; +}); + +/** + * Computes a hash of an 'obj'. + * Hash of a: + * string is string + * number is number as string + * object is either result of calling $$hashKey function on the object or uniquely generated id, + * that is also assigned to the $$hashKey property of the object. + * + * @param obj + * @returns {string} hash string such that the same input will have the same hash string. + * The resulting string key is in 'type:hashKey' format. + */ +function hashKey(obj) { + var objType = typeof obj, + key; + + if (objType == 'object' && obj !== null) { + if (typeof (key = obj.$$hashKey) == 'function') { + // must invoke on object to keep the right this + key = obj.$$hashKey(); + } else if (key === undefined) { + key = obj.$$hashKey = nextUid(); + } + } else { + key = obj; + } + + return objType + ':' + key; +} + +/** + * HashMap which can use objects as keys + */ +function HashMap(array){ + forEach(array, this.put, this); +} +HashMap.prototype = { + /** + * Store key value pair + * @param key key to store can be any type + * @param value value to store can be any type + */ + put: function(key, value) { + this[hashKey(key)] = value; + }, + + /** + * @param key + * @returns {Object} the value for the key + */ + get: function(key) { + return this[hashKey(key)]; + }, + + /** + * Remove the key/value pair + * @param key + */ + remove: function(key) { + var value = this[key = hashKey(key)]; + delete this[key]; + return value; + } +}; + +/** + * @ngdoc function + * @module ng + * @name angular.injector + * @kind function + * + * @description + * Creates an injector function that can be used for retrieving services as well as for + * dependency injection (see {@link guide/di dependency injection}). + * + + * @param {Array.} modules A list of module functions or their aliases. See + * {@link angular.module}. The `ng` module must be explicitly added. + * @returns {function()} Injector function. See {@link auto.$injector $injector}. + * + * @example + * Typical usage + * ```js + * // create an injector + * var $injector = angular.injector(['ng']); + * + * // use the injector to kick off your application + * // use the type inference to auto inject arguments, or use implicit injection + * $injector.invoke(function($rootScope, $compile, $document){ + * $compile($document)($rootScope); + * $rootScope.$digest(); + * }); + * ``` + * + * Sometimes you want to get access to the injector of a currently running Angular app + * from outside Angular. Perhaps, you want to inject and compile some markup after the + * application has been bootstrapped. You can do this using the extra `injector()` added + * to JQuery/jqLite elements. See {@link angular.element}. + * + * *This is fairly rare but could be the case if a third party library is injecting the + * markup.* + * + * In the following example a new block of HTML containing a `ng-controller` + * directive is added to the end of the document body by JQuery. We then compile and link + * it into the current AngularJS scope. + * + * ```js + * var $div = $('
{{content.label}}
'); + * $(document.body).append($div); + * + * angular.element(document).injector().invoke(function($compile) { + * var scope = angular.element($div).scope(); + * $compile($div)(scope); + * }); + * ``` + */ + + +/** + * @ngdoc module + * @name auto + * @description + * + * Implicit module which gets automatically added to each {@link auto.$injector $injector}. + */ + +var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; +var FN_ARG_SPLIT = /,/; +var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; +var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; +var $injectorMinErr = minErr('$injector'); +function annotate(fn) { + var $inject, + fnText, + argDecl, + last; + + if (typeof fn == 'function') { + if (!($inject = fn.$inject)) { + $inject = []; + if (fn.length) { + fnText = fn.toString().replace(STRIP_COMMENTS, ''); + argDecl = fnText.match(FN_ARGS); + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ + arg.replace(FN_ARG, function(all, underscore, name){ + $inject.push(name); + }); + }); + } + fn.$inject = $inject; + } + } else if (isArray(fn)) { + last = fn.length - 1; + assertArgFn(fn[last], 'fn'); + $inject = fn.slice(0, last); + } else { + assertArgFn(fn, 'fn', true); + } + return $inject; +} + +/////////////////////////////////////// + +/** + * @ngdoc service + * @name $injector + * @kind function + * + * @description + * + * `$injector` is used to retrieve object instances as defined by + * {@link auto.$provide provider}, instantiate types, invoke methods, + * and load modules. + * + * The following always holds true: + * + * ```js + * var $injector = angular.injector(); + * expect($injector.get('$injector')).toBe($injector); + * expect($injector.invoke(function($injector){ + * return $injector; + * }).toBe($injector); + * ``` + * + * # Injection Function Annotation + * + * JavaScript does not have annotations, and annotations are needed for dependency injection. The + * following are all valid ways of annotating function with injection arguments and are equivalent. + * + * ```js + * // inferred (only works if code not minified/obfuscated) + * $injector.invoke(function(serviceA){}); + * + * // annotated + * function explicit(serviceA) {}; + * explicit.$inject = ['serviceA']; + * $injector.invoke(explicit); + * + * // inline + * $injector.invoke(['serviceA', function(serviceA){}]); + * ``` + * + * ## Inference + * + * In JavaScript calling `toString()` on a function returns the function definition. The definition + * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with + * minification, and obfuscation tools since these tools change the argument names. + * + * ## `$inject` Annotation + * By adding an `$inject` property onto a function the injection parameters can be specified. + * + * ## Inline + * As an array of injection names, where the last item in the array is the function to call. + */ + +/** + * @ngdoc method + * @name $injector#get + * + * @description + * Return an instance of the service. + * + * @param {string} name The name of the instance to retrieve. + * @return {*} The instance. + */ + +/** + * @ngdoc method + * @name $injector#invoke + * + * @description + * Invoke the method and supply the method arguments from the `$injector`. + * + * @param {!Function} fn The function to invoke. Function parameters are injected according to the + * {@link guide/di $inject Annotation} rules. + * @param {Object=} self The `this` for the invoked method. + * @param {Object=} locals Optional object. If preset then any argument names are read from this + * object first, before the `$injector` is consulted. + * @returns {*} the value returned by the invoked `fn` function. + */ + +/** + * @ngdoc method + * @name $injector#has + * + * @description + * Allows the user to query if the particular service exists. + * + * @param {string} Name of the service to query. + * @returns {boolean} returns true if injector has given service. + */ + +/** + * @ngdoc method + * @name $injector#instantiate + * @description + * Create a new instance of JS type. The method takes a constructor function, invokes the new + * operator, and supplies all of the arguments to the constructor function as specified by the + * constructor annotation. + * + * @param {Function} Type Annotated constructor function. + * @param {Object=} locals Optional object. If preset then any argument names are read from this + * object first, before the `$injector` is consulted. + * @returns {Object} new instance of `Type`. + */ + +/** + * @ngdoc method + * @name $injector#annotate + * + * @description + * Returns an array of service names which the function is requesting for injection. This API is + * used by the injector to determine which services need to be injected into the function when the + * function is invoked. There are three ways in which the function can be annotated with the needed + * dependencies. + * + * # Argument names + * + * The simplest form is to extract the dependencies from the arguments of the function. This is done + * by converting the function into a string using `toString()` method and extracting the argument + * names. + * ```js + * // Given + * function MyController($scope, $route) { + * // ... + * } + * + * // Then + * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); + * ``` + * + * This method does not work with code minification / obfuscation. For this reason the following + * annotation strategies are supported. + * + * # The `$inject` property + * + * If a function has an `$inject` property and its value is an array of strings, then the strings + * represent names of services to be injected into the function. + * ```js + * // Given + * var MyController = function(obfuscatedScope, obfuscatedRoute) { + * // ... + * } + * // Define function dependencies + * MyController['$inject'] = ['$scope', '$route']; + * + * // Then + * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); + * ``` + * + * # The array notation + * + * It is often desirable to inline Injected functions and that's when setting the `$inject` property + * is very inconvenient. In these situations using the array notation to specify the dependencies in + * a way that survives minification is a better choice: + * + * ```js + * // We wish to write this (not minification / obfuscation safe) + * injector.invoke(function($compile, $rootScope) { + * // ... + * }); + * + * // We are forced to write break inlining + * var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) { + * // ... + * }; + * tmpFn.$inject = ['$compile', '$rootScope']; + * injector.invoke(tmpFn); + * + * // To better support inline function the inline annotation is supported + * injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) { + * // ... + * }]); + * + * // Therefore + * expect(injector.annotate( + * ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}]) + * ).toEqual(['$compile', '$rootScope']); + * ``` + * + * @param {Function|Array.} fn Function for which dependent service names need to + * be retrieved as described above. + * + * @returns {Array.} The names of the services which the function requires. + */ + + + + +/** + * @ngdoc object + * @name $provide + * + * @description + * + * The {@link auto.$provide $provide} service has a number of methods for registering components + * with the {@link auto.$injector $injector}. Many of these functions are also exposed on + * {@link angular.Module}. + * + * An Angular **service** is a singleton object created by a **service factory**. These **service + * factories** are functions which, in turn, are created by a **service provider**. + * The **service providers** are constructor functions. When instantiated they must contain a + * property called `$get`, which holds the **service factory** function. + * + * When you request a service, the {@link auto.$injector $injector} is responsible for finding the + * correct **service provider**, instantiating it and then calling its `$get` **service factory** + * function to get the instance of the **service**. + * + * Often services have no configuration options and there is no need to add methods to the service + * provider. The provider will be no more than a constructor function with a `$get` property. For + * these cases the {@link auto.$provide $provide} service has additional helper methods to register + * services without specifying a provider. + * + * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the + * {@link auto.$injector $injector} + * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by + * providers and services. + * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by + * services, not providers. + * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, + * that will be wrapped in a **service provider** object, whose `$get` property will contain the + * given factory function. + * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class` + * that will be wrapped in a **service provider** object, whose `$get` property will instantiate + * a new object using the given constructor function. + * + * See the individual methods for more information and examples. + */ + +/** + * @ngdoc method + * @name $provide#provider + * @description + * + * Register a **provider function** with the {@link auto.$injector $injector}. Provider functions + * are constructor functions, whose instances are responsible for "providing" a factory for a + * service. + * + * Service provider names start with the name of the service they provide followed by `Provider`. + * For example, the {@link ng.$log $log} service has a provider called + * {@link ng.$logProvider $logProvider}. + * + * Service provider objects can have additional methods which allow configuration of the provider + * and its service. Importantly, you can configure what kind of service is created by the `$get` + * method, or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a + * method {@link ng.$logProvider#debugEnabled debugEnabled} + * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the + * console or not. + * + * @param {string} name The name of the instance. NOTE: the provider will be available under `name + + 'Provider'` key. + * @param {(Object|function())} provider If the provider is: + * + * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using + * {@link auto.$injector#invoke $injector.invoke()} when an instance needs to be created. + * - `Constructor`: a new instance of the provider will be created using + * {@link auto.$injector#instantiate $injector.instantiate()}, then treated as `object`. + * + * @returns {Object} registered provider instance + + * @example + * + * The following example shows how to create a simple event tracking service and register it using + * {@link auto.$provide#provider $provide.provider()}. + * + * ```js + * // Define the eventTracker provider + * function EventTrackerProvider() { + * var trackingUrl = '/track'; + * + * // A provider method for configuring where the tracked events should been saved + * this.setTrackingUrl = function(url) { + * trackingUrl = url; + * }; + * + * // The service factory function + * this.$get = ['$http', function($http) { + * var trackedEvents = {}; + * return { + * // Call this to track an event + * event: function(event) { + * var count = trackedEvents[event] || 0; + * count += 1; + * trackedEvents[event] = count; + * return count; + * }, + * // Call this to save the tracked events to the trackingUrl + * save: function() { + * $http.post(trackingUrl, trackedEvents); + * } + * }; + * }]; + * } + * + * describe('eventTracker', function() { + * var postSpy; + * + * beforeEach(module(function($provide) { + * // Register the eventTracker provider + * $provide.provider('eventTracker', EventTrackerProvider); + * })); + * + * beforeEach(module(function(eventTrackerProvider) { + * // Configure eventTracker provider + * eventTrackerProvider.setTrackingUrl('/custom-track'); + * })); + * + * it('tracks events', inject(function(eventTracker) { + * expect(eventTracker.event('login')).toEqual(1); + * expect(eventTracker.event('login')).toEqual(2); + * })); + * + * it('saves to the tracking url', inject(function(eventTracker, $http) { + * postSpy = spyOn($http, 'post'); + * eventTracker.event('login'); + * eventTracker.save(); + * expect(postSpy).toHaveBeenCalled(); + * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); + * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); + * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); + * })); + * }); + * ``` + */ + +/** + * @ngdoc method + * @name $provide#factory + * @description + * + * Register a **service factory**, which will be called to return the service instance. + * This is short for registering a service where its provider consists of only a `$get` property, + * which is the given service factory function. + * You should use {@link auto.$provide#factory $provide.factory(getFn)} if you do not need to + * configure your service in a provider. + * + * @param {string} name The name of the instance. + * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand + * for `$provide.provider(name, {$get: $getFn})`. + * @returns {Object} registered provider instance + * + * @example + * Here is an example of registering a service + * ```js + * $provide.factory('ping', ['$http', function($http) { + * return function ping() { + * return $http.send('/ping'); + * }; + * }]); + * ``` + * You would then inject and use this service like this: + * ```js + * someModule.controller('Ctrl', ['ping', function(ping) { + * ping(); + * }]); + * ``` + */ + + +/** + * @ngdoc method + * @name $provide#service + * @description + * + * Register a **service constructor**, which will be invoked with `new` to create the service + * instance. + * This is short for registering a service where its provider's `$get` property is the service + * constructor function that will be used to instantiate the service instance. + * + * You should use {@link auto.$provide#service $provide.service(class)} if you define your service + * as a type/class. + * + * @param {string} name The name of the instance. + * @param {Function} constructor A class (constructor function) that will be instantiated. + * @returns {Object} registered provider instance + * + * @example + * Here is an example of registering a service using + * {@link auto.$provide#service $provide.service(class)}. + * ```js + * var Ping = function($http) { + * this.$http = $http; + * }; + * + * Ping.$inject = ['$http']; + * + * Ping.prototype.send = function() { + * return this.$http.get('/ping'); + * }; + * $provide.service('ping', Ping); + * ``` + * You would then inject and use this service like this: + * ```js + * someModule.controller('Ctrl', ['ping', function(ping) { + * ping.send(); + * }]); + * ``` + */ + + +/** + * @ngdoc method + * @name $provide#value + * @description + * + * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a + * number, an array, an object or a function. This is short for registering a service where its + * provider's `$get` property is a factory function that takes no arguments and returns the **value + * service**. + * + * Value services are similar to constant services, except that they cannot be injected into a + * module configuration function (see {@link angular.Module#config}) but they can be overridden by + * an Angular + * {@link auto.$provide#decorator decorator}. + * + * @param {string} name The name of the instance. + * @param {*} value The value. + * @returns {Object} registered provider instance + * + * @example + * Here are some examples of creating value services. + * ```js + * $provide.value('ADMIN_USER', 'admin'); + * + * $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 }); + * + * $provide.value('halfOf', function(value) { + * return value / 2; + * }); + * ``` + */ + + +/** + * @ngdoc method + * @name $provide#constant + * @description + * + * Register a **constant service**, such as a string, a number, an array, an object or a function, + * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be + * injected into a module configuration function (see {@link angular.Module#config}) and it cannot + * be overridden by an Angular {@link auto.$provide#decorator decorator}. + * + * @param {string} name The name of the constant. + * @param {*} value The constant value. + * @returns {Object} registered instance + * + * @example + * Here a some examples of creating constants: + * ```js + * $provide.constant('SHARD_HEIGHT', 306); + * + * $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); + * + * $provide.constant('double', function(value) { + * return value * 2; + * }); + * ``` + */ + + +/** + * @ngdoc method + * @name $provide#decorator + * @description + * + * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator + * intercepts the creation of a service, allowing it to override or modify the behaviour of the + * service. The object returned by the decorator may be the original service, or a new service + * object which replaces or wraps and delegates to the original service. + * + * @param {string} name The name of the service to decorate. + * @param {function()} decorator This function will be invoked when the service needs to be + * instantiated and should return the decorated service instance. The function is called using + * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. + * Local injection arguments: + * + * * `$delegate` - The original service instance, which can be monkey patched, configured, + * decorated or delegated to. + * + * @example + * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting + * calls to {@link ng.$log#error $log.warn()}. + * ```js + * $provide.decorator('$log', ['$delegate', function($delegate) { + * $delegate.warn = $delegate.error; + * return $delegate; + * }]); + * ``` + */ + + +function createInjector(modulesToLoad) { + var INSTANTIATING = {}, + providerSuffix = 'Provider', + path = [], + loadedModules = new HashMap(), + providerCache = { + $provide: { + provider: supportObject(provider), + factory: supportObject(factory), + service: supportObject(service), + value: supportObject(value), + constant: supportObject(constant), + decorator: decorator + } + }, + providerInjector = (providerCache.$injector = + createInternalInjector(providerCache, function() { + throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); + })), + instanceCache = {}, + instanceInjector = (instanceCache.$injector = + createInternalInjector(instanceCache, function(servicename) { + var provider = providerInjector.get(servicename + providerSuffix); + return instanceInjector.invoke(provider.$get, provider); + })); + + + forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); + + return instanceInjector; + + //////////////////////////////////// + // $provider + //////////////////////////////////// + + function supportObject(delegate) { + return function(key, value) { + if (isObject(key)) { + forEach(key, reverseParams(delegate)); + } else { + return delegate(key, value); + } + }; + } + + function provider(name, provider_) { + assertNotHasOwnProperty(name, 'service'); + if (isFunction(provider_) || isArray(provider_)) { + provider_ = providerInjector.instantiate(provider_); + } + if (!provider_.$get) { + throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); + } + return providerCache[name + providerSuffix] = provider_; + } + + function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } + + function service(name, constructor) { + return factory(name, ['$injector', function($injector) { + return $injector.instantiate(constructor); + }]); + } + + function value(name, val) { return factory(name, valueFn(val)); } + + function constant(name, value) { + assertNotHasOwnProperty(name, 'constant'); + providerCache[name] = value; + instanceCache[name] = value; + } + + function decorator(serviceName, decorFn) { + var origProvider = providerInjector.get(serviceName + providerSuffix), + orig$get = origProvider.$get; + + origProvider.$get = function() { + var origInstance = instanceInjector.invoke(orig$get, origProvider); + return instanceInjector.invoke(decorFn, null, {$delegate: origInstance}); + }; + } + + //////////////////////////////////// + // Module Loading + //////////////////////////////////// + function loadModules(modulesToLoad){ + var runBlocks = [], moduleFn, invokeQueue, i, ii; + forEach(modulesToLoad, function(module) { + if (loadedModules.get(module)) return; + loadedModules.put(module, true); + + try { + if (isString(module)) { + moduleFn = angularModule(module); + runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); + + for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { + var invokeArgs = invokeQueue[i], + provider = providerInjector.get(invokeArgs[0]); + + provider[invokeArgs[1]].apply(provider, invokeArgs[2]); + } + } else if (isFunction(module)) { + runBlocks.push(providerInjector.invoke(module)); + } else if (isArray(module)) { + runBlocks.push(providerInjector.invoke(module)); + } else { + assertArgFn(module, 'module'); + } + } catch (e) { + if (isArray(module)) { + module = module[module.length - 1]; + } + if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { + // Safari & FF's stack traces don't contain error.message content + // unlike those of Chrome and IE + // So if stack doesn't contain message, we create a new string that contains both. + // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. + /* jshint -W022 */ + e = e.message + '\n' + e.stack; + } + throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", + module, e.stack || e.message || e); + } + }); + return runBlocks; + } + + //////////////////////////////////// + // internal Injector + //////////////////////////////////// + + function createInternalInjector(cache, factory) { + + function getService(serviceName) { + if (cache.hasOwnProperty(serviceName)) { + if (cache[serviceName] === INSTANTIATING) { + throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); + } + return cache[serviceName]; + } else { + try { + path.unshift(serviceName); + cache[serviceName] = INSTANTIATING; + return cache[serviceName] = factory(serviceName); + } catch (err) { + if (cache[serviceName] === INSTANTIATING) { + delete cache[serviceName]; + } + throw err; + } finally { + path.shift(); + } + } + } + + function invoke(fn, self, locals){ + var args = [], + $inject = annotate(fn), + length, i, + key; + + for(i = 0, length = $inject.length; i < length; i++) { + key = $inject[i]; + if (typeof key !== 'string') { + throw $injectorMinErr('itkn', + 'Incorrect injection token! Expected service name as string, got {0}', key); + } + args.push( + locals && locals.hasOwnProperty(key) + ? locals[key] + : getService(key) + ); + } + if (!fn.$inject) { + // this means that we must be an array. + fn = fn[length]; + } + + // http://jsperf.com/angularjs-invoke-apply-vs-switch + // #5388 + return fn.apply(self, args); + } + + function instantiate(Type, locals) { + var Constructor = function() {}, + instance, returnedValue; + + // Check if Type is annotated and use just the given function at n-1 as parameter + // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); + Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; + instance = new Constructor(); + returnedValue = invoke(Type, instance, locals); + + return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; + } + + return { + invoke: invoke, + instantiate: instantiate, + get: getService, + annotate: annotate, + has: function(name) { + return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); + } + }; + } +} + +/** + * @ngdoc service + * @name $anchorScroll + * @kind function + * @requires $window + * @requires $location + * @requires $rootScope + * + * @description + * When called, it checks current value of `$location.hash()` and scrolls to the related element, + * according to rules specified in + * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). + * + * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. + * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. + * + * @example + + +
+ Go to bottom + You're at the bottom! +
+
+ + function ScrollCtrl($scope, $location, $anchorScroll) { + $scope.gotoBottom = function (){ + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); + + // call $anchorScroll() + $anchorScroll(); + }; + } + + + #scrollArea { + height: 350px; + overflow: auto; + } + + #bottom { + display: block; + margin-top: 2000px; + } + +
+ */ +function $AnchorScrollProvider() { + + var autoScrollingEnabled = true; + + this.disableAutoScrolling = function() { + autoScrollingEnabled = false; + }; + + this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { + var document = $window.document; + + // helper function to get first anchor from a NodeList + // can't use filter.filter, as it accepts only instances of Array + // and IE can't convert NodeList to an array using [].slice + // TODO(vojta): use filter if we change it to accept lists as well + function getFirstAnchor(list) { + var result = null; + forEach(list, function(element) { + if (!result && lowercase(element.nodeName) === 'a') result = element; + }); + return result; + } + + function scroll() { + var hash = $location.hash(), elm; + + // empty hash, scroll to the top of the page + if (!hash) $window.scrollTo(0, 0); + + // element with given id + else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); + + // first anchor with given name :-D + else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); + + // no element and hash == 'top', scroll to the top of the page + else if (hash === 'top') $window.scrollTo(0, 0); + } + + // does not scroll when user clicks on anchor link that is currently on + // (no url change, no $location.hash() change), browser native does scroll + if (autoScrollingEnabled) { + $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, + function autoScrollWatchAction() { + $rootScope.$evalAsync(scroll); + }); + } + + return scroll; + }]; +} + +var $animateMinErr = minErr('$animate'); + +/** + * @ngdoc provider + * @name $animateProvider + * + * @description + * Default implementation of $animate that doesn't perform any animations, instead just + * synchronously performs DOM + * updates and calls done() callbacks. + * + * In order to enable animations the ngAnimate module has to be loaded. + * + * To see the functional implementation check out src/ngAnimate/animate.js + */ +var $AnimateProvider = ['$provide', function($provide) { + + + this.$$selectors = {}; + + + /** + * @ngdoc method + * @name $animateProvider#register + * + * @description + * Registers a new injectable animation factory function. The factory function produces the + * animation object which contains callback functions for each event that is expected to be + * animated. + * + * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` + * must be called once the element animation is complete. If a function is returned then the + * animation service will use this function to cancel the animation whenever a cancel event is + * triggered. + * + * + * ```js + * return { + * eventFn : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction() { + * //code to cancel the animation + * } + * } + * } + * ``` + * + * @param {string} name The name of the animation. + * @param {Function} factory The factory function that will be executed to return the animation + * object. + */ + this.register = function(name, factory) { + var key = name + '-animation'; + if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', + "Expecting class selector starting with '.' got '{0}'.", name); + this.$$selectors[name.substr(1)] = key; + $provide.factory(key, factory); + }; + + /** + * @ngdoc method + * @name $animateProvider#classNameFilter + * + * @description + * Sets and/or returns the CSS class regular expression that is checked when performing + * an animation. Upon bootstrap the classNameFilter value is not set at all and will + * therefore enable $animate to attempt to perform an animation on any element. + * When setting the classNameFilter value, animations will only be performed on elements + * that successfully match the filter expression. This in turn can boost performance + * for low-powered devices as well as applications containing a lot of structural operations. + * @param {RegExp=} expression The className expression which will be checked against all animations + * @return {RegExp} The current CSS className expression value. If null then there is no expression value + */ + this.classNameFilter = function(expression) { + if(arguments.length === 1) { + this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; + } + return this.$$classNameFilter; + }; + + this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) { + + function async(fn) { + fn && $$asyncCallback(fn); + } + + /** + * + * @ngdoc service + * @name $animate + * @description The $animate service provides rudimentary DOM manipulation functions to + * insert, remove and move elements within the DOM, as well as adding and removing classes. + * This service is the core service used by the ngAnimate $animator service which provides + * high-level animation hooks for CSS and JavaScript. + * + * $animate is available in the AngularJS core, however, the ngAnimate module must be included + * to enable full out animation support. Otherwise, $animate will only perform simple DOM + * manipulation operations. + * + * To learn more about enabling animation support, click here to visit the {@link ngAnimate + * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service + * page}. + */ + return { + + /** + * + * @ngdoc method + * @name $animate#enter + * @kind function + * @description Inserts the element into the DOM either after the `after` element or within + * the `parent` element. Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will be inserted into the DOM + * @param {DOMElement} parent the parent element which will append the element as + * a child (if the after element is not present) + * @param {DOMElement} after the sibling element which will append the element + * after itself + * @param {Function=} done callback function that will be called after the element has been + * inserted into the DOM + */ + enter : function(element, parent, after, done) { + if (after) { + after.after(element); + } else { + if (!parent || !parent[0]) { + parent = after.parent(); + } + parent.append(element); + } + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#leave + * @kind function + * @description Removes the element from the DOM. Once complete, the done() callback will be + * fired (if provided). + * @param {DOMElement} element the element which will be removed from the DOM + * @param {Function=} done callback function that will be called after the element has been + * removed from the DOM + */ + leave : function(element, done) { + element.remove(); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#move + * @kind function + * @description Moves the position of the provided element within the DOM to be placed + * either after the `after` element or inside of the `parent` element. Once complete, the + * done() callback will be fired (if provided). + * + * @param {DOMElement} element the element which will be moved around within the + * DOM + * @param {DOMElement} parent the parent element where the element will be + * inserted into (if the after element is not present) + * @param {DOMElement} after the sibling element where the element will be + * positioned next to + * @param {Function=} done the callback function (if provided) that will be fired after the + * element has been moved to its new position + */ + move : function(element, parent, after, done) { + // Do not remove element before insert. Removing will cause data associated with the + // element to be dropped. Insert will implicitly do the remove. + this.enter(element, parent, after, done); + }, + + /** + * + * @ngdoc method + * @name $animate#addClass + * @kind function + * @description Adds the provided className CSS class value to the provided element. Once + * complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will have the className value + * added to it + * @param {string} className the CSS class which will be added to the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * className value has been added to the element + */ + addClass : function(element, className, done) { + className = isString(className) ? + className : + isArray(className) ? className.join(' ') : ''; + forEach(element, function (element) { + jqLiteAddClass(element, className); + }); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#removeClass + * @kind function + * @description Removes the provided className CSS class value from the provided element. + * Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will have the className value + * removed from it + * @param {string} className the CSS class which will be removed from the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * className value has been removed from the element + */ + removeClass : function(element, className, done) { + className = isString(className) ? + className : + isArray(className) ? className.join(' ') : ''; + forEach(element, function (element) { + jqLiteRemoveClass(element, className); + }); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#setClass + * @kind function + * @description Adds and/or removes the given CSS classes to and from the element. + * Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will have its CSS classes changed + * removed from it + * @param {string} add the CSS classes which will be added to the element + * @param {string} remove the CSS class which will be removed from the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * CSS classes have been set on the element + */ + setClass : function(element, add, remove, done) { + forEach(element, function (element) { + jqLiteAddClass(element, add); + jqLiteRemoveClass(element, remove); + }); + async(done); + }, + + enabled : noop + }; + }]; +}]; + +function $$AsyncCallbackProvider(){ + this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { + return $$rAF.supported + ? function(fn) { return $$rAF(fn); } + : function(fn) { + return $timeout(fn, 0, false); + }; + }]; +} + +/** + * ! This is a private undocumented service ! + * + * @name $browser + * @requires $log + * @description + * This object has two goals: + * + * - hide all the global state in the browser caused by the window object + * - abstract away all the browser specific features and inconsistencies + * + * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` + * service, which can be used for convenient testing of the application without the interaction with + * the real browser apis. + */ +/** + * @param {object} window The global window object. + * @param {object} document jQuery wrapped document. + * @param {function()} XHR XMLHttpRequest constructor. + * @param {object} $log console.log or an object with the same interface. + * @param {object} $sniffer $sniffer service + */ +function Browser(window, document, $log, $sniffer) { + var self = this, + rawDocument = document[0], + location = window.location, + history = window.history, + setTimeout = window.setTimeout, + clearTimeout = window.clearTimeout, + pendingDeferIds = {}; + + self.isMock = false; + + var outstandingRequestCount = 0; + var outstandingRequestCallbacks = []; + + // TODO(vojta): remove this temporary api + self.$$completeOutstandingRequest = completeOutstandingRequest; + self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; + + /** + * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` + * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. + */ + function completeOutstandingRequest(fn) { + try { + fn.apply(null, sliceArgs(arguments, 1)); + } finally { + outstandingRequestCount--; + if (outstandingRequestCount === 0) { + while(outstandingRequestCallbacks.length) { + try { + outstandingRequestCallbacks.pop()(); + } catch (e) { + $log.error(e); + } + } + } + } + } + + /** + * @private + * Note: this method is used only by scenario runner + * TODO(vojta): prefix this method with $$ ? + * @param {function()} callback Function that will be called when no outstanding request + */ + self.notifyWhenNoOutstandingRequests = function(callback) { + // force browser to execute all pollFns - this is needed so that cookies and other pollers fire + // at some deterministic time in respect to the test runner's actions. Leaving things up to the + // regular poller would result in flaky tests. + forEach(pollFns, function(pollFn){ pollFn(); }); + + if (outstandingRequestCount === 0) { + callback(); + } else { + outstandingRequestCallbacks.push(callback); + } + }; + + ////////////////////////////////////////////////////////////// + // Poll Watcher API + ////////////////////////////////////////////////////////////// + var pollFns = [], + pollTimeout; + + /** + * @name $browser#addPollFn + * + * @param {function()} fn Poll function to add + * + * @description + * Adds a function to the list of functions that poller periodically executes, + * and starts polling if not started yet. + * + * @returns {function()} the added function + */ + self.addPollFn = function(fn) { + if (isUndefined(pollTimeout)) startPoller(100, setTimeout); + pollFns.push(fn); + return fn; + }; + + /** + * @param {number} interval How often should browser call poll functions (ms) + * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. + * + * @description + * Configures the poller to run in the specified intervals, using the specified + * setTimeout fn and kicks it off. + */ + function startPoller(interval, setTimeout) { + (function check() { + forEach(pollFns, function(pollFn){ pollFn(); }); + pollTimeout = setTimeout(check, interval); + })(); + } + + ////////////////////////////////////////////////////////////// + // URL API + ////////////////////////////////////////////////////////////// + + var lastBrowserUrl = location.href, + baseElement = document.find('base'), + newLocation = null; + + /** + * @name $browser#url + * + * @description + * GETTER: + * Without any argument, this method just returns current value of location.href. + * + * SETTER: + * With at least one argument, this method sets url to new value. + * If html5 history api supported, pushState/replaceState is used, otherwise + * location.href/location.replace is used. + * Returns its own instance to allow chaining + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@link ng.$location $location service} to change url. + * + * @param {string} url New url (when used as setter) + * @param {boolean=} replace Should new url replace current history record ? + */ + self.url = function(url, replace) { + // Android Browser BFCache causes location, history reference to become stale. + if (location !== window.location) location = window.location; + if (history !== window.history) history = window.history; + + // setter + if (url) { + if (lastBrowserUrl == url) return; + lastBrowserUrl = url; + if ($sniffer.history) { + if (replace) history.replaceState(null, '', url); + else { + history.pushState(null, '', url); + // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462 + baseElement.attr('href', baseElement.attr('href')); + } + } else { + newLocation = url; + if (replace) { + location.replace(url); + } else { + location.href = url; + } + } + return self; + // getter + } else { + // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href + // methods not updating location.href synchronously. + // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 + return newLocation || location.href.replace(/%27/g,"'"); + } + }; + + var urlChangeListeners = [], + urlChangeInit = false; + + function fireUrlChange() { + newLocation = null; + if (lastBrowserUrl == self.url()) return; + + lastBrowserUrl = self.url(); + forEach(urlChangeListeners, function(listener) { + listener(self.url()); + }); + } + + /** + * @name $browser#onUrlChange + * + * @description + * Register callback function that will be called, when url changes. + * + * It's only called when the url is changed from outside of angular: + * - user types different url into address bar + * - user clicks on history (forward/back) button + * - user clicks on a link + * + * It's not called when url is changed by $browser.url() method + * + * The listener gets called with new url as parameter. + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@link ng.$location $location service} to monitor url changes in angular apps. + * + * @param {function(string)} listener Listener function to be called when url changes. + * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. + */ + self.onUrlChange = function(callback) { + // TODO(vojta): refactor to use node's syntax for events + if (!urlChangeInit) { + // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) + // don't fire popstate when user change the address bar and don't fire hashchange when url + // changed by push/replaceState + + // html5 history api - popstate event + if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); + // hashchange event + if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); + // polling + else self.addPollFn(fireUrlChange); + + urlChangeInit = true; + } + + urlChangeListeners.push(callback); + return callback; + }; + + ////////////////////////////////////////////////////////////// + // Misc API + ////////////////////////////////////////////////////////////// + + /** + * @name $browser#baseHref + * + * @description + * Returns current + * (always relative - without domain) + * + * @returns {string} The current base href + */ + self.baseHref = function() { + var href = baseElement.attr('href'); + return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; + }; + + ////////////////////////////////////////////////////////////// + // Cookies API + ////////////////////////////////////////////////////////////// + var lastCookies = {}; + var lastCookieString = ''; + var cookiePath = self.baseHref(); + + /** + * @name $browser#cookies + * + * @param {string=} name Cookie name + * @param {string=} value Cookie value + * + * @description + * The cookies method provides a 'private' low level access to browser cookies. + * It is not meant to be used directly, use the $cookie service instead. + * + * The return values vary depending on the arguments that the method was called with as follows: + * + * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify + * it + * - cookies(name, value) -> set name to value, if value is undefined delete the cookie + * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that + * way) + * + * @returns {Object} Hash of all cookies (if called without any parameter) + */ + self.cookies = function(name, value) { + /* global escape: false, unescape: false */ + var cookieLength, cookieArray, cookie, i, index; + + if (name) { + if (value === undefined) { + rawDocument.cookie = escape(name) + "=;path=" + cookiePath + + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; + } else { + if (isString(value)) { + cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + + ';path=' + cookiePath).length + 1; + + // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: + // - 300 cookies + // - 20 cookies per unique domain + // - 4096 bytes per cookie + if (cookieLength > 4096) { + $log.warn("Cookie '"+ name + + "' possibly not set or overflowed because it was too large ("+ + cookieLength + " > 4096 bytes)!"); + } + } + } + } else { + if (rawDocument.cookie !== lastCookieString) { + lastCookieString = rawDocument.cookie; + cookieArray = lastCookieString.split("; "); + lastCookies = {}; + + for (i = 0; i < cookieArray.length; i++) { + cookie = cookieArray[i]; + index = cookie.indexOf('='); + if (index > 0) { //ignore nameless cookies + name = unescape(cookie.substring(0, index)); + // the first value that is seen for a cookie is the most + // specific one. values for the same cookie name that + // follow are for less specific paths. + if (lastCookies[name] === undefined) { + lastCookies[name] = unescape(cookie.substring(index + 1)); + } + } + } + } + return lastCookies; + } + }; + + + /** + * @name $browser#defer + * @param {function()} fn A function, who's execution should be deferred. + * @param {number=} [delay=0] of milliseconds to defer the function execution. + * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. + * + * @description + * Executes a fn asynchronously via `setTimeout(fn, delay)`. + * + * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using + * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed + * via `$browser.defer.flush()`. + * + */ + self.defer = function(fn, delay) { + var timeoutId; + outstandingRequestCount++; + timeoutId = setTimeout(function() { + delete pendingDeferIds[timeoutId]; + completeOutstandingRequest(fn); + }, delay || 0); + pendingDeferIds[timeoutId] = true; + return timeoutId; + }; + + + /** + * @name $browser#defer.cancel + * + * @description + * Cancels a deferred task identified with `deferId`. + * + * @param {*} deferId Token returned by the `$browser.defer` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. + */ + self.defer.cancel = function(deferId) { + if (pendingDeferIds[deferId]) { + delete pendingDeferIds[deferId]; + clearTimeout(deferId); + completeOutstandingRequest(noop); + return true; + } + return false; + }; + +} + +function $BrowserProvider(){ + this.$get = ['$window', '$log', '$sniffer', '$document', + function( $window, $log, $sniffer, $document){ + return new Browser($window, $document, $log, $sniffer); + }]; +} + +/** + * @ngdoc service + * @name $cacheFactory + * + * @description + * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to + * them. + * + * ```js + * + * var cache = $cacheFactory('cacheId'); + * expect($cacheFactory.get('cacheId')).toBe(cache); + * expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined(); + * + * cache.put("key", "value"); + * cache.put("another key", "another value"); + * + * // We've specified no options on creation + * expect(cache.info()).toEqual({id: 'cacheId', size: 2}); + * + * ``` + * + * + * @param {string} cacheId Name or id of the newly created cache. + * @param {object=} options Options object that specifies the cache behavior. Properties: + * + * - `{number=}` `capacity` — turns the cache into LRU cache. + * + * @returns {object} Newly created cache object with the following set of methods: + * + * - `{object}` `info()` — Returns id, size, and options of cache. + * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns + * it. + * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. + * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. + * - `{void}` `removeAll()` — Removes all cached values. + * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. + * + * @example + + +
+ + + + +

Cached Values

+
+ + : + +
+ +

Cache Info

+
+ + : + +
+
+
+ + angular.module('cacheExampleApp', []). + controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) { + $scope.keys = []; + $scope.cache = $cacheFactory('cacheId'); + $scope.put = function(key, value) { + $scope.cache.put(key, value); + $scope.keys.push(key); + }; + }]); + + + p { + margin: 10px 0 3px; + } + +
+ */ +function $CacheFactoryProvider() { + + this.$get = function() { + var caches = {}; + + function cacheFactory(cacheId, options) { + if (cacheId in caches) { + throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); + } + + var size = 0, + stats = extend({}, options, {id: cacheId}), + data = {}, + capacity = (options && options.capacity) || Number.MAX_VALUE, + lruHash = {}, + freshEnd = null, + staleEnd = null; + + /** + * @ngdoc type + * @name $cacheFactory.Cache + * + * @description + * A cache object used to store and retrieve data, primarily used by + * {@link $http $http} and the {@link ng.directive:script script} directive to cache + * templates and other data. + * + * ```js + * angular.module('superCache') + * .factory('superCache', ['$cacheFactory', function($cacheFactory) { + * return $cacheFactory('super-cache'); + * }]); + * ``` + * + * Example test: + * + * ```js + * it('should behave like a cache', inject(function(superCache) { + * superCache.put('key', 'value'); + * superCache.put('another key', 'another value'); + * + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 2 + * }); + * + * superCache.remove('another key'); + * expect(superCache.get('another key')).toBeUndefined(); + * + * superCache.removeAll(); + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 0 + * }); + * })); + * ``` + */ + return caches[cacheId] = { + + /** + * @ngdoc method + * @name $cacheFactory.Cache#put + * @kind function + * + * @description + * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be + * retrieved later, and incrementing the size of the cache if the key was not already + * present in the cache. If behaving like an LRU cache, it will also remove stale + * entries from the set. + * + * It will not insert undefined values into the cache. + * + * @param {string} key the key under which the cached data is stored. + * @param {*} value the value to store alongside the key. If it is undefined, the key + * will not be stored. + * @returns {*} the value stored. + */ + put: function(key, value) { + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); + + refresh(lruEntry); + } + + if (isUndefined(value)) return; + if (!(key in data)) size++; + data[key] = value; + + if (size > capacity) { + this.remove(staleEnd.key); + } + + return value; + }, + + /** + * @ngdoc method + * @name $cacheFactory.Cache#get + * @kind function + * + * @description + * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the data to be retrieved + * @returns {*} the value stored. + */ + get: function(key) { + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; + + if (!lruEntry) return; + + refresh(lruEntry); + } + + return data[key]; + }, + + + /** + * @ngdoc method + * @name $cacheFactory.Cache#remove + * @kind function + * + * @description + * Removes an entry from the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the entry to be removed + */ + remove: function(key) { + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; + + if (!lruEntry) return; + + if (lruEntry == freshEnd) freshEnd = lruEntry.p; + if (lruEntry == staleEnd) staleEnd = lruEntry.n; + link(lruEntry.n,lruEntry.p); + + delete lruHash[key]; + } + + delete data[key]; + size--; + }, + + + /** + * @ngdoc method + * @name $cacheFactory.Cache#removeAll + * @kind function + * + * @description + * Clears the cache object of any entries. + */ + removeAll: function() { + data = {}; + size = 0; + lruHash = {}; + freshEnd = staleEnd = null; + }, + + + /** + * @ngdoc method + * @name $cacheFactory.Cache#destroy + * @kind function + * + * @description + * Destroys the {@link $cacheFactory.Cache Cache} object entirely, + * removing it from the {@link $cacheFactory $cacheFactory} set. + */ + destroy: function() { + data = null; + stats = null; + lruHash = null; + delete caches[cacheId]; + }, + + + /** + * @ngdoc method + * @name $cacheFactory.Cache#info + * @kind function + * + * @description + * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. + * + * @returns {object} an object with the following properties: + *
    + *
  • **id**: the id of the cache instance
  • + *
  • **size**: the number of entries kept in the cache instance
  • + *
  • **...**: any additional properties from the options object when creating the + * cache.
  • + *
+ */ + info: function() { + return extend({}, stats, {size: size}); + } + }; + + + /** + * makes the `entry` the freshEnd of the LRU linked list + */ + function refresh(entry) { + if (entry != freshEnd) { + if (!staleEnd) { + staleEnd = entry; + } else if (staleEnd == entry) { + staleEnd = entry.n; + } + + link(entry.n, entry.p); + link(entry, freshEnd); + freshEnd = entry; + freshEnd.n = null; + } + } + + + /** + * bidirectionally links two entries of the LRU linked list + */ + function link(nextEntry, prevEntry) { + if (nextEntry != prevEntry) { + if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify + if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify + } + } + } + + + /** + * @ngdoc method + * @name $cacheFactory#info + * + * @description + * Get information about all the caches that have been created + * + * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` + */ + cacheFactory.info = function() { + var info = {}; + forEach(caches, function(cache, cacheId) { + info[cacheId] = cache.info(); + }); + return info; + }; + + + /** + * @ngdoc method + * @name $cacheFactory#get + * + * @description + * Get access to a cache object by the `cacheId` used when it was created. + * + * @param {string} cacheId Name or id of a cache to access. + * @returns {object} Cache object identified by the cacheId or undefined if no such cache. + */ + cacheFactory.get = function(cacheId) { + return caches[cacheId]; + }; + + + return cacheFactory; + }; +} + +/** + * @ngdoc service + * @name $templateCache + * + * @description + * The first time a template is used, it is loaded in the template cache for quick retrieval. You + * can load templates directly into the cache in a `script` tag, or by consuming the + * `$templateCache` service directly. + * + * Adding via the `script` tag: + * + * ```html + * + * ``` + * + * **Note:** the `script` tag containing the template does not need to be included in the `head` of + * the document, but it must be below the `ng-app` definition. + * + * Adding via the $templateCache service: + * + * ```js + * var myApp = angular.module('myApp', []); + * myApp.run(function($templateCache) { + * $templateCache.put('templateId.html', 'This is the content of the template'); + * }); + * ``` + * + * To retrieve the template later, simply use it in your HTML: + * ```html + *
+ * ``` + * + * or get it via Javascript: + * ```js + * $templateCache.get('templateId.html') + * ``` + * + * See {@link ng.$cacheFactory $cacheFactory}. + * + */ +function $TemplateCacheProvider() { + this.$get = ['$cacheFactory', function($cacheFactory) { + return $cacheFactory('templates'); + }]; +} + +/* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! + * + * DOM-related variables: + * + * - "node" - DOM Node + * - "element" - DOM Element or Node + * - "$node" or "$element" - jqLite-wrapped node or element + * + * + * Compiler related stuff: + * + * - "linkFn" - linking fn of a single directive + * - "nodeLinkFn" - function that aggregates all linking fns for a particular node + * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node + * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) + */ + + +/** + * @ngdoc service + * @name $compile + * @kind function + * + * @description + * Compiles an HTML string or DOM into a template and produces a template function, which + * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together. + * + * The compilation is a process of walking the DOM tree and matching DOM elements to + * {@link ng.$compileProvider#directive directives}. + * + *
+ * **Note:** This document is an in-depth reference of all directive options. + * For a gentle introduction to directives with examples of common use cases, + * see the {@link guide/directive directive guide}. + *
+ * + * ## Comprehensive Directive API + * + * There are many different options for a directive. + * + * The difference resides in the return value of the factory function. + * You can either return a "Directive Definition Object" (see below) that defines the directive properties, + * or just the `postLink` function (all other properties will have the default values). + * + *
+ * **Best Practice:** It's recommended to use the "directive definition object" form. + *
+ * + * Here's an example directive declared with a Directive Definition Object: + * + * ```js + * var myModule = angular.module(...); + * + * myModule.directive('directiveName', function factory(injectables) { + * var directiveDefinitionObject = { + * priority: 0, + * template: '
', // or // function(tElement, tAttrs) { ... }, + * // or + * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, + * transclude: false, + * restrict: 'A', + * scope: false, + * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, + * controllerAs: 'stringAlias', + * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], + * compile: function compile(tElement, tAttrs, transclude) { + * return { + * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, + * post: function postLink(scope, iElement, iAttrs, controller) { ... } + * } + * // or + * // return function postLink( ... ) { ... } + * }, + * // or + * // link: { + * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, + * // post: function postLink(scope, iElement, iAttrs, controller) { ... } + * // } + * // or + * // link: function postLink( ... ) { ... } + * }; + * return directiveDefinitionObject; + * }); + * ``` + * + *
+ * **Note:** Any unspecified options will use the default value. You can see the default values below. + *
+ * + * Therefore the above can be simplified as: + * + * ```js + * var myModule = angular.module(...); + * + * myModule.directive('directiveName', function factory(injectables) { + * var directiveDefinitionObject = { + * link: function postLink(scope, iElement, iAttrs) { ... } + * }; + * return directiveDefinitionObject; + * // or + * // return function postLink(scope, iElement, iAttrs) { ... } + * }); + * ``` + * + * + * + * ### Directive Definition Object + * + * The directive definition object provides instructions to the {@link ng.$compile + * compiler}. The attributes are: + * + * #### `priority` + * When there are multiple directives defined on a single DOM element, sometimes it + * is necessary to specify the order in which the directives are applied. The `priority` is used + * to sort the directives before their `compile` functions get called. Priority is defined as a + * number. Directives with greater numerical `priority` are compiled first. Pre-link functions + * are also run in priority order, but post-link functions are run in reverse order. The order + * of directives with the same priority is undefined. The default priority is `0`. + * + * #### `terminal` + * If set to true then the current `priority` will be the last set of directives + * which will execute (any directives at the current priority will still execute + * as the order of execution on same `priority` is undefined). + * + * #### `scope` + * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the + * same element request a new scope, only one new scope is created. The new scope rule does not + * apply for the root of the template since the root of the template always gets a new scope. + * + * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from + * normal scope in that it does not prototypically inherit from the parent scope. This is useful + * when creating reusable components, which should not accidentally read or modify data in the + * parent scope. + * + * The 'isolate' scope takes an object hash which defines a set of local scope properties + * derived from the parent scope. These local properties are useful for aliasing values for + * templates. Locals definition is a hash of local scope property to its source: + * + * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is + * always a string since DOM attributes are strings. If no `attr` name is specified then the + * attribute name is assumed to be the same as the local name. + * Given `` and widget definition + * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect + * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the + * `localName` property on the widget scope. The `name` is read from the parent scope (not + * component scope). + * + * * `=` or `=attr` - set up bi-directional binding between a local scope property and the + * parent scope property of name defined via the value of the `attr` attribute. If no `attr` + * name is specified then the attribute name is assumed to be the same as the local name. + * Given `` and widget definition of + * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the + * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected + * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent + * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You + * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. + * + * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. + * If no `attr` name is specified then the attribute name is assumed to be the same as the + * local name. Given `` and widget definition of + * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to + * a function wrapper for the `count = count + value` expression. Often it's desirable to + * pass data from the isolated scope via an expression and to the parent scope, this can be + * done by passing a map of local variable names and values into the expression wrapper fn. + * For example, if the expression is `increment(amount)` then we can specify the amount value + * by calling the `localFn` as `localFn({amount: 22})`. + * + * + * + * #### `controller` + * Controller constructor function. The controller is instantiated before the + * pre-linking phase and it is shared with other directives (see + * `require` attribute). This allows the directives to communicate with each other and augment + * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals: + * + * * `$scope` - Current scope associated with the element + * * `$element` - Current element + * * `$attrs` - Current attributes object for the element + * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope. + * The scope can be overridden by an optional first argument. + * `function([scope], cloneLinkingFn)`. + * + * + * #### `require` + * Require another directive and inject its controller as the fourth argument to the linking function. The + * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the + * injected argument will be an array in corresponding order. If no such directive can be + * found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with: + * + * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. + * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. + * * `^` - Locate the required controller by searching the element's parents. Throw an error if not found. + * * `?^` - Attempt to locate the required controller by searching the element's parents or pass `null` to the + * `link` fn if not found. + * + * + * #### `controllerAs` + * Controller alias at the directive scope. An alias for the controller so it + * can be referenced at the directive template. The directive needs to define a scope for this + * configuration to be used. Useful in the case when directive is used as component. + * + * + * #### `restrict` + * String of subset of `EACM` which restricts the directive to a specific directive + * declaration style. If omitted, the default (attributes only) is used. + * + * * `E` - Element name: `` + * * `A` - Attribute (default): `
` + * * `C` - Class: `
` + * * `M` - Comment: `` + * + * + * #### `template` + * replace the current element with the contents of the HTML. The replacement process + * migrates all of the attributes / classes from the old element to the new one. See the + * {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive + * Directives Guide} for an example. + * + * You can specify `template` as a string representing the template or as a function which takes + * two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and + * returns a string value representing the template. + * + * + * #### `templateUrl` + * Same as `template` but the template is loaded from the specified URL. Because + * the template loading is asynchronous the compilation/linking is suspended until the template + * is loaded. + * + * You can specify `templateUrl` as a string representing the URL or as a function which takes two + * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns + * a string value representing the url. In either case, the template URL is passed through {@link + * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. + * + * + * #### `replace` ([*DEPRECATED*!], will be removed in next major release) + * specify where the template should be inserted. Defaults to `false`. + * + * * `true` - the template will replace the current element. + * * `false` - the template will replace the contents of the current element. + * + * + * #### `transclude` + * compile the content of the element and make it available to the directive. + * Typically used with {@link ng.directive:ngTransclude + * ngTransclude}. The advantage of transclusion is that the linking function receives a + * transclusion function which is pre-bound to the correct scope. In a typical setup the widget + * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate` + * scope. This makes it possible for the widget to have private state, and the transclusion to + * be bound to the parent (pre-`isolate`) scope. + * + * * `true` - transclude the content of the directive. + * * `'element'` - transclude the whole element including any directives defined at lower priority. + * + * + * #### `compile` + * + * ```js + * function compile(tElement, tAttrs, transclude) { ... } + * ``` + * + * The compile function deals with transforming the template DOM. Since most directives do not do + * template transformation, it is not used often. The compile function takes the following arguments: + * + * * `tElement` - template element - The element where the directive has been declared. It is + * safe to do template transformation on the element and child elements only. + * + * * `tAttrs` - template attributes - Normalized list of attributes declared on this element shared + * between all directive compile functions. + * + * * `transclude` - [*DEPRECATED*!] A transclude linking function: `function(scope, cloneLinkingFn)` + * + *
+ * **Note:** The template instance and the link instance may be different objects if the template has + * been cloned. For this reason it is **not** safe to do anything other than DOM transformations that + * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration + * should be done in a linking function rather than in a compile function. + *
+ + *
+ * **Note:** The compile function cannot handle directives that recursively use themselves in their + * own templates or compile functions. Compiling these directives results in an infinite loop and a + * stack overflow errors. + * + * This can be avoided by manually using $compile in the postLink function to imperatively compile + * a directive's template instead of relying on automatic template compilation via `template` or + * `templateUrl` declaration or manual compilation inside the compile function. + *
+ * + *
+ * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it + * e.g. does not know about the right outer scope. Please use the transclude function that is passed + * to the link function instead. + *
+ + * A compile function can have a return value which can be either a function or an object. + * + * * returning a (post-link) function - is equivalent to registering the linking function via the + * `link` property of the config object when the compile function is empty. + * + * * returning an object with function(s) registered via `pre` and `post` properties - allows you to + * control when a linking function should be called during the linking phase. See info about + * pre-linking and post-linking functions below. + * + * + * #### `link` + * This property is used only if the `compile` property is not defined. + * + * ```js + * function link(scope, iElement, iAttrs, controller, transcludeFn) { ... } + * ``` + * + * The link function is responsible for registering DOM listeners as well as updating the DOM. It is + * executed after the template has been cloned. This is where most of the directive logic will be + * put. + * + * * `scope` - {@link ng.$rootScope.Scope Scope} - The scope to be used by the + * directive for registering {@link ng.$rootScope.Scope#$watch watches}. + * + * * `iElement` - instance element - The element where the directive is to be used. It is safe to + * manipulate the children of the element only in `postLink` function since the children have + * already been linked. + * + * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared + * between all directive linking functions. + * + * * `controller` - a controller instance - A controller instance if at least one directive on the + * element defines a controller. The controller is shared among all the directives, which allows + * the directives to use the controllers as a communication channel. + * + * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. + * The scope can be overridden by an optional first argument. This is the same as the `$transclude` + * parameter of directive controllers. + * `function([scope], cloneLinkingFn)`. + * + * + * #### Pre-linking function + * + * Executed before the child elements are linked. Not safe to do DOM transformation since the + * compiler linking function will fail to locate the correct elements for linking. + * + * #### Post-linking function + * + * Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function. + * + * + * ### Attributes + * + * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the + * `link()` or `compile()` functions. It has a variety of uses. + * + * accessing *Normalized attribute names:* + * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. + * the attributes object allows for normalized access to + * the attributes. + * + * * *Directive inter-communication:* All directives share the same instance of the attributes + * object which allows the directives to use the attributes object as inter directive + * communication. + * + * * *Supports interpolation:* Interpolation attributes are assigned to the attribute object + * allowing other directives to read the interpolated value. + * + * * *Observing interpolated attributes:* Use `$observe` to observe the value changes of attributes + * that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also + * the only way to easily get the actual value because during the linking phase the interpolation + * hasn't been evaluated yet and so the value is at this time set to `undefined`. + * + * ```js + * function linkingFn(scope, elm, attrs, ctrl) { + * // get the attribute value + * console.log(attrs.ngModel); + * + * // change the attribute + * attrs.$set('ngModel', 'new value'); + * + * // observe changes to interpolated attribute + * attrs.$observe('ngModel', function(value) { + * console.log('ngModel has changed value to ' + value); + * }); + * } + * ``` + * + * Below is an example using `$compileProvider`. + * + *
+ * **Note**: Typically directives are registered with `module.directive`. The example below is + * to illustrate how `$compile` works. + *
+ * + + + +
+
+
+
+
+
+ + it('should auto compile', function() { + var textarea = $('textarea'); + var output = $('div[compile]'); + // The initial state reads 'Hello Angular'. + expect(output.getText()).toBe('Hello Angular'); + textarea.clear(); + textarea.sendKeys('{{name}}!'); + expect(output.getText()).toBe('Angular!'); + }); + +
+ + * + * + * @param {string|DOMElement} element Element or HTML string to compile into a template function. + * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives. + * @param {number} maxPriority only apply directives lower than given priority (Only effects the + * root element(s), not their children) + * @returns {function(scope, cloneAttachFn=)} a link function which is used to bind template + * (a DOM element/tree) to a scope. Where: + * + * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. + * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the + * `template` and call the `cloneAttachFn` function allowing the caller to attach the + * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is + * called as:
`cloneAttachFn(clonedElement, scope)` where: + * + * * `clonedElement` - is a clone of the original `element` passed into the compiler. + * * `scope` - is the current scope with which the linking function is working with. + * + * Calling the linking function returns the element of the template. It is either the original + * element passed in, or the clone of the element if the `cloneAttachFn` is provided. + * + * After linking the view is not updated until after a call to $digest which typically is done by + * Angular automatically. + * + * If you need access to the bound view, there are two ways to do it: + * + * - If you are not asking the linking function to clone the template, create the DOM element(s) + * before you send them to the compiler and keep this reference around. + * ```js + * var element = $compile('

{{total}}

')(scope); + * ``` + * + * - if on the other hand, you need the element to be cloned, the view reference from the original + * example would not point to the clone, but rather to the original template that was cloned. In + * this case, you can access the clone via the cloneAttachFn: + * ```js + * var templateElement = angular.element('

{{total}}

'), + * scope = ....; + * + * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { + * //attach the clone to DOM document at the right place + * }); + * + * //now we have reference to the cloned DOM via `clonedElement` + * ``` + * + * + * For information on how the compiler works, see the + * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. + */ + +var $compileMinErr = minErr('$compile'); + +/** + * @ngdoc provider + * @name $compileProvider + * @kind function + * + * @description + */ +$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; +function $CompileProvider($provide, $$sanitizeUriProvider) { + var hasDirectives = {}, + Suffix = 'Directive', + COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/; + + // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes + // The assumption is that future DOM event attribute names will begin with + // 'on' and be composed of only English letters. + var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; + + /** + * @ngdoc method + * @name $compileProvider#directive + * @kind function + * + * @description + * Register a new directive with the compiler. + * + * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which + * will match as ng-bind), or an object map of directives where the keys are the + * names and the values are the factories. + * @param {Function|Array} directiveFactory An injectable directive factory function. See + * {@link guide/directive} for more info. + * @returns {ng.$compileProvider} Self for chaining. + */ + this.directive = function registerDirective(name, directiveFactory) { + assertNotHasOwnProperty(name, 'directive'); + if (isString(name)) { + assertArg(directiveFactory, 'directiveFactory'); + if (!hasDirectives.hasOwnProperty(name)) { + hasDirectives[name] = []; + $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', + function($injector, $exceptionHandler) { + var directives = []; + forEach(hasDirectives[name], function(directiveFactory, index) { + try { + var directive = $injector.invoke(directiveFactory); + if (isFunction(directive)) { + directive = { compile: valueFn(directive) }; + } else if (!directive.compile && directive.link) { + directive.compile = valueFn(directive.link); + } + directive.priority = directive.priority || 0; + directive.index = index; + directive.name = directive.name || name; + directive.require = directive.require || (directive.controller && directive.name); + directive.restrict = directive.restrict || 'A'; + directives.push(directive); + } catch (e) { + $exceptionHandler(e); + } + }); + return directives; + }]); + } + hasDirectives[name].push(directiveFactory); + } else { + forEach(name, reverseParams(registerDirective)); + } + return this; + }; + + + /** + * @ngdoc method + * @name $compileProvider#aHrefSanitizationWhitelist + * @kind function + * + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during a[href] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to a[href] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.aHrefSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); + return this; + } else { + return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); + } + }; + + + /** + * @ngdoc method + * @name $compileProvider#imgSrcSanitizationWhitelist + * @kind function + * + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during img[src] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to img[src] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.imgSrcSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); + return this; + } else { + return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); + } + }; + + this.$get = [ + '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', + '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', + function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, + $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { + + var Attributes = function(element, attr) { + this.$$element = element; + this.$attr = attr || {}; + }; + + Attributes.prototype = { + $normalize: directiveNormalize, + + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$addClass + * @kind function + * + * @description + * Adds the CSS class value specified by the classVal parameter to the element. If animations + * are enabled then an animation will be triggered for the class addition. + * + * @param {string} classVal The className value that will be added to the element + */ + $addClass : function(classVal) { + if(classVal && classVal.length > 0) { + $animate.addClass(this.$$element, classVal); + } + }, + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$removeClass + * @kind function + * + * @description + * Removes the CSS class value specified by the classVal parameter from the element. If + * animations are enabled then an animation will be triggered for the class removal. + * + * @param {string} classVal The className value that will be removed from the element + */ + $removeClass : function(classVal) { + if(classVal && classVal.length > 0) { + $animate.removeClass(this.$$element, classVal); + } + }, + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$updateClass + * @kind function + * + * @description + * Adds and removes the appropriate CSS class values to the element based on the difference + * between the new and old CSS class values (specified as newClasses and oldClasses). + * + * @param {string} newClasses The current CSS className value + * @param {string} oldClasses The former CSS className value + */ + $updateClass : function(newClasses, oldClasses) { + var toAdd = tokenDifference(newClasses, oldClasses); + var toRemove = tokenDifference(oldClasses, newClasses); + + if(toAdd.length === 0) { + $animate.removeClass(this.$$element, toRemove); + } else if(toRemove.length === 0) { + $animate.addClass(this.$$element, toAdd); + } else { + $animate.setClass(this.$$element, toAdd, toRemove); + } + }, + + /** + * Set a normalized attribute on the element in a way such that all directives + * can share the attribute. This function properly handles boolean attributes. + * @param {string} key Normalized key. (ie ngAttribute) + * @param {string|boolean} value The value to set. If `null` attribute will be deleted. + * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. + * Defaults to true. + * @param {string=} attrName Optional none normalized name. Defaults to key. + */ + $set: function(key, value, writeAttr, attrName) { + // TODO: decide whether or not to throw an error if "class" + //is set through this function since it may cause $updateClass to + //become unstable. + + var booleanKey = getBooleanAttrName(this.$$element[0], key), + normalizedVal, + nodeName; + + if (booleanKey) { + this.$$element.prop(key, value); + attrName = booleanKey; + } + + this[key] = value; + + // translate normalized key to actual key + if (attrName) { + this.$attr[key] = attrName; + } else { + attrName = this.$attr[key]; + if (!attrName) { + this.$attr[key] = attrName = snake_case(key, '-'); + } + } + + nodeName = nodeName_(this.$$element); + + // sanitize a[href] and img[src] values + if ((nodeName === 'A' && key === 'href') || + (nodeName === 'IMG' && key === 'src')) { + this[key] = value = $$sanitizeUri(value, key === 'src'); + } + + if (writeAttr !== false) { + if (value === null || value === undefined) { + this.$$element.removeAttr(attrName); + } else { + this.$$element.attr(attrName, value); + } + } + + // fire observers + var $$observers = this.$$observers; + $$observers && forEach($$observers[key], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); + }, + + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$observe + * @kind function + * + * @description + * Observes an interpolated attribute. + * + * The observer function will be invoked once during the next `$digest` following + * compilation. The observer is then invoked whenever the interpolated value + * changes. + * + * @param {string} key Normalized key. (ie ngAttribute) . + * @param {function(interpolatedValue)} fn Function that will be called whenever + the interpolated value of the attribute changes. + * See the {@link guide/directive#Attributes Directives} guide for more info. + * @returns {function()} the `fn` parameter. + */ + $observe: function(key, fn) { + var attrs = this, + $$observers = (attrs.$$observers || (attrs.$$observers = {})), + listeners = ($$observers[key] || ($$observers[key] = [])); + + listeners.push(fn); + $rootScope.$evalAsync(function() { + if (!listeners.$$inter) { + // no one registered attribute interpolation function, so lets call it manually + fn(attrs[key]); + } + }); + return fn; + } + }; + + var startSymbol = $interpolate.startSymbol(), + endSymbol = $interpolate.endSymbol(), + denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') + ? identity + : function denormalizeTemplate(template) { + return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); + }, + NG_ATTR_BINDING = /^ngAttr[A-Z]/; + + + return compile; + + //================================ + + function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, + previousCompileContext) { + if (!($compileNodes instanceof jqLite)) { + // jquery always rewraps, whereas we need to preserve the original selector so that we can + // modify it. + $compileNodes = jqLite($compileNodes); + } + // We can not compile top level text elements since text nodes can be merged and we will + // not be able to attach scope data to them, so we will wrap them in + forEach($compileNodes, function(node, index){ + if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { + $compileNodes[index] = node = jqLite(node).wrap('').parent()[0]; + } + }); + var compositeLinkFn = + compileNodes($compileNodes, transcludeFn, $compileNodes, + maxPriority, ignoreDirective, previousCompileContext); + safeAddClass($compileNodes, 'ng-scope'); + return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){ + assertArg(scope, 'scope'); + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart + // and sometimes changes the structure of the DOM. + var $linkNode = cloneConnectFn + ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! + : $compileNodes; + + forEach(transcludeControllers, function(instance, name) { + $linkNode.data('$' + name + 'Controller', instance); + }); + + // Attach scope only to non-text nodes. + for(var i = 0, ii = $linkNode.length; i + addDirective(directives, + directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); + + // iterate over the attributes + for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, + j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { + var attrStartName = false; + var attrEndName = false; + + attr = nAttrs[j]; + if (!msie || msie >= 8 || attr.specified) { + name = attr.name; + // support ngAttr attribute binding + ngAttrName = directiveNormalize(name); + if (NG_ATTR_BINDING.test(ngAttrName)) { + name = snake_case(ngAttrName.substr(6), '-'); + } + + var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); + if (ngAttrName === directiveNName + 'Start') { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); + } + + nName = directiveNormalize(name.toLowerCase()); + attrsMap[nName] = name; + attrs[nName] = value = trim(attr.value); + if (getBooleanAttrName(node, nName)) { + attrs[nName] = true; // presence means true + } + addAttrInterpolateDirective(node, directives, value, nName); + addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, + attrEndName); + } + } + + // use class as directive + className = node.className; + if (isString(className) && className !== '') { + while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { + nName = directiveNormalize(match[2]); + if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { + attrs[nName] = trim(match[3]); + } + className = className.substr(match.index + match[0].length); + } + } + break; + case 3: /* Text Node */ + addTextInterpolateDirective(directives, node.nodeValue); + break; + case 8: /* Comment */ + try { + match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); + if (match) { + nName = directiveNormalize(match[1]); + if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { + attrs[nName] = trim(match[2]); + } + } + } catch (e) { + // turns out that under some circumstances IE9 throws errors when one attempts to read + // comment's node value. + // Just ignore it and continue. (Can't seem to reproduce in test case.) + } + break; + } + + directives.sort(byPriority); + return directives; + } + + /** + * Given a node with an directive-start it collects all of the siblings until it finds + * directive-end. + * @param node + * @param attrStart + * @param attrEnd + * @returns {*} + */ + function groupScan(node, attrStart, attrEnd) { + var nodes = []; + var depth = 0; + if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { + var startNode = node; + do { + if (!node) { + throw $compileMinErr('uterdir', + "Unterminated attribute, found '{0}' but no matching '{1}' found.", + attrStart, attrEnd); + } + if (node.nodeType == 1 /** Element **/) { + if (node.hasAttribute(attrStart)) depth++; + if (node.hasAttribute(attrEnd)) depth--; + } + nodes.push(node); + node = node.nextSibling; + } while (depth > 0); + } else { + nodes.push(node); + } + + return jqLite(nodes); + } + + /** + * Wrapper for linking function which converts normal linking function into a grouped + * linking function. + * @param linkFn + * @param attrStart + * @param attrEnd + * @returns {Function} + */ + function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { + return function(scope, element, attrs, controllers, transcludeFn) { + element = groupScan(element[0], attrStart, attrEnd); + return linkFn(scope, element, attrs, controllers, transcludeFn); + }; + } + + /** + * Once the directives have been collected, their compile functions are executed. This method + * is responsible for inlining directive templates as well as terminating the application + * of the directives if the terminal directive has been reached. + * + * @param {Array} directives Array of collected directives to execute their compile function. + * this needs to be pre-sorted by priority order. + * @param {Node} compileNode The raw DOM node to apply the compile functions to + * @param {Object} templateAttrs The shared attribute function + * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the + * scope argument is auto-generated to the new + * child of the transcluded parent scope. + * @param {JQLite} jqCollection If we are working on the root of the compile tree then this + * argument has the root jqLite array so that we can replace nodes + * on it. + * @param {Object=} originalReplaceDirective An optional directive that will be ignored when + * compiling the transclusion. + * @param {Array.} preLinkFns + * @param {Array.} postLinkFns + * @param {Object} previousCompileContext Context used for previous compilation of the current + * node + * @returns {Function} linkFn + */ + function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, + jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, + previousCompileContext) { + previousCompileContext = previousCompileContext || {}; + + var terminalPriority = -Number.MAX_VALUE, + newScopeDirective, + controllerDirectives = previousCompileContext.controllerDirectives, + newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, + templateDirective = previousCompileContext.templateDirective, + nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, + hasTranscludeDirective = false, + hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, + $compileNode = templateAttrs.$$element = jqLite(compileNode), + directive, + directiveName, + $template, + replaceDirective = originalReplaceDirective, + childTranscludeFn = transcludeFn, + linkFn, + directiveValue; + + // executes all directives on the current element + for(var i = 0, ii = directives.length; i < ii; i++) { + directive = directives[i]; + var attrStart = directive.$$start; + var attrEnd = directive.$$end; + + // collect multiblock sections + if (attrStart) { + $compileNode = groupScan(compileNode, attrStart, attrEnd); + } + $template = undefined; + + if (terminalPriority > directive.priority) { + break; // prevent further processing of directives + } + + if (directiveValue = directive.scope) { + newScopeDirective = newScopeDirective || directive; + + // skip the check for directives with async templates, we'll check the derived sync + // directive when the template arrives + if (!directive.templateUrl) { + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, + $compileNode); + if (isObject(directiveValue)) { + newIsolateScopeDirective = directive; + } + } + } + + directiveName = directive.name; + + if (!directive.templateUrl && directive.controller) { + directiveValue = directive.controller; + controllerDirectives = controllerDirectives || {}; + assertNoDuplicate("'" + directiveName + "' controller", + controllerDirectives[directiveName], directive, $compileNode); + controllerDirectives[directiveName] = directive; + } + + if (directiveValue = directive.transclude) { + hasTranscludeDirective = true; + + // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. + // This option should only be used by directives that know how to safely handle element transclusion, + // where the transcluded nodes are added or replaced after linking. + if (!directive.$$tlb) { + assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode); + nonTlbTranscludeDirective = directive; + } + + if (directiveValue == 'element') { + hasElementTranscludeDirective = true; + terminalPriority = directive.priority; + $template = groupScan(compileNode, attrStart, attrEnd); + $compileNode = templateAttrs.$$element = + jqLite(document.createComment(' ' + directiveName + ': ' + + templateAttrs[directiveName] + ' ')); + compileNode = $compileNode[0]; + replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); + + childTranscludeFn = compile($template, transcludeFn, terminalPriority, + replaceDirective && replaceDirective.name, { + // Don't pass in: + // - controllerDirectives - otherwise we'll create duplicates controllers + // - newIsolateScopeDirective or templateDirective - combining templates with + // element transclusion doesn't make sense. + // + // We need only nonTlbTranscludeDirective so that we prevent putting transclusion + // on the same element more than once. + nonTlbTranscludeDirective: nonTlbTranscludeDirective + }); + } else { + $template = jqLite(jqLiteClone(compileNode)).contents(); + $compileNode.empty(); // clear contents + childTranscludeFn = compile($template, transcludeFn); + } + } + + if (directive.template) { + assertNoDuplicate('template', templateDirective, directive, $compileNode); + templateDirective = directive; + + directiveValue = (isFunction(directive.template)) + ? directive.template($compileNode, templateAttrs) + : directive.template; + + directiveValue = denormalizeTemplate(directiveValue); + + if (directive.replace) { + replaceDirective = directive; + if (jqLiteIsTextNode(directiveValue)) { + $template = []; + } else { + $template = jqLite(trim(directiveValue)); + } + compileNode = $template[0]; + + if ($template.length != 1 || compileNode.nodeType !== 1) { + throw $compileMinErr('tplrt', + "Template for directive '{0}' must have exactly one root element. {1}", + directiveName, ''); + } + + replaceWith(jqCollection, $compileNode, compileNode); + + var newTemplateAttrs = {$attr: {}}; + + // combine directives from the original node and from the template: + // - take the array of directives for this element + // - split it into two parts, those that already applied (processed) and those that weren't (unprocessed) + // - collect directives from the template and sort them by priority + // - combine directives as: processed + template + unprocessed + var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); + var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); + + if (newIsolateScopeDirective) { + markDirectivesAsIsolate(templateDirectives); + } + directives = directives.concat(templateDirectives).concat(unprocessedDirectives); + mergeTemplateAttributes(templateAttrs, newTemplateAttrs); + + ii = directives.length; + } else { + $compileNode.html(directiveValue); + } + } + + if (directive.templateUrl) { + assertNoDuplicate('template', templateDirective, directive, $compileNode); + templateDirective = directive; + + if (directive.replace) { + replaceDirective = directive; + } + + nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, + templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, { + controllerDirectives: controllerDirectives, + newIsolateScopeDirective: newIsolateScopeDirective, + templateDirective: templateDirective, + nonTlbTranscludeDirective: nonTlbTranscludeDirective + }); + ii = directives.length; + } else if (directive.compile) { + try { + linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); + if (isFunction(linkFn)) { + addLinkFns(null, linkFn, attrStart, attrEnd); + } else if (linkFn) { + addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); + } + } catch (e) { + $exceptionHandler(e, startingTag($compileNode)); + } + } + + if (directive.terminal) { + nodeLinkFn.terminal = true; + terminalPriority = Math.max(terminalPriority, directive.priority); + } + + } + + nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; + nodeLinkFn.transclude = hasTranscludeDirective && childTranscludeFn; + previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; + + // might be normal or delayed nodeLinkFn depending on if templateUrl is present + return nodeLinkFn; + + //////////////////// + + function addLinkFns(pre, post, attrStart, attrEnd) { + if (pre) { + if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); + pre.require = directive.require; + pre.directiveName = directiveName; + if (newIsolateScopeDirective === directive || directive.$$isolateScope) { + pre = cloneAndAnnotateFn(pre, {isolateScope: true}); + } + preLinkFns.push(pre); + } + if (post) { + if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); + post.require = directive.require; + post.directiveName = directiveName; + if (newIsolateScopeDirective === directive || directive.$$isolateScope) { + post = cloneAndAnnotateFn(post, {isolateScope: true}); + } + postLinkFns.push(post); + } + } + + + function getControllers(directiveName, require, $element, elementControllers) { + var value, retrievalMethod = 'data', optional = false; + if (isString(require)) { + while((value = require.charAt(0)) == '^' || value == '?') { + require = require.substr(1); + if (value == '^') { + retrievalMethod = 'inheritedData'; + } + optional = optional || value == '?'; + } + value = null; + + if (elementControllers && retrievalMethod === 'data') { + value = elementControllers[require]; + } + value = value || $element[retrievalMethod]('$' + require + 'Controller'); + + if (!value && !optional) { + throw $compileMinErr('ctreq', + "Controller '{0}', required by directive '{1}', can't be found!", + require, directiveName); + } + return value; + } else if (isArray(require)) { + value = []; + forEach(require, function(require) { + value.push(getControllers(directiveName, require, $element, elementControllers)); + }); + } + return value; + } + + + function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { + var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; + + if (compileNode === linkNode) { + attrs = templateAttrs; + } else { + attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); + } + $element = attrs.$$element; + + if (newIsolateScopeDirective) { + var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; + var $linkNode = jqLite(linkNode); + + isolateScope = scope.$new(true); + + if (templateDirective && (templateDirective === newIsolateScopeDirective || + templateDirective === newIsolateScopeDirective.$$originalDirective)) { + $linkNode.data('$isolateScope', isolateScope) ; + } else { + $linkNode.data('$isolateScopeNoTemplate', isolateScope); + } + + + + safeAddClass($linkNode, 'ng-isolate-scope'); + + forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { + var match = definition.match(LOCAL_REGEXP) || [], + attrName = match[3] || scopeName, + optional = (match[2] == '?'), + mode = match[1], // @, =, or & + lastValue, + parentGet, parentSet, compare; + + isolateScope.$$isolateBindings[scopeName] = mode + attrName; + + switch (mode) { + + case '@': + attrs.$observe(attrName, function(value) { + isolateScope[scopeName] = value; + }); + attrs.$$observers[attrName].$$scope = scope; + if( attrs[attrName] ) { + // If the attribute has been provided then we trigger an interpolation to ensure + // the value is there for use in the link fn + isolateScope[scopeName] = $interpolate(attrs[attrName])(scope); + } + break; + + case '=': + if (optional && !attrs[attrName]) { + return; + } + parentGet = $parse(attrs[attrName]); + if (parentGet.literal) { + compare = equals; + } else { + compare = function(a,b) { return a === b; }; + } + parentSet = parentGet.assign || function() { + // reset the change, or we will throw this exception on every $digest + lastValue = isolateScope[scopeName] = parentGet(scope); + throw $compileMinErr('nonassign', + "Expression '{0}' used with directive '{1}' is non-assignable!", + attrs[attrName], newIsolateScopeDirective.name); + }; + lastValue = isolateScope[scopeName] = parentGet(scope); + isolateScope.$watch(function parentValueWatch() { + var parentValue = parentGet(scope); + if (!compare(parentValue, isolateScope[scopeName])) { + // we are out of sync and need to copy + if (!compare(parentValue, lastValue)) { + // parent changed and it has precedence + isolateScope[scopeName] = parentValue; + } else { + // if the parent can be assigned then do so + parentSet(scope, parentValue = isolateScope[scopeName]); + } + } + return lastValue = parentValue; + }, null, parentGet.literal); + break; + + case '&': + parentGet = $parse(attrs[attrName]); + isolateScope[scopeName] = function(locals) { + return parentGet(scope, locals); + }; + break; + + default: + throw $compileMinErr('iscp', + "Invalid isolate scope definition for directive '{0}'." + + " Definition: {... {1}: '{2}' ...}", + newIsolateScopeDirective.name, scopeName, definition); + } + }); + } + transcludeFn = boundTranscludeFn && controllersBoundTransclude; + if (controllerDirectives) { + forEach(controllerDirectives, function(directive) { + var locals = { + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, + $element: $element, + $attrs: attrs, + $transclude: transcludeFn + }, controllerInstance; + + controller = directive.controller; + if (controller == '@') { + controller = attrs[directive.name]; + } + + controllerInstance = $controller(controller, locals); + // For directives with element transclusion the element is a comment, + // but jQuery .data doesn't support attaching data to comment nodes as it's hard to + // clean up (http://bugs.jquery.com/ticket/8335). + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + if (!hasElementTranscludeDirective) { + $element.data('$' + directive.name + 'Controller', controllerInstance); + } + + if (directive.controllerAs) { + locals.$scope[directive.controllerAs] = controllerInstance; + } + }); + } + + // PRELINKING + for(i = 0, ii = preLinkFns.length; i < ii; i++) { + try { + linkFn = preLinkFns[i]; + linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); + } catch (e) { + $exceptionHandler(e, startingTag($element)); + } + } + + // RECURSION + // We only pass the isolate scope, if the isolate directive has a template, + // otherwise the child elements do not belong to the isolate directive. + var scopeToChild = scope; + if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { + scopeToChild = isolateScope; + } + childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + + // POSTLINKING + for(i = postLinkFns.length - 1; i >= 0; i--) { + try { + linkFn = postLinkFns[i]; + linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); + } catch (e) { + $exceptionHandler(e, startingTag($element)); + } + } + + // This is the function that is injected as `$transclude`. + function controllersBoundTransclude(scope, cloneAttachFn) { + var transcludeControllers; + + // no scope passed + if (arguments.length < 2) { + cloneAttachFn = scope; + scope = undefined; + } + + if (hasElementTranscludeDirective) { + transcludeControllers = elementControllers; + } + + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); + } + } + } + + function markDirectivesAsIsolate(directives) { + // mark all directives as needing isolate scope. + for (var j = 0, jj = directives.length; j < jj; j++) { + directives[j] = inherit(directives[j], {$$isolateScope: true}); + } + } + + /** + * looks up the directive and decorates it with exception handling and proper parameters. We + * call this the boundDirective. + * + * @param {string} name name of the directive to look up. + * @param {string} location The directive must be found in specific format. + * String containing any of theses characters: + * + * * `E`: element name + * * `A': attribute + * * `C`: class + * * `M`: comment + * @returns {boolean} true if directive was added. + */ + function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, + endAttrName) { + if (name === ignoreDirective) return null; + var match = null; + if (hasDirectives.hasOwnProperty(name)) { + for(var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i directive.priority) && + directive.restrict.indexOf(location) != -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + } + tDirectives.push(directive); + match = directive; + } + } catch(e) { $exceptionHandler(e); } + } + } + return match; + } + + + /** + * When the element is replaced with HTML template then the new attributes + * on the template need to be merged with the existing attributes in the DOM. + * The desired effect is to have both of the attributes present. + * + * @param {object} dst destination attributes (original DOM) + * @param {object} src source attributes (from the directive template) + */ + function mergeTemplateAttributes(dst, src) { + var srcAttr = src.$attr, + dstAttr = dst.$attr, + $element = dst.$$element; + + // reapply the old attributes to the new element + forEach(dst, function(value, key) { + if (key.charAt(0) != '$') { + if (src[key] && src[key] !== value) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } + dst.$set(key, value, true, srcAttr[key]); + } + }); + + // copy the new attributes on the old attrs object + forEach(src, function(value, key) { + if (key == 'class') { + safeAddClass($element, value); + dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; + } else if (key == 'style') { + $element.attr('style', $element.attr('style') + ';' + value); + dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; + // `dst` will never contain hasOwnProperty as DOM parser won't let it. + // You will get an "InvalidCharacterError: DOM Exception 5" error if you + // have an attribute like "has-own-property" or "data-has-own-property", etc. + } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { + dst[key] = value; + dstAttr[key] = srcAttr[key]; + } + }); + } + + + function compileTemplateUrl(directives, $compileNode, tAttrs, + $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { + var linkQueue = [], + afterTemplateNodeLinkFn, + afterTemplateChildLinkFn, + beforeTemplateCompileNode = $compileNode[0], + origAsyncDirective = directives.shift(), + // The fact that we have to copy and patch the directive seems wrong! + derivedSyncDirective = extend({}, origAsyncDirective, { + templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective + }), + templateUrl = (isFunction(origAsyncDirective.templateUrl)) + ? origAsyncDirective.templateUrl($compileNode, tAttrs) + : origAsyncDirective.templateUrl; + + $compileNode.empty(); + + $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). + success(function(content) { + var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; + + content = denormalizeTemplate(content); + + if (origAsyncDirective.replace) { + if (jqLiteIsTextNode(content)) { + $template = []; + } else { + $template = jqLite(trim(content)); + } + compileNode = $template[0]; + + if ($template.length != 1 || compileNode.nodeType !== 1) { + throw $compileMinErr('tplrt', + "Template for directive '{0}' must have exactly one root element. {1}", + origAsyncDirective.name, templateUrl); + } + + tempTemplateAttrs = {$attr: {}}; + replaceWith($rootElement, $compileNode, compileNode); + var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); + + if (isObject(origAsyncDirective.scope)) { + markDirectivesAsIsolate(templateDirectives); + } + directives = templateDirectives.concat(directives); + mergeTemplateAttributes(tAttrs, tempTemplateAttrs); + } else { + compileNode = beforeTemplateCompileNode; + $compileNode.html(content); + } + + directives.unshift(derivedSyncDirective); + + afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, + childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, + previousCompileContext); + forEach($rootElement, function(node, i) { + if (node == compileNode) { + $rootElement[i] = $compileNode[0]; + } + }); + afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); + + + while(linkQueue.length) { + var scope = linkQueue.shift(), + beforeTemplateLinkNode = linkQueue.shift(), + linkRootElement = linkQueue.shift(), + boundTranscludeFn = linkQueue.shift(), + linkNode = $compileNode[0]; + + if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { + var oldClasses = beforeTemplateLinkNode.className; + + if (!(previousCompileContext.hasElementTranscludeDirective && + origAsyncDirective.replace)) { + // it was cloned therefore we have to clone as well. + linkNode = jqLiteClone(compileNode); + } + + replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); + + // Copy in CSS classes from original node + safeAddClass(jqLite(linkNode), oldClasses); + } + if (afterTemplateNodeLinkFn.transclude) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude); + } else { + childBoundTranscludeFn = boundTranscludeFn; + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, + childBoundTranscludeFn); + } + linkQueue = null; + }). + error(function(response, code, headers, config) { + throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url); + }); + + return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { + if (linkQueue) { + linkQueue.push(scope); + linkQueue.push(node); + linkQueue.push(rootElement); + linkQueue.push(boundTranscludeFn); + } else { + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, boundTranscludeFn); + } + }; + } + + + /** + * Sorting function for bound directives. + */ + function byPriority(a, b) { + var diff = b.priority - a.priority; + if (diff !== 0) return diff; + if (a.name !== b.name) return (a.name < b.name) ? -1 : 1; + return a.index - b.index; + } + + + function assertNoDuplicate(what, previousDirective, directive, element) { + if (previousDirective) { + throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}', + previousDirective.name, directive.name, what, startingTag(element)); + } + } + + + function addTextInterpolateDirective(directives, text) { + var interpolateFn = $interpolate(text, true); + if (interpolateFn) { + directives.push({ + priority: 0, + compile: valueFn(function textInterpolateLinkFn(scope, node) { + var parent = node.parent(), + bindings = parent.data('$binding') || []; + bindings.push(interpolateFn); + safeAddClass(parent.data('$binding', bindings), 'ng-binding'); + scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { + node[0].nodeValue = value; + }); + }) + }); + } + } + + + function getTrustedContext(node, attrNormalizedName) { + if (attrNormalizedName == "srcdoc") { + return $sce.HTML; + } + var tag = nodeName_(node); + // maction[xlink:href] can source SVG. It's not limited to . + if (attrNormalizedName == "xlinkHref" || + (tag == "FORM" && attrNormalizedName == "action") || + (tag != "IMG" && (attrNormalizedName == "src" || + attrNormalizedName == "ngSrc"))) { + return $sce.RESOURCE_URL; + } + } + + + function addAttrInterpolateDirective(node, directives, value, name) { + var interpolateFn = $interpolate(value, true); + + // no interpolation found -> ignore + if (!interpolateFn) return; + + + if (name === "multiple" && nodeName_(node) === "SELECT") { + throw $compileMinErr("selmulti", + "Binding to the 'multiple' attribute is not supported. Element: {0}", + startingTag(node)); + } + + directives.push({ + priority: 100, + compile: function() { + return { + pre: function attrInterpolatePreLinkFn(scope, element, attr) { + var $$observers = (attr.$$observers || (attr.$$observers = {})); + + if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { + throw $compileMinErr('nodomevents', + "Interpolations for HTML DOM event attributes are disallowed. Please use the " + + "ng- versions (such as ng-click instead of onclick) instead."); + } + + // we need to interpolate again, in case the attribute value has been updated + // (e.g. by another directive's compile function) + interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); + + // if attribute was updated so that there is no interpolation going on we don't want to + // register any observers + if (!interpolateFn) return; + + // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the + // actual attr value + attr[name] = interpolateFn(scope); + ($$observers[name] || ($$observers[name] = [])).$$inter = true; + (attr.$$observers && attr.$$observers[name].$$scope || scope). + $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { + //special case for class attribute addition + removal + //so that class changes can tap into the animation + //hooks provided by the $animate service. Be sure to + //skip animations when the first digest occurs (when + //both the new and the old values are the same) since + //the CSS classes are the non-interpolated values + if(name === 'class' && newValue != oldValue) { + attr.$updateClass(newValue, oldValue); + } else { + attr.$set(name, newValue); + } + }); + } + }; + } + }); + } + + + /** + * This is a special jqLite.replaceWith, which can replace items which + * have no parents, provided that the containing jqLite collection is provided. + * + * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes + * in the root of the tree. + * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep + * the shell, but replace its DOM node reference. + * @param {Node} newNode The new DOM node. + */ + function replaceWith($rootElement, elementsToRemove, newNode) { + var firstElementToRemove = elementsToRemove[0], + removeCount = elementsToRemove.length, + parent = firstElementToRemove.parentNode, + i, ii; + + if ($rootElement) { + for(i = 0, ii = $rootElement.length; i < ii; i++) { + if ($rootElement[i] == firstElementToRemove) { + $rootElement[i++] = newNode; + for (var j = i, j2 = j + removeCount - 1, + jj = $rootElement.length; + j < jj; j++, j2++) { + if (j2 < jj) { + $rootElement[j] = $rootElement[j2]; + } else { + delete $rootElement[j]; + } + } + $rootElement.length -= removeCount - 1; + break; + } + } + } + + if (parent) { + parent.replaceChild(newNode, firstElementToRemove); + } + var fragment = document.createDocumentFragment(); + fragment.appendChild(firstElementToRemove); + newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; + for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { + var element = elementsToRemove[k]; + jqLite(element).remove(); // must do this way to clean up expando + fragment.appendChild(element); + delete elementsToRemove[k]; + } + + elementsToRemove[0] = newNode; + elementsToRemove.length = 1; + } + + + function cloneAndAnnotateFn(fn, annotation) { + return extend(function() { return fn.apply(null, arguments); }, fn, annotation); + } + }]; +} + +var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; +/** + * Converts all accepted directives format into proper directive name. + * All of these will become 'myDirective': + * my:Directive + * my-directive + * x-my-directive + * data-my:directive + * + * Also there is special case for Moz prefix starting with upper case letter. + * @param name Name to normalize + */ +function directiveNormalize(name) { + return camelCase(name.replace(PREFIX_REGEXP, '')); +} + +/** + * @ngdoc type + * @name $compile.directive.Attributes + * + * @description + * A shared object between directive compile / linking functions which contains normalized DOM + * element attributes. The values reflect current binding state `{{ }}`. The normalization is + * needed since all of these are treated as equivalent in Angular: + * + * ``` + * + * ``` + */ + +/** + * @ngdoc property + * @name $compile.directive.Attributes#$attr + * @returns {object} A map of DOM element attribute names to the normalized name. This is + * needed to do reverse lookup from normalized name back to actual name. + */ + + +/** + * @ngdoc method + * @name $compile.directive.Attributes#$set + * @kind function + * + * @description + * Set DOM element attribute value. + * + * + * @param {string} name Normalized element attribute name of the property to modify. The name is + * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr} + * property to the original name. + * @param {string} value Value to set the attribute to. The value can be an interpolated string. + */ + + + +/** + * Closure compiler type information + */ + +function nodesetLinkingFn( + /* angular.Scope */ scope, + /* NodeList */ nodeList, + /* Element */ rootElement, + /* function(Function) */ boundTranscludeFn +){} + +function directiveLinkingFn( + /* nodesetLinkingFn */ nodesetLinkingFn, + /* angular.Scope */ scope, + /* Node */ node, + /* Element */ rootElement, + /* function(Function) */ boundTranscludeFn +){} + +function tokenDifference(str1, str2) { + var values = '', + tokens1 = str1.split(/\s+/), + tokens2 = str2.split(/\s+/); + + outer: + for(var i = 0; i < tokens1.length; i++) { + var token = tokens1[i]; + for(var j = 0; j < tokens2.length; j++) { + if(token == tokens2[j]) continue outer; + } + values += (values.length > 0 ? ' ' : '') + token; + } + return values; +} + +/** + * @ngdoc provider + * @name $controllerProvider + * @description + * The {@link ng.$controller $controller service} is used by Angular to create new + * controllers. + * + * This provider allows controller registration via the + * {@link ng.$controllerProvider#register register} method. + */ +function $ControllerProvider() { + var controllers = {}, + CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; + + + /** + * @ngdoc method + * @name $controllerProvider#register + * @param {string|Object} name Controller name, or an object map of controllers where the keys are + * the names and the values are the constructors. + * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI + * annotations in the array notation). + */ + this.register = function(name, constructor) { + assertNotHasOwnProperty(name, 'controller'); + if (isObject(name)) { + extend(controllers, name); + } else { + controllers[name] = constructor; + } + }; + + + this.$get = ['$injector', '$window', function($injector, $window) { + + /** + * @ngdoc service + * @name $controller + * @requires $injector + * + * @param {Function|string} constructor If called with a function then it's considered to be the + * controller constructor function. Otherwise it's considered to be a string which is used + * to retrieve the controller constructor using the following steps: + * + * * check if a controller with given name is registered via `$controllerProvider` + * * check if evaluating the string on the current scope returns a constructor + * * check `window[constructor]` on the global `window` object + * + * @param {Object} locals Injection locals for Controller. + * @return {Object} Instance of given controller. + * + * @description + * `$controller` service is responsible for instantiating controllers. + * + * It's just a simple call to {@link auto.$injector $injector}, but extracted into + * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). + */ + return function(expression, locals) { + var instance, match, constructor, identifier; + + if(isString(expression)) { + match = expression.match(CNTRL_REG), + constructor = match[1], + identifier = match[3]; + expression = controllers.hasOwnProperty(constructor) + ? controllers[constructor] + : getter(locals.$scope, constructor, true) || getter($window, constructor, true); + + assertArgFn(expression, constructor, true); + } + + instance = $injector.instantiate(expression, locals); + + if (identifier) { + if (!(locals && typeof locals.$scope == 'object')) { + throw minErr('$controller')('noscp', + "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", + constructor || expression.name, identifier); + } + + locals.$scope[identifier] = instance; + } + + return instance; + }; + }]; +} + +/** + * @ngdoc service + * @name $document + * @requires $window + * + * @description + * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. + * + * @example + + +
+

$document title:

+

window.document title:

+
+
+ + function MainCtrl($scope, $document) { + $scope.title = $document[0].title; + $scope.windowTitle = angular.element(window.document)[0].title; + } + +
+ */ +function $DocumentProvider(){ + this.$get = ['$window', function(window){ + return jqLite(window.document); + }]; +} + +/** + * @ngdoc service + * @name $exceptionHandler + * @requires ng.$log + * + * @description + * Any uncaught exception in angular expressions is delegated to this service. + * The default implementation simply delegates to `$log.error` which logs it into + * the browser console. + * + * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by + * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing. + * + * ## Example: + * + * ```js + * angular.module('exceptionOverride', []).factory('$exceptionHandler', function () { + * return function (exception, cause) { + * exception.message += ' (caused by "' + cause + '")'; + * throw exception; + * }; + * }); + * ``` + * + * This example will override the normal action of `$exceptionHandler`, to make angular + * exceptions fail hard when they happen, instead of just logging to the console. + * + * @param {Error} exception Exception associated with the error. + * @param {string=} cause optional information about the context in which + * the error was thrown. + * + */ +function $ExceptionHandlerProvider() { + this.$get = ['$log', function($log) { + return function(exception, cause) { + $log.error.apply($log, arguments); + }; + }]; +} + +/** + * Parse headers into key value object + * + * @param {string} headers Raw headers as a string + * @returns {Object} Parsed headers as key value object + */ +function parseHeaders(headers) { + var parsed = {}, key, val, i; + + if (!headers) return parsed; + + forEach(headers.split('\n'), function(line) { + i = line.indexOf(':'); + key = lowercase(trim(line.substr(0, i))); + val = trim(line.substr(i + 1)); + + if (key) { + if (parsed[key]) { + parsed[key] += ', ' + val; + } else { + parsed[key] = val; + } + } + }); + + return parsed; +} + + +/** + * Returns a function that provides access to parsed headers. + * + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + * - if called with single an argument returns a single header value or null + * - if called with no arguments returns an object containing all headers. + */ +function headersGetter(headers) { + var headersObj = isObject(headers) ? headers : undefined; + + return function(name) { + if (!headersObj) headersObj = parseHeaders(headers); + + if (name) { + return headersObj[lowercase(name)] || null; + } + + return headersObj; + }; +} + + +/** + * Chain all given functions + * + * This function is used for both request and response transforming + * + * @param {*} data Data to transform. + * @param {function(string=)} headers Http headers getter fn. + * @param {(Function|Array.)} fns Function or an array of functions. + * @returns {*} Transformed data. + */ +function transformData(data, headers, fns) { + if (isFunction(fns)) + return fns(data, headers); + + forEach(fns, function(fn) { + data = fn(data, headers); + }); + + return data; +} + + +function isSuccess(status) { + return 200 <= status && status < 300; +} + + +function $HttpProvider() { + var JSON_START = /^\s*(\[|\{[^\{])/, + JSON_END = /[\}\]]\s*$/, + PROTECTION_PREFIX = /^\)\]\}',?\n/, + CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; + + var defaults = this.defaults = { + // transform incoming response data + transformResponse: [function(data) { + if (isString(data)) { + // strip json vulnerability protection prefix + data = data.replace(PROTECTION_PREFIX, ''); + if (JSON_START.test(data) && JSON_END.test(data)) + data = fromJson(data); + } + return data; + }], + + // transform outgoing request data + transformRequest: [function(d) { + return isObject(d) && !isFile(d) && !isBlob(d) ? toJson(d) : d; + }], + + // default headers + headers: { + common: { + 'Accept': 'application/json, text/plain, */*' + }, + post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON) + }, + + xsrfCookieName: 'XSRF-TOKEN', + xsrfHeaderName: 'X-XSRF-TOKEN' + }; + + /** + * Are ordered by request, i.e. they are applied in the same order as the + * array, on request, but reverse order, on response. + */ + var interceptorFactories = this.interceptors = []; + + /** + * For historical reasons, response interceptors are ordered by the order in which + * they are applied to the response. (This is the opposite of interceptorFactories) + */ + var responseInterceptorFactories = this.responseInterceptors = []; + + this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', + function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + + var defaultCache = $cacheFactory('$http'); + + /** + * Interceptors stored in reverse order. Inner interceptors before outer interceptors. + * The reversal is needed so that we can build up the interception chain around the + * server request. + */ + var reversedInterceptors = []; + + forEach(interceptorFactories, function(interceptorFactory) { + reversedInterceptors.unshift(isString(interceptorFactory) + ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); + }); + + forEach(responseInterceptorFactories, function(interceptorFactory, index) { + var responseFn = isString(interceptorFactory) + ? $injector.get(interceptorFactory) + : $injector.invoke(interceptorFactory); + + /** + * Response interceptors go before "around" interceptors (no real reason, just + * had to pick one.) But they are already reversed, so we can't use unshift, hence + * the splice. + */ + reversedInterceptors.splice(index, 0, { + response: function(response) { + return responseFn($q.when(response)); + }, + responseError: function(response) { + return responseFn($q.reject(response)); + } + }); + }); + + + /** + * @ngdoc service + * @kind function + * @name $http + * @requires ng.$httpBackend + * @requires $cacheFactory + * @requires $rootScope + * @requires $q + * @requires $injector + * + * @description + * The `$http` service is a core Angular service that facilitates communication with the remote + * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest) + * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP). + * + * For unit testing applications that use `$http` service, see + * {@link ngMock.$httpBackend $httpBackend mock}. + * + * For a higher level of abstraction, please check out the {@link ngResource.$resource + * $resource} service. + * + * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by + * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage + * it is important to familiarize yourself with these APIs and the guarantees they provide. + * + * + * # General usage + * The `$http` service is a function which takes a single argument — a configuration object — + * that is used to generate an HTTP request and returns a {@link ng.$q promise} + * with two $http specific methods: `success` and `error`. + * + * ```js + * $http({method: 'GET', url: '/someUrl'}). + * success(function(data, status, headers, config) { + * // this callback will be called asynchronously + * // when the response is available + * }). + * error(function(data, status, headers, config) { + * // called asynchronously if an error occurs + * // or server returns response with an error status. + * }); + * ``` + * + * Since the returned value of calling the $http function is a `promise`, you can also use + * the `then` method to register callbacks, and these callbacks will receive a single argument – + * an object representing the response. See the API signature and type info below for more + * details. + * + * A response status code between 200 and 299 is considered a success status and + * will result in the success callback being called. Note that if the response is a redirect, + * XMLHttpRequest will transparently follow it, meaning that the error callback will not be + * called for such responses. + * + * # Writing Unit Tests that use $http + * When unit testing (using {@link ngMock ngMock}), it is necessary to call + * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending + * request using trained responses. + * + * ``` + * $httpBackend.expectGET(...); + * $http.get(...); + * $httpBackend.flush(); + * ``` + * + * # Shortcut methods + * + * Shortcut methods are also available. All shortcut methods require passing in the URL, and + * request data must be passed in for POST/PUT requests. + * + * ```js + * $http.get('/someUrl').success(successCallback); + * $http.post('/someUrl', data).success(successCallback); + * ``` + * + * Complete list of shortcut methods: + * + * - {@link ng.$http#get $http.get} + * - {@link ng.$http#head $http.head} + * - {@link ng.$http#post $http.post} + * - {@link ng.$http#put $http.put} + * - {@link ng.$http#delete $http.delete} + * - {@link ng.$http#jsonp $http.jsonp} + * + * + * # Setting HTTP Headers + * + * The $http service will automatically add certain HTTP headers to all requests. These defaults + * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration + * object, which currently contains this default configuration: + * + * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): + * - `Accept: application/json, text/plain, * / *` + * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) + * - `Content-Type: application/json` + * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) + * - `Content-Type: application/json` + * + * To add or overwrite these defaults, simply add or remove a property from these configuration + * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object + * with the lowercased HTTP method name as the key, e.g. + * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. + * + * The defaults can also be set at runtime via the `$http.defaults` object in the same + * fashion. For example: + * + * ``` + * module.run(function($http) { + * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w' + * }); + * ``` + * + * In addition, you can supply a `headers` property in the config object passed when + * calling `$http(config)`, which overrides the defaults without changing them globally. + * + * + * # Transforming Requests and Responses + * + * Both requests and responses can be transformed using transform functions. By default, Angular + * applies these transformations: + * + * Request transformations: + * + * - If the `data` property of the request configuration object contains an object, serialize it + * into JSON format. + * + * Response transformations: + * + * - If XSRF prefix is detected, strip it (see Security Considerations section below). + * - If JSON response is detected, deserialize it using a JSON parser. + * + * To globally augment or override the default transforms, modify the + * `$httpProvider.defaults.transformRequest` and `$httpProvider.defaults.transformResponse` + * properties. These properties are by default an array of transform functions, which allows you + * to `push` or `unshift` a new transformation function into the transformation chain. You can + * also decide to completely override any default transformations by assigning your + * transformation functions to these properties directly without the array wrapper. These defaults + * are again available on the $http factory at run-time, which may be useful if you have run-time + * services you wish to be involved in your transformations. + * + * Similarly, to locally override the request/response transforms, augment the + * `transformRequest` and/or `transformResponse` properties of the configuration object passed + * into `$http`. + * + * + * # Caching + * + * To enable caching, set the request configuration `cache` property to `true` (to use default + * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). + * When the cache is enabled, `$http` stores the response from the server in the specified + * cache. The next time the same request is made, the response is served from the cache without + * sending a request to the server. + * + * Note that even if the response is served from cache, delivery of the data is asynchronous in + * the same way that real requests are. + * + * If there are multiple GET requests for the same URL that should be cached using the same + * cache, but the cache is not populated yet, only one request to the server will be made and + * the remaining requests will be fulfilled using the response from the first request. + * + * You can change the default cache to a new object (built with + * {@link ng.$cacheFactory `$cacheFactory`}) by updating the + * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set + * their `cache` property to `true` will now use this cache object. + * + * If you set the default cache to `false` then only requests that specify their own custom + * cache object will be cached. + * + * # Interceptors + * + * Before you start creating interceptors, be sure to understand the + * {@link ng.$q $q and deferred/promise APIs}. + * + * For purposes of global error handling, authentication, or any kind of synchronous or + * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be + * able to intercept requests before they are handed to the server and + * responses before they are handed over to the application code that + * initiated these requests. The interceptors leverage the {@link ng.$q + * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. + * + * The interceptors are service factories that are registered with the `$httpProvider` by + * adding them to the `$httpProvider.interceptors` array. The factory is called and + * injected with dependencies (if specified) and returns the interceptor. + * + * There are two kinds of interceptors (and two kinds of rejection interceptors): + * + * * `request`: interceptors get called with a http `config` object. The function is free to + * modify the `config` object or create a new one. The function needs to return the `config` + * object directly, or a promise containing the `config` or a new `config` object. + * * `requestError`: interceptor gets called when a previous interceptor threw an error or + * resolved with a rejection. + * * `response`: interceptors get called with http `response` object. The function is free to + * modify the `response` object or create a new one. The function needs to return the `response` + * object directly, or as a promise containing the `response` or a new `response` object. + * * `responseError`: interceptor gets called when a previous interceptor threw an error or + * resolved with a rejection. + * + * + * ```js + * // register the interceptor as a service + * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { + * return { + * // optional method + * 'request': function(config) { + * // do something on success + * return config; + * }, + * + * // optional method + * 'requestError': function(rejection) { + * // do something on error + * if (canRecover(rejection)) { + * return responseOrNewPromise + * } + * return $q.reject(rejection); + * }, + * + * + * + * // optional method + * 'response': function(response) { + * // do something on success + * return response; + * }, + * + * // optional method + * 'responseError': function(rejection) { + * // do something on error + * if (canRecover(rejection)) { + * return responseOrNewPromise + * } + * return $q.reject(rejection); + * } + * }; + * }); + * + * $httpProvider.interceptors.push('myHttpInterceptor'); + * + * + * // alternatively, register the interceptor via an anonymous factory + * $httpProvider.interceptors.push(function($q, dependency1, dependency2) { + * return { + * 'request': function(config) { + * // same as above + * }, + * + * 'response': function(response) { + * // same as above + * } + * }; + * }); + * ``` + * + * # Response interceptors (DEPRECATED) + * + * Before you start creating interceptors, be sure to understand the + * {@link ng.$q $q and deferred/promise APIs}. + * + * For purposes of global error handling, authentication or any kind of synchronous or + * asynchronous preprocessing of received responses, it is desirable to be able to intercept + * responses for http requests before they are handed over to the application code that + * initiated these requests. The response interceptors leverage the {@link ng.$q + * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. + * + * The interceptors are service factories that are registered with the $httpProvider by + * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and + * injected with dependencies (if specified) and returns the interceptor — a function that + * takes a {@link ng.$q promise} and returns the original or a new promise. + * + * ```js + * // register the interceptor as a service + * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { + * return function(promise) { + * return promise.then(function(response) { + * // do something on success + * return response; + * }, function(response) { + * // do something on error + * if (canRecover(response)) { + * return responseOrNewPromise + * } + * return $q.reject(response); + * }); + * } + * }); + * + * $httpProvider.responseInterceptors.push('myHttpInterceptor'); + * + * + * // register the interceptor via an anonymous factory + * $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { + * return function(promise) { + * // same as above + * } + * }); + * ``` + * + * + * # Security Considerations + * + * When designing web applications, consider security threats from: + * + * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) + * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) + * + * Both server and the client must cooperate in order to eliminate these threats. Angular comes + * pre-configured with strategies that address these issues, but for this to work backend server + * cooperation is required. + * + * ## JSON Vulnerability Protection + * + * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) + * allows third party website to turn your JSON resource URL into + * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To + * counter this your server can prefix all JSON requests with following string `")]}',\n"`. + * Angular will automatically strip the prefix before processing it as JSON. + * + * For example if your server needs to return: + * ```js + * ['one','two'] + * ``` + * + * which is vulnerable to attack, your server can return: + * ```js + * )]}', + * ['one','two'] + * ``` + * + * Angular will strip the prefix, before processing the JSON. + * + * + * ## Cross Site Request Forgery (XSRF) Protection + * + * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which + * an unauthorized site can gain your user's private data. Angular provides a mechanism + * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie + * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only + * JavaScript that runs on your domain could read the cookie, your server can be assured that + * the XHR came from JavaScript running on your domain. The header will not be set for + * cross-domain requests. + * + * To take advantage of this, your server needs to set a token in a JavaScript readable session + * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the + * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure + * that only JavaScript running on your domain could have sent the request. The token must be + * unique for each user and must be verifiable by the server (to prevent the JavaScript from + * making up its own tokens). We recommend that the token is a digest of your site's + * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) + * for added security. + * + * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName + * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, + * or the per-request config object. + * + * + * @param {object} config Object describing the request to be made and how it should be + * processed. The object has following properties: + * + * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) + * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. + * - **params** – `{Object.}` – Map of strings or objects which will be turned + * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be + * JSONified. + * - **data** – `{string|Object}` – Data to be sent as the request message data. + * - **headers** – `{Object}` – Map of strings or functions which return strings representing + * HTTP headers to send to the server. If the return value of a function is null, the + * header will not be sent. + * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. + * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. + * - **transformRequest** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * request body and headers and returns its transformed (typically serialized) version. + * - **transformResponse** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * response body and headers and returns its transformed (typically deserialized) version. + * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the + * GET request, otherwise if a cache instance built with + * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for + * caching. + * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} + * that should abort the request when resolved. + * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the + * XHR object. See [requests with credentials]https://developer.mozilla.org/en/http_access_control#section_5 + * for more information. + * - **responseType** - `{string}` - see + * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). + * + * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the + * standard `then` method and two http specific methods: `success` and `error`. The `then` + * method takes two arguments a success and an error callback which will be called with a + * response object. The `success` and `error` methods take a single argument - a function that + * will be called when the request succeeds or fails respectively. The arguments passed into + * these functions are destructured representation of the response object passed into the + * `then` method. The response object has these properties: + * + * - **data** – `{string|Object}` – The response body transformed with the transform + * functions. + * - **status** – `{number}` – HTTP status code of the response. + * - **headers** – `{function([headerName])}` – Header getter function. + * - **config** – `{Object}` – The configuration object that was used to generate the request. + * - **statusText** – `{string}` – HTTP status text of the response. + * + * @property {Array.} pendingRequests Array of config objects for currently pending + * requests. This is primarily meant to be used for debugging purposes. + * + * + * @example + + +
+ + +
+ + + +
http status code: {{status}}
+
http response data: {{data}}
+
+
+ + function FetchCtrl($scope, $http, $templateCache) { + $scope.method = 'GET'; + $scope.url = 'http-hello.html'; + + $scope.fetch = function() { + $scope.code = null; + $scope.response = null; + + $http({method: $scope.method, url: $scope.url, cache: $templateCache}). + success(function(data, status) { + $scope.status = status; + $scope.data = data; + }). + error(function(data, status) { + $scope.data = data || "Request failed"; + $scope.status = status; + }); + }; + + $scope.updateModel = function(method, url) { + $scope.method = method; + $scope.url = url; + }; + } + + + Hello, $http! + + + var status = element(by.binding('status')); + var data = element(by.binding('data')); + var fetchBtn = element(by.id('fetchbtn')); + var sampleGetBtn = element(by.id('samplegetbtn')); + var sampleJsonpBtn = element(by.id('samplejsonpbtn')); + var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); + + it('should make an xhr GET request', function() { + sampleGetBtn.click(); + fetchBtn.click(); + expect(status.getText()).toMatch('200'); + expect(data.getText()).toMatch(/Hello, \$http!/); + }); + + it('should make a JSONP request to angularjs.org', function() { + sampleJsonpBtn.click(); + fetchBtn.click(); + expect(status.getText()).toMatch('200'); + expect(data.getText()).toMatch(/Super Hero!/); + }); + + it('should make JSONP request to invalid URL and invoke the error handler', + function() { + invalidJsonpBtn.click(); + fetchBtn.click(); + expect(status.getText()).toMatch('0'); + expect(data.getText()).toMatch('Request failed'); + }); + +
+ */ + function $http(requestConfig) { + var config = { + method: 'get', + transformRequest: defaults.transformRequest, + transformResponse: defaults.transformResponse + }; + var headers = mergeHeaders(requestConfig); + + extend(config, requestConfig); + config.headers = headers; + config.method = uppercase(config.method); + + var xsrfValue = urlIsSameOrigin(config.url) + ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] + : undefined; + if (xsrfValue) { + headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; + } + + + var serverRequest = function(config) { + headers = config.headers; + var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); + + // strip content-type if data is undefined + if (isUndefined(config.data)) { + forEach(headers, function(value, header) { + if (lowercase(header) === 'content-type') { + delete headers[header]; + } + }); + } + + if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { + config.withCredentials = defaults.withCredentials; + } + + // send request + return sendReq(config, reqData, headers).then(transformResponse, transformResponse); + }; + + var chain = [serverRequest, undefined]; + var promise = $q.when(config); + + // apply interceptors + forEach(reversedInterceptors, function(interceptor) { + if (interceptor.request || interceptor.requestError) { + chain.unshift(interceptor.request, interceptor.requestError); + } + if (interceptor.response || interceptor.responseError) { + chain.push(interceptor.response, interceptor.responseError); + } + }); + + while(chain.length) { + var thenFn = chain.shift(); + var rejectFn = chain.shift(); + + promise = promise.then(thenFn, rejectFn); + } + + promise.success = function(fn) { + promise.then(function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + promise.error = function(fn) { + promise.then(null, function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + return promise; + + function transformResponse(response) { + // make a copy since the response must be cacheable + var resp = extend({}, response, { + data: transformData(response.data, response.headers, config.transformResponse) + }); + return (isSuccess(response.status)) + ? resp + : $q.reject(resp); + } + + function mergeHeaders(config) { + var defHeaders = defaults.headers, + reqHeaders = extend({}, config.headers), + defHeaderName, lowercaseDefHeaderName, reqHeaderName; + + defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); + + // execute if header value is function + execHeaders(defHeaders); + execHeaders(reqHeaders); + + // using for-in instead of forEach to avoid unecessary iteration after header has been found + defaultHeadersIteration: + for (defHeaderName in defHeaders) { + lowercaseDefHeaderName = lowercase(defHeaderName); + + for (reqHeaderName in reqHeaders) { + if (lowercase(reqHeaderName) === lowercaseDefHeaderName) { + continue defaultHeadersIteration; + } + } + + reqHeaders[defHeaderName] = defHeaders[defHeaderName]; + } + + return reqHeaders; + + function execHeaders(headers) { + var headerContent; + + forEach(headers, function(headerFn, header) { + if (isFunction(headerFn)) { + headerContent = headerFn(); + if (headerContent != null) { + headers[header] = headerContent; + } else { + delete headers[header]; + } + } + }); + } + } + } + + $http.pendingRequests = []; + + /** + * @ngdoc method + * @name $http#get + * + * @description + * Shortcut method to perform `GET` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#delete + * + * @description + * Shortcut method to perform `DELETE` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#head + * + * @description + * Shortcut method to perform `HEAD` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#jsonp + * + * @description + * Shortcut method to perform `JSONP` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request. + * Should contain `JSON_CALLBACK` string. + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethods('get', 'delete', 'head', 'jsonp'); + + /** + * @ngdoc method + * @name $http#post + * + * @description + * Shortcut method to perform `POST` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#put + * + * @description + * Shortcut method to perform `PUT` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethodsWithData('post', 'put'); + + /** + * @ngdoc property + * @name $http#defaults + * + * @description + * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of + * default headers, withCredentials as well as request and response transformations. + * + * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. + */ + $http.defaults = defaults; + + + return $http; + + + function createShortMethods(names) { + forEach(arguments, function(name) { + $http[name] = function(url, config) { + return $http(extend(config || {}, { + method: name, + url: url + })); + }; + }); + } + + + function createShortMethodsWithData(name) { + forEach(arguments, function(name) { + $http[name] = function(url, data, config) { + return $http(extend(config || {}, { + method: name, + url: url, + data: data + })); + }; + }); + } + + + /** + * Makes the request. + * + * !!! ACCESSES CLOSURE VARS: + * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests + */ + function sendReq(config, reqData, reqHeaders) { + var deferred = $q.defer(), + promise = deferred.promise, + cache, + cachedResp, + url = buildUrl(config.url, config.params); + + $http.pendingRequests.push(config); + promise.then(removePendingReq, removePendingReq); + + + if ((config.cache || defaults.cache) && config.cache !== false && config.method == 'GET') { + cache = isObject(config.cache) ? config.cache + : isObject(defaults.cache) ? defaults.cache + : defaultCache; + } + + if (cache) { + cachedResp = cache.get(url); + if (isDefined(cachedResp)) { + if (cachedResp.then) { + // cached request has already been sent, but there is no response yet + cachedResp.then(removePendingReq, removePendingReq); + return cachedResp; + } else { + // serving from cache + if (isArray(cachedResp)) { + resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]); + } else { + resolvePromise(cachedResp, 200, {}, 'OK'); + } + } + } else { + // put the promise for the non-transformed response into cache as a placeholder + cache.put(url, promise); + } + } + + // if we won't have the response in cache, send the request to the backend + if (isUndefined(cachedResp)) { + $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, + config.withCredentials, config.responseType); + } + + return promise; + + + /** + * Callback registered to $httpBackend(): + * - caches the response if desired + * - resolves the raw $http promise + * - calls $apply + */ + function done(status, response, headersString, statusText) { + if (cache) { + if (isSuccess(status)) { + cache.put(url, [status, response, parseHeaders(headersString), statusText]); + } else { + // remove promise from the cache + cache.remove(url); + } + } + + resolvePromise(response, status, headersString, statusText); + if (!$rootScope.$$phase) $rootScope.$apply(); + } + + + /** + * Resolves the raw $http promise. + */ + function resolvePromise(response, status, headers, statusText) { + // normalize internal statuses to 0 + status = Math.max(status, 0); + + (isSuccess(status) ? deferred.resolve : deferred.reject)({ + data: response, + status: status, + headers: headersGetter(headers), + config: config, + statusText : statusText + }); + } + + + function removePendingReq() { + var idx = indexOf($http.pendingRequests, config); + if (idx !== -1) $http.pendingRequests.splice(idx, 1); + } + } + + + function buildUrl(url, params) { + if (!params) return url; + var parts = []; + forEachSorted(params, function(value, key) { + if (value === null || isUndefined(value)) return; + if (!isArray(value)) value = [value]; + + forEach(value, function(v) { + if (isObject(v)) { + v = toJson(v); + } + parts.push(encodeUriQuery(key) + '=' + + encodeUriQuery(v)); + }); + }); + if(parts.length > 0) { + url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + } + return url; + } + + + }]; +} + +function createXhr(method) { + //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest + //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest + //if it is available + if (msie <= 8 && (!method.match(/^(get|post|head|put|delete|options)$/i) || + !window.XMLHttpRequest)) { + return new window.ActiveXObject("Microsoft.XMLHTTP"); + } else if (window.XMLHttpRequest) { + return new window.XMLHttpRequest(); + } + + throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest."); +} + +/** + * @ngdoc service + * @name $httpBackend + * @requires $window + * @requires $document + * + * @description + * HTTP backend used by the {@link ng.$http service} that delegates to + * XMLHttpRequest object or JSONP and deals with browser incompatibilities. + * + * You should never need to use this service directly, instead use the higher-level abstractions: + * {@link ng.$http $http} or {@link ngResource.$resource $resource}. + * + * During testing this implementation is swapped with {@link ngMock.$httpBackend mock + * $httpBackend} which can be trained with responses. + */ +function $HttpBackendProvider() { + this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { + return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); + }]; +} + +function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { + var ABORTED = -1; + + // TODO(vojta): fix the signature + return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { + var status; + $browser.$$incOutstandingRequestCount(); + url = url || $browser.url(); + + if (lowercase(method) == 'jsonp') { + var callbackId = '_' + (callbacks.counter++).toString(36); + callbacks[callbackId] = function(data) { + callbacks[callbackId].data = data; + callbacks[callbackId].called = true; + }; + + var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), + callbackId, function(status, text) { + completeRequest(callback, status, callbacks[callbackId].data, "", text); + callbacks[callbackId] = noop; + }); + } else { + + var xhr = createXhr(method); + + xhr.open(method, url, true); + forEach(headers, function(value, key) { + if (isDefined(value)) { + xhr.setRequestHeader(key, value); + } + }); + + // In IE6 and 7, this might be called synchronously when xhr.send below is called and the + // response is in the cache. the promise api will ensure that to the app code the api is + // always async + xhr.onreadystatechange = function() { + // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by + // xhrs that are resolved while the app is in the background (see #5426). + // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before + // continuing + // + // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and + // Safari respectively. + if (xhr && xhr.readyState == 4) { + var responseHeaders = null, + response = null; + + if(status !== ABORTED) { + responseHeaders = xhr.getAllResponseHeaders(); + + // responseText is the old-school way of retrieving response (supported by IE8 & 9) + // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) + response = ('response' in xhr) ? xhr.response : xhr.responseText; + } + + completeRequest(callback, + status || xhr.status, + response, + responseHeaders, + xhr.statusText || ''); + } + }; + + if (withCredentials) { + xhr.withCredentials = true; + } + + if (responseType) { + try { + xhr.responseType = responseType; + } catch (e) { + // WebKit added support for the json responseType value on 09/03/2013 + // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are + // known to throw when setting the value "json" as the response type. Other older + // browsers implementing the responseType + // + // The json response type can be ignored if not supported, because JSON payloads are + // parsed on the client-side regardless. + if (responseType !== 'json') { + throw e; + } + } + } + + xhr.send(post || null); + } + + if (timeout > 0) { + var timeoutId = $browserDefer(timeoutRequest, timeout); + } else if (timeout && timeout.then) { + timeout.then(timeoutRequest); + } + + + function timeoutRequest() { + status = ABORTED; + jsonpDone && jsonpDone(); + xhr && xhr.abort(); + } + + function completeRequest(callback, status, response, headersString, statusText) { + // cancel timeout and subsequent timeout promise resolution + timeoutId && $browserDefer.cancel(timeoutId); + jsonpDone = xhr = null; + + // fix status code when it is 0 (0 status is undocumented). + // Occurs when accessing file resources or on Android 4.1 stock browser + // while retrieving files from application cache. + if (status === 0) { + status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; + } + + // normalize IE bug (http://bugs.jquery.com/ticket/1450) + status = status === 1223 ? 204 : status; + statusText = statusText || ''; + + callback(status, response, headersString, statusText); + $browser.$$completeOutstandingRequest(noop); + } + }; + + function jsonpReq(url, callbackId, done) { + // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: + // - fetches local scripts via XHR and evals them + // - adds and immediately removes script elements from the document + var script = rawDocument.createElement('script'), callback = null; + script.type = "text/javascript"; + script.src = url; + script.async = true; + + callback = function(event) { + removeEventListenerFn(script, "load", callback); + removeEventListenerFn(script, "error", callback); + rawDocument.body.removeChild(script); + script = null; + var status = -1; + var text = "unknown"; + + if (event) { + if (event.type === "load" && !callbacks[callbackId].called) { + event = { type: "error" }; + } + text = event.type; + status = event.type === "error" ? 404 : 200; + } + + if (done) { + done(status, text); + } + }; + + addEventListenerFn(script, "load", callback); + addEventListenerFn(script, "error", callback); + + if (msie <= 8) { + script.onreadystatechange = function() { + if (isString(script.readyState) && /loaded|complete/.test(script.readyState)) { + script.onreadystatechange = null; + callback({ + type: 'load' + }); + } + }; + } + + rawDocument.body.appendChild(script); + return callback; + } +} + +var $interpolateMinErr = minErr('$interpolate'); + +/** + * @ngdoc provider + * @name $interpolateProvider + * @kind function + * + * @description + * + * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. + * + * @example + + + +
+ //demo.label// +
+
+ + it('should interpolate binding with custom symbols', function() { + expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.'); + }); + +
+ */ +function $InterpolateProvider() { + var startSymbol = '{{'; + var endSymbol = '}}'; + + /** + * @ngdoc method + * @name $interpolateProvider#startSymbol + * @description + * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. + * + * @param {string=} value new value to set the starting symbol to. + * @returns {string|self} Returns the symbol when used as getter and self if used as setter. + */ + this.startSymbol = function(value){ + if (value) { + startSymbol = value; + return this; + } else { + return startSymbol; + } + }; + + /** + * @ngdoc method + * @name $interpolateProvider#endSymbol + * @description + * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. + * + * @param {string=} value new value to set the ending symbol to. + * @returns {string|self} Returns the symbol when used as getter and self if used as setter. + */ + this.endSymbol = function(value){ + if (value) { + endSymbol = value; + return this; + } else { + return endSymbol; + } + }; + + + this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { + var startSymbolLength = startSymbol.length, + endSymbolLength = endSymbol.length; + + /** + * @ngdoc service + * @name $interpolate + * @kind function + * + * @requires $parse + * @requires $sce + * + * @description + * + * Compiles a string with markup into an interpolation function. This service is used by the + * HTML {@link ng.$compile $compile} service for data binding. See + * {@link ng.$interpolateProvider $interpolateProvider} for configuring the + * interpolation markup. + * + * + * ```js + * var $interpolate = ...; // injected + * var exp = $interpolate('Hello {{name | uppercase}}!'); + * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); + * ``` + * + * + * @param {string} text The text with markup to interpolate. + * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have + * embedded expression in order to return an interpolation function. Strings with no + * embedded expression will return null for the interpolation function. + * @param {string=} trustedContext when provided, the returned function passes the interpolated + * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, + * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that + * provides Strict Contextual Escaping for details. + * @returns {function(context)} an interpolation function which is used to compute the + * interpolated string. The function has these parameters: + * + * * `context`: an object against which any expressions embedded in the strings are evaluated + * against. + * + */ + function $interpolate(text, mustHaveExpression, trustedContext) { + var startIndex, + endIndex, + index = 0, + parts = [], + length = text.length, + hasInterpolation = false, + fn, + exp, + concat = []; + + while(index < length) { + if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { + (index != startIndex) && parts.push(text.substring(index, startIndex)); + parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); + fn.exp = exp; + index = endIndex + endSymbolLength; + hasInterpolation = true; + } else { + // we did not find anything, so we have to add the remainder to the parts array + (index != length) && parts.push(text.substring(index)); + index = length; + } + } + + if (!(length = parts.length)) { + // we added, nothing, must have been an empty string. + parts.push(''); + length = 1; + } + + // Concatenating expressions makes it hard to reason about whether some combination of + // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a + // single expression be used for iframe[src], object[src], etc., we ensure that the value + // that's used is assigned or constructed by some JS code somewhere that is more testable or + // make it obvious that you bound the value to some user controlled value. This helps reduce + // the load when auditing for XSS issues. + if (trustedContext && parts.length > 1) { + throw $interpolateMinErr('noconcat', + "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + + "interpolations that concatenate multiple expressions when a trusted value is " + + "required. See http://docs.angularjs.org/api/ng.$sce", text); + } + + if (!mustHaveExpression || hasInterpolation) { + concat.length = length; + fn = function(context) { + try { + for(var i = 0, ii = length, part; i + * **Note**: Intervals created by this service must be explicitly destroyed when you are finished + * with them. In particular they are not automatically destroyed when a controller's scope or a + * directive's element are destroyed. + * You should take this into consideration and make sure to always cancel the interval at the + * appropriate moment. See the example below for more details on how and when to do this. + * + * + * @param {function()} fn A function that should be called repeatedly. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @returns {promise} A promise which will be notified on each iteration. + * + * @example + * + * + * + * + *
+ *
+ * Date format:
+ * Current time is: + *
+ * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
+ *
+ * + *
+ *
+ */ + function interval(fn, delay, count, invokeApply) { + var setInterval = $window.setInterval, + clearInterval = $window.clearInterval, + deferred = $q.defer(), + promise = deferred.promise, + iteration = 0, + skipApply = (isDefined(invokeApply) && !invokeApply); + + count = isDefined(count) ? count : 0; + + promise.then(null, null, fn); + + promise.$$intervalId = setInterval(function tick() { + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + deferred.resolve(iteration); + clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + } + + if (!skipApply) $rootScope.$apply(); + + }, delay); + + intervals[promise.$$intervalId] = deferred; + + return promise; + } + + + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {promise} promise returned by the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully canceled. + */ + interval.cancel = function(promise) { + if (promise && promise.$$intervalId in intervals) { + intervals[promise.$$intervalId].reject('canceled'); + clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + return true; + } + return false; + }; + + return interval; + }]; +} + +/** + * @ngdoc service + * @name $locale + * + * @description + * $locale service provides localization rules for various Angular components. As of right now the + * only public api is: + * + * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) + */ +function $LocaleProvider(){ + this.$get = function() { + return { + id: 'en-us', + + NUMBER_FORMATS: { + DECIMAL_SEP: '.', + GROUP_SEP: ',', + PATTERNS: [ + { // Decimal Pattern + minInt: 1, + minFrac: 0, + maxFrac: 3, + posPre: '', + posSuf: '', + negPre: '-', + negSuf: '', + gSize: 3, + lgSize: 3 + },{ //Currency Pattern + minInt: 1, + minFrac: 2, + maxFrac: 2, + posPre: '\u00A4', + posSuf: '', + negPre: '(\u00A4', + negSuf: ')', + gSize: 3, + lgSize: 3 + } + ], + CURRENCY_SYM: '$' + }, + + DATETIME_FORMATS: { + MONTH: + 'January,February,March,April,May,June,July,August,September,October,November,December' + .split(','), + SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), + DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), + SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), + AMPMS: ['AM','PM'], + medium: 'MMM d, y h:mm:ss a', + short: 'M/d/yy h:mm a', + fullDate: 'EEEE, MMMM d, y', + longDate: 'MMMM d, y', + mediumDate: 'MMM d, y', + shortDate: 'M/d/yy', + mediumTime: 'h:mm:ss a', + shortTime: 'h:mm a' + }, + + pluralCat: function(num) { + if (num === 1) { + return 'one'; + } + return 'other'; + } + }; + }; +} + +var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, + DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; +var $locationMinErr = minErr('$location'); + + +/** + * Encode path using encodeUriSegment, ignoring forward slashes + * + * @param {string} path Path to encode + * @returns {string} + */ +function encodePath(path) { + var segments = path.split('/'), + i = segments.length; + + while (i--) { + segments[i] = encodeUriSegment(segments[i]); + } + + return segments.join('/'); +} + +function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { + var parsedUrl = urlResolve(absoluteUrl, appBase); + + locationObj.$$protocol = parsedUrl.protocol; + locationObj.$$host = parsedUrl.hostname; + locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; +} + + +function parseAppUrl(relativeUrl, locationObj, appBase) { + var prefixed = (relativeUrl.charAt(0) !== '/'); + if (prefixed) { + relativeUrl = '/' + relativeUrl; + } + var match = urlResolve(relativeUrl, appBase); + locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? + match.pathname.substring(1) : match.pathname); + locationObj.$$search = parseKeyValue(match.search); + locationObj.$$hash = decodeURIComponent(match.hash); + + // make sure path starts with '/'; + if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { + locationObj.$$path = '/' + locationObj.$$path; + } +} + + +/** + * + * @param {string} begin + * @param {string} whole + * @returns {string} returns text from whole after begin or undefined if it does not begin with + * expected string. + */ +function beginsWith(begin, whole) { + if (whole.indexOf(begin) === 0) { + return whole.substr(begin.length); + } +} + + +function stripHash(url) { + var index = url.indexOf('#'); + return index == -1 ? url : url.substr(0, index); +} + + +function stripFile(url) { + return url.substr(0, stripHash(url).lastIndexOf('/') + 1); +} + +/* return the server only (scheme://host:port) */ +function serverBase(url) { + return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); +} + + +/** + * LocationHtml5Url represents an url + * This object is exposed as $location service when HTML5 mode is enabled and supported + * + * @constructor + * @param {string} appBase application base URL + * @param {string} basePrefix url path prefix + */ +function LocationHtml5Url(appBase, basePrefix) { + this.$$html5 = true; + basePrefix = basePrefix || ''; + var appBaseNoFile = stripFile(appBase); + parseAbsoluteUrl(appBase, this, appBase); + + + /** + * Parse given html5 (regular) url string into properties + * @param {string} newAbsoluteUrl HTML5 url + * @private + */ + this.$$parse = function(url) { + var pathUrl = beginsWith(appBaseNoFile, url); + if (!isString(pathUrl)) { + throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, + appBaseNoFile); + } + + parseAppUrl(pathUrl, this, appBase); + + if (!this.$$path) { + this.$$path = '/'; + } + + this.$$compose(); + }; + + /** + * Compose url and update `absUrl` property + * @private + */ + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' + }; + + this.$$rewrite = function(url) { + var appUrl, prevAppUrl; + + if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { + prevAppUrl = appUrl; + if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { + return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + } else { + return appBase + prevAppUrl; + } + } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { + return appBaseNoFile + appUrl; + } else if (appBaseNoFile == url + '/') { + return appBaseNoFile; + } + }; +} + + +/** + * LocationHashbangUrl represents url + * This object is exposed as $location service when developer doesn't opt into html5 mode. + * It also serves as the base class for html5 mode fallback on legacy browsers. + * + * @constructor + * @param {string} appBase application base URL + * @param {string} hashPrefix hashbang prefix + */ +function LocationHashbangUrl(appBase, hashPrefix) { + var appBaseNoFile = stripFile(appBase); + + parseAbsoluteUrl(appBase, this, appBase); + + + /** + * Parse given hashbang url into properties + * @param {string} url Hashbang url + * @private + */ + this.$$parse = function(url) { + var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); + var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' + ? beginsWith(hashPrefix, withoutBaseUrl) + : (this.$$html5) + ? withoutBaseUrl + : ''; + + if (!isString(withoutHashUrl)) { + throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, + hashPrefix); + } + parseAppUrl(withoutHashUrl, this, appBase); + + this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); + + this.$$compose(); + + /* + * In Windows, on an anchor node on documents loaded from + * the filesystem, the browser will return a pathname + * prefixed with the drive name ('/C:/path') when a + * pathname without a drive is set: + * * a.setAttribute('href', '/foo') + * * a.pathname === '/C:/foo' //true + * + * Inside of Angular, we're always using pathnames that + * do not include drive names for routing. + */ + function removeWindowsDriveName (path, url, base) { + /* + Matches paths for file protocol on windows, + such as /C:/foo/bar, and captures only /foo/bar. + */ + var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; + + var firstPathSegmentMatch; + + //Get the relative path from the input URL. + if (url.indexOf(base) === 0) { + url = url.replace(base, ''); + } + + // The input URL intentionally contains a first path segment that ends with a colon. + if (windowsFilePathExp.exec(url)) { + return path; + } + + firstPathSegmentMatch = windowsFilePathExp.exec(path); + return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path; + } + }; + + /** + * Compose hashbang url and update `absUrl` property + * @private + */ + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); + }; + + this.$$rewrite = function(url) { + if(stripHash(appBase) == stripHash(url)) { + return url; + } + }; +} + + +/** + * LocationHashbangUrl represents url + * This object is exposed as $location service when html5 history api is enabled but the browser + * does not support it. + * + * @constructor + * @param {string} appBase application base URL + * @param {string} hashPrefix hashbang prefix + */ +function LocationHashbangInHtml5Url(appBase, hashPrefix) { + this.$$html5 = true; + LocationHashbangUrl.apply(this, arguments); + + var appBaseNoFile = stripFile(appBase); + + this.$$rewrite = function(url) { + var appUrl; + + if ( appBase == stripHash(url) ) { + return url; + } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { + return appBase + hashPrefix + appUrl; + } else if ( appBaseNoFile === url + '/') { + return appBaseNoFile; + } + }; + + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + // include hashPrefix in $$absUrl when $$url is empty so IE8 & 9 do not reload page because of removal of '#' + this.$$absUrl = appBase + hashPrefix + this.$$url; + }; + +} + + +LocationHashbangInHtml5Url.prototype = + LocationHashbangUrl.prototype = + LocationHtml5Url.prototype = { + + /** + * Are we in html5 mode? + * @private + */ + $$html5: false, + + /** + * Has any change been replacing ? + * @private + */ + $$replace: false, + + /** + * @ngdoc method + * @name $location#absUrl + * + * @description + * This method is getter only. + * + * Return full url representation with all segments encoded according to rules specified in + * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). + * + * @return {string} full url + */ + absUrl: locationGetter('$$absUrl'), + + /** + * @ngdoc method + * @name $location#url + * + * @description + * This method is getter / setter. + * + * Return url (e.g. `/path?a=b#hash`) when called without any parameter. + * + * Change path, search and hash, when called with parameter and return `$location`. + * + * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) + * @param {string=} replace The path that will be changed + * @return {string} url + */ + url: function(url, replace) { + if (isUndefined(url)) + return this.$$url; + + var match = PATH_MATCH.exec(url); + if (match[1]) this.path(decodeURIComponent(match[1])); + if (match[2] || match[1]) this.search(match[3] || ''); + this.hash(match[5] || '', replace); + + return this; + }, + + /** + * @ngdoc method + * @name $location#protocol + * + * @description + * This method is getter only. + * + * Return protocol of current url. + * + * @return {string} protocol of current url + */ + protocol: locationGetter('$$protocol'), + + /** + * @ngdoc method + * @name $location#host + * + * @description + * This method is getter only. + * + * Return host of current url. + * + * @return {string} host of current url. + */ + host: locationGetter('$$host'), + + /** + * @ngdoc method + * @name $location#port + * + * @description + * This method is getter only. + * + * Return port of current url. + * + * @return {Number} port + */ + port: locationGetter('$$port'), + + /** + * @ngdoc method + * @name $location#path + * + * @description + * This method is getter / setter. + * + * Return path of current url when called without any parameter. + * + * Change path when called with parameter and return `$location`. + * + * Note: Path should always begin with forward slash (/), this method will add the forward slash + * if it is missing. + * + * @param {string=} path New path + * @return {string} path + */ + path: locationGetterSetter('$$path', function(path) { + return path.charAt(0) == '/' ? path : '/' + path; + }), + + /** + * @ngdoc method + * @name $location#search + * + * @description + * This method is getter / setter. + * + * Return search part (as object) of current url when called without any parameter. + * + * Change search part when called with parameter and return `$location`. + * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var searchObject = $location.search(); + * // => {foo: 'bar', baz: 'xoxo'} + * + * + * // set foo to 'yipee' + * $location.search('foo', 'yipee'); + * // => $location + * ``` + * + * @param {string|Object.|Object.>} search New search params - string or + * hash object. + * + * When called with a single argument the method acts as a setter, setting the `search` component + * of `$location` to the specified value. + * + * If the argument is a hash object containing an array of values, these values will be encoded + * as duplicate search parameters in the url. + * + * @param {(string|Array)=} paramValue If `search` is a string, then `paramValue` will + * override only a single search property. + * + * If `paramValue` is an array, it will override the property of the `search` component of + * `$location` specified via the first argument. + * + * If `paramValue` is `null`, the property specified via the first argument will be deleted. + * + * @return {Object} If called with no arguments returns the parsed `search` object. If called with + * one or more arguments returns `$location` object itself. + */ + search: function(search, paramValue) { + switch (arguments.length) { + case 0: + return this.$$search; + case 1: + if (isString(search)) { + this.$$search = parseKeyValue(search); + } else if (isObject(search)) { + this.$$search = search; + } else { + throw $locationMinErr('isrcharg', + 'The first argument of the `$location#search()` call must be a string or an object.'); + } + break; + default: + if (isUndefined(paramValue) || paramValue === null) { + delete this.$$search[search]; + } else { + this.$$search[search] = paramValue; + } + } + + this.$$compose(); + return this; + }, + + /** + * @ngdoc method + * @name $location#hash + * + * @description + * This method is getter / setter. + * + * Return hash fragment when called without any parameter. + * + * Change hash fragment when called with parameter and return `$location`. + * + * @param {string=} hash New hash fragment + * @return {string} hash + */ + hash: locationGetterSetter('$$hash', identity), + + /** + * @ngdoc method + * @name $location#replace + * + * @description + * If called, all changes to $location during current `$digest` will be replacing current history + * record, instead of adding new one. + */ + replace: function() { + this.$$replace = true; + return this; + } +}; + +function locationGetter(property) { + return function() { + return this[property]; + }; +} + + +function locationGetterSetter(property, preprocess) { + return function(value) { + if (isUndefined(value)) + return this[property]; + + this[property] = preprocess(value); + this.$$compose(); + + return this; + }; +} + + +/** + * @ngdoc service + * @name $location + * + * @requires $rootElement + * + * @description + * The $location service parses the URL in the browser address bar (based on the + * [window.location](https://developer.mozilla.org/en/window.location)) and makes the URL + * available to your application. Changes to the URL in the address bar are reflected into + * $location service and changes to $location are reflected into the browser address bar. + * + * **The $location service:** + * + * - Exposes the current URL in the browser address bar, so you can + * - Watch and observe the URL. + * - Change the URL. + * - Synchronizes the URL with the browser when the user + * - Changes the address bar. + * - Clicks the back or forward button (or clicks a History link). + * - Clicks on a link. + * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). + * + * For more information see {@link guide/$location Developer Guide: Using $location} + */ + +/** + * @ngdoc provider + * @name $locationProvider + * @description + * Use the `$locationProvider` to configure how the application deep linking paths are stored. + */ +function $LocationProvider(){ + var hashPrefix = '', + html5Mode = false; + + /** + * @ngdoc property + * @name $locationProvider#hashPrefix + * @description + * @param {string=} prefix Prefix for hash part (containing path and search) + * @returns {*} current value if used as getter or itself (chaining) if used as setter + */ + this.hashPrefix = function(prefix) { + if (isDefined(prefix)) { + hashPrefix = prefix; + return this; + } else { + return hashPrefix; + } + }; + + /** + * @ngdoc property + * @name $locationProvider#html5Mode + * @description + * @param {boolean=} mode Use HTML5 strategy if available. + * @returns {*} current value if used as getter or itself (chaining) if used as setter + */ + this.html5Mode = function(mode) { + if (isDefined(mode)) { + html5Mode = mode; + return this; + } else { + return html5Mode; + } + }; + + /** + * @ngdoc event + * @name $location#$locationChangeStart + * @eventType broadcast on root scope + * @description + * Broadcasted before a URL will change. This change can be prevented by calling + * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more + * details about event object. Upon successful change + * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. + * + * @param {Object} angularEvent Synthetic event object. + * @param {string} newUrl New URL + * @param {string=} oldUrl URL that was before it was changed. + */ + + /** + * @ngdoc event + * @name $location#$locationChangeSuccess + * @eventType broadcast on root scope + * @description + * Broadcasted after a URL was changed. + * + * @param {Object} angularEvent Synthetic event object. + * @param {string} newUrl New URL + * @param {string=} oldUrl URL that was before it was changed. + */ + + this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', + function( $rootScope, $browser, $sniffer, $rootElement) { + var $location, + LocationMode, + baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' + initialUrl = $browser.url(), + appBase; + + if (html5Mode) { + appBase = serverBase(initialUrl) + (baseHref || '/'); + LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; + } else { + appBase = stripHash(initialUrl); + LocationMode = LocationHashbangUrl; + } + $location = new LocationMode(appBase, '#' + hashPrefix); + $location.$$parse($location.$$rewrite(initialUrl)); + + $rootElement.on('click', function(event) { + // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) + // currently we open nice url link and redirect then + + if (event.ctrlKey || event.metaKey || event.which == 2) return; + + var elm = jqLite(event.target); + + // traverse the DOM up to find first A tag + while (lowercase(elm[0].nodeName) !== 'a') { + // ignore rewriting if no A tag (reached root element, or no parent - removed from document) + if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; + } + + var absHref = elm.prop('href'); + + if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { + // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during + // an animation. + absHref = urlResolve(absHref.animVal).href; + } + + // Make relative links work in HTML5 mode for legacy browsers (or at least IE8 & 9) + // The href should be a regular url e.g. /link/somewhere or link/somewhere or ../somewhere or + // somewhere#anchor or http://example.com/somewhere + if (LocationMode === LocationHashbangInHtml5Url) { + // get the actual href attribute - see + // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx + var href = elm.attr('href') || elm.attr('xlink:href'); + + if (href.indexOf('://') < 0) { // Ignore absolute URLs + var prefix = '#' + hashPrefix; + if (href[0] == '/') { + // absolute path - replace old path + absHref = appBase + prefix + href; + } else if (href[0] == '#') { + // local anchor + absHref = appBase + prefix + ($location.path() || '/') + href; + } else { + // relative path - join with current path + var stack = $location.path().split("/"), + parts = href.split("/"); + for (var i=0; i html5 url + if ($location.absUrl() != initialUrl) { + $browser.url($location.absUrl(), true); + } + + // update $location when $browser url changes + $browser.onUrlChange(function(newUrl) { + if ($location.absUrl() != newUrl) { + $rootScope.$evalAsync(function() { + var oldUrl = $location.absUrl(); + + $location.$$parse(newUrl); + if ($rootScope.$broadcast('$locationChangeStart', newUrl, + oldUrl).defaultPrevented) { + $location.$$parse(oldUrl); + $browser.url(oldUrl); + } else { + afterLocationChange(oldUrl); + } + }); + if (!$rootScope.$$phase) $rootScope.$digest(); + } + }); + + // update browser + var changeCounter = 0; + $rootScope.$watch(function $locationWatch() { + var oldUrl = $browser.url(); + var currentReplace = $location.$$replace; + + if (!changeCounter || oldUrl != $location.absUrl()) { + changeCounter++; + $rootScope.$evalAsync(function() { + if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). + defaultPrevented) { + $location.$$parse(oldUrl); + } else { + $browser.url($location.absUrl(), currentReplace); + afterLocationChange(oldUrl); + } + }); + } + $location.$$replace = false; + + return changeCounter; + }); + + return $location; + + function afterLocationChange(oldUrl) { + $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); + } +}]; +} + +/** + * @ngdoc service + * @name $log + * @requires $window + * + * @description + * Simple service for logging. Default implementation safely writes the message + * into the browser's console (if present). + * + * The main purpose of this service is to simplify debugging and troubleshooting. + * + * The default is to log `debug` messages. You can use + * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. + * + * @example + + + function LogCtrl($scope, $log) { + $scope.$log = $log; + $scope.message = 'Hello World!'; + } + + +
+

Reload this page with open console, enter text and hit the log button...

+ Message: + + + + + +
+
+
+ */ + +/** + * @ngdoc provider + * @name $logProvider + * @description + * Use the `$logProvider` to configure how the application logs messages + */ +function $LogProvider(){ + var debug = true, + self = this; + + /** + * @ngdoc property + * @name $logProvider#debugEnabled + * @description + * @param {boolean=} flag enable or disable debug level messages + * @returns {*} current value if used as getter or itself (chaining) if used as setter + */ + this.debugEnabled = function(flag) { + if (isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; + + this.$get = ['$window', function($window){ + return { + /** + * @ngdoc method + * @name $log#log + * + * @description + * Write a log message + */ + log: consoleLog('log'), + + /** + * @ngdoc method + * @name $log#info + * + * @description + * Write an information message + */ + info: consoleLog('info'), + + /** + * @ngdoc method + * @name $log#warn + * + * @description + * Write a warning message + */ + warn: consoleLog('warn'), + + /** + * @ngdoc method + * @name $log#error + * + * @description + * Write an error message + */ + error: consoleLog('error'), + + /** + * @ngdoc method + * @name $log#debug + * + * @description + * Write a debug message + */ + debug: (function () { + var fn = consoleLog('debug'); + + return function() { + if (debug) { + fn.apply(self, arguments); + } + }; + }()) + }; + + function formatError(arg) { + if (arg instanceof Error) { + if (arg.stack) { + arg = (arg.message && arg.stack.indexOf(arg.message) === -1) + ? 'Error: ' + arg.message + '\n' + arg.stack + : arg.stack; + } else if (arg.sourceURL) { + arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; + } + } + return arg; + } + + function consoleLog(type) { + var console = $window.console || {}, + logFn = console[type] || console.log || noop, + hasApply = false; + + // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. + // The reason behind this is that console.log has type "object" in IE8... + try { + hasApply = !!logFn.apply; + } catch (e) {} + + if (hasApply) { + return function() { + var args = []; + forEach(arguments, function(arg) { + args.push(formatError(arg)); + }); + return logFn.apply(console, args); + }; + } + + // we are IE which either doesn't have window.console => this is noop and we do nothing, + // or we are IE where console.log doesn't have apply so we log at least first 2 args + return function(arg1, arg2) { + logFn(arg1, arg2 == null ? '' : arg2); + }; + } + }]; +} + +var $parseMinErr = minErr('$parse'); +var promiseWarningCache = {}; +var promiseWarning; + +// Sandboxing Angular Expressions +// ------------------------------ +// Angular expressions are generally considered safe because these expressions only have direct +// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by +// obtaining a reference to native JS functions such as the Function constructor. +// +// As an example, consider the following Angular expression: +// +// {}.toString.constructor(alert("evil JS code")) +// +// We want to prevent this type of access. For the sake of performance, during the lexing phase we +// disallow any "dotted" access to any member named "constructor". +// +// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor +// while evaluating the expression, which is a stronger but more expensive test. Since reflective +// calls are expensive anyway, this is not such a big deal compared to static dereferencing. +// +// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits +// against the expression language, but not to prevent exploits that were enabled by exposing +// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good +// practice and therefore we are not even trying to protect against interaction with an object +// explicitly exposed in this way. +// +// A developer could foil the name check by aliasing the Function constructor under a different +// name on the scope. +// +// In general, it is not possible to access a Window object from an angular expression unless a +// window or some DOM object that has a reference to window is published onto a Scope. + +function ensureSafeMemberName(name, fullExpression) { + if (name === "constructor") { + throw $parseMinErr('isecfld', + 'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } + return name; +} + +function ensureSafeObject(obj, fullExpression) { + // nifty check if obj is Function that is fast and works across iframes and other contexts + if (obj) { + if (obj.constructor === obj) { + throw $parseMinErr('isecfn', + 'Referencing Function in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (// isWindow(obj) + obj.document && obj.location && obj.alert && obj.setInterval) { + throw $parseMinErr('isecwindow', + 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (// isElement(obj) + obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { + throw $parseMinErr('isecdom', + 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } + } + return obj; +} + +var OPERATORS = { + /* jshint bitwise : false */ + 'null':function(){return null;}, + 'true':function(){return true;}, + 'false':function(){return false;}, + undefined:noop, + '+':function(self, locals, a,b){ + a=a(self, locals); b=b(self, locals); + if (isDefined(a)) { + if (isDefined(b)) { + return a + b; + } + return a; + } + return isDefined(b)?b:undefined;}, + '-':function(self, locals, a,b){ + a=a(self, locals); b=b(self, locals); + return (isDefined(a)?a:0)-(isDefined(b)?b:0); + }, + '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);}, + '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);}, + '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, + '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, + '=':noop, + '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);}, + '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);}, + '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, + '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, + '<':function(self, locals, a,b){return a(self, locals)':function(self, locals, a,b){return a(self, locals)>b(self, locals);}, + '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);}, + '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);}, + '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);}, + '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);}, + '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);}, +// '|':function(self, locals, a,b){return a|b;}, + '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, + '!':function(self, locals, a){return !a(self, locals);} +}; +/* jshint bitwise: true */ +var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; + + +///////////////////////////////////////// + + +/** + * @constructor + */ +var Lexer = function (options) { + this.options = options; +}; + +Lexer.prototype = { + constructor: Lexer, + + lex: function (text) { + this.text = text; + + this.index = 0; + this.ch = undefined; + this.lastCh = ':'; // can start regexp + + this.tokens = []; + + while (this.index < this.text.length) { + this.ch = this.text.charAt(this.index); + if (this.is('"\'')) { + this.readString(this.ch); + } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { + this.readNumber(); + } else if (this.isIdent(this.ch)) { + this.readIdent(); + } else if (this.is('(){}[].,;:?')) { + this.tokens.push({ + index: this.index, + text: this.ch + }); + this.index++; + } else if (this.isWhitespace(this.ch)) { + this.index++; + continue; + } else { + var ch2 = this.ch + this.peek(); + var ch3 = ch2 + this.peek(2); + var fn = OPERATORS[this.ch]; + var fn2 = OPERATORS[ch2]; + var fn3 = OPERATORS[ch3]; + if (fn3) { + this.tokens.push({index: this.index, text: ch3, fn: fn3}); + this.index += 3; + } else if (fn2) { + this.tokens.push({index: this.index, text: ch2, fn: fn2}); + this.index += 2; + } else if (fn) { + this.tokens.push({ + index: this.index, + text: this.ch, + fn: fn + }); + this.index += 1; + } else { + this.throwError('Unexpected next character ', this.index, this.index + 1); + } + } + this.lastCh = this.ch; + } + return this.tokens; + }, + + is: function(chars) { + return chars.indexOf(this.ch) !== -1; + }, + + was: function(chars) { + return chars.indexOf(this.lastCh) !== -1; + }, + + peek: function(i) { + var num = i || 1; + return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; + }, + + isNumber: function(ch) { + return ('0' <= ch && ch <= '9'); + }, + + isWhitespace: function(ch) { + // IE treats non-breaking space as \u00A0 + return (ch === ' ' || ch === '\r' || ch === '\t' || + ch === '\n' || ch === '\v' || ch === '\u00A0'); + }, + + isIdent: function(ch) { + return ('a' <= ch && ch <= 'z' || + 'A' <= ch && ch <= 'Z' || + '_' === ch || ch === '$'); + }, + + isExpOperator: function(ch) { + return (ch === '-' || ch === '+' || this.isNumber(ch)); + }, + + throwError: function(error, start, end) { + end = end || this.index; + var colStr = (isDefined(start) + ? 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']' + : ' ' + end); + throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].', + error, colStr, this.text); + }, + + readNumber: function() { + var number = ''; + var start = this.index; + while (this.index < this.text.length) { + var ch = lowercase(this.text.charAt(this.index)); + if (ch == '.' || this.isNumber(ch)) { + number += ch; + } else { + var peekCh = this.peek(); + if (ch == 'e' && this.isExpOperator(peekCh)) { + number += ch; + } else if (this.isExpOperator(ch) && + peekCh && this.isNumber(peekCh) && + number.charAt(number.length - 1) == 'e') { + number += ch; + } else if (this.isExpOperator(ch) && + (!peekCh || !this.isNumber(peekCh)) && + number.charAt(number.length - 1) == 'e') { + this.throwError('Invalid exponent'); + } else { + break; + } + } + this.index++; + } + number = 1 * number; + this.tokens.push({ + index: start, + text: number, + literal: true, + constant: true, + fn: function() { return number; } + }); + }, + + readIdent: function() { + var parser = this; + + var ident = ''; + var start = this.index; + + var lastDot, peekIndex, methodName, ch; + + while (this.index < this.text.length) { + ch = this.text.charAt(this.index); + if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { + if (ch === '.') lastDot = this.index; + ident += ch; + } else { + break; + } + this.index++; + } + + //check if this is not a method invocation and if it is back out to last dot + if (lastDot) { + peekIndex = this.index; + while (peekIndex < this.text.length) { + ch = this.text.charAt(peekIndex); + if (ch === '(') { + methodName = ident.substr(lastDot - start + 1); + ident = ident.substr(0, lastDot - start); + this.index = peekIndex; + break; + } + if (this.isWhitespace(ch)) { + peekIndex++; + } else { + break; + } + } + } + + + var token = { + index: start, + text: ident + }; + + // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn + if (OPERATORS.hasOwnProperty(ident)) { + token.fn = OPERATORS[ident]; + token.literal = true; + token.constant = true; + } else { + var getter = getterFn(ident, this.options, this.text); + token.fn = extend(function(self, locals) { + return (getter(self, locals)); + }, { + assign: function(self, value) { + return setter(self, ident, value, parser.text, parser.options); + } + }); + } + + this.tokens.push(token); + + if (methodName) { + this.tokens.push({ + index:lastDot, + text: '.' + }); + this.tokens.push({ + index: lastDot + 1, + text: methodName + }); + } + }, + + readString: function(quote) { + var start = this.index; + this.index++; + var string = ''; + var rawString = quote; + var escape = false; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + rawString += ch; + if (escape) { + if (ch === 'u') { + var hex = this.text.substring(this.index + 1, this.index + 5); + if (!hex.match(/[\da-f]{4}/i)) + this.throwError('Invalid unicode escape [\\u' + hex + ']'); + this.index += 4; + string += String.fromCharCode(parseInt(hex, 16)); + } else { + var rep = ESCAPE[ch]; + if (rep) { + string += rep; + } else { + string += ch; + } + } + escape = false; + } else if (ch === '\\') { + escape = true; + } else if (ch === quote) { + this.index++; + this.tokens.push({ + index: start, + text: rawString, + string: string, + literal: true, + constant: true, + fn: function() { return string; } + }); + return; + } else { + string += ch; + } + this.index++; + } + this.throwError('Unterminated quote', start); + } +}; + + +/** + * @constructor + */ +var Parser = function (lexer, $filter, options) { + this.lexer = lexer; + this.$filter = $filter; + this.options = options; +}; + +Parser.ZERO = extend(function () { + return 0; +}, { + constant: true +}); + +Parser.prototype = { + constructor: Parser, + + parse: function (text) { + this.text = text; + + this.tokens = this.lexer.lex(text); + + var value = this.statements(); + + if (this.tokens.length !== 0) { + this.throwError('is an unexpected token', this.tokens[0]); + } + + value.literal = !!value.literal; + value.constant = !!value.constant; + + return value; + }, + + primary: function () { + var primary; + if (this.expect('(')) { + primary = this.filterChain(); + this.consume(')'); + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else { + var token = this.expect(); + primary = token.fn; + if (!primary) { + this.throwError('not a primary expression', token); + } + primary.literal = !!token.literal; + primary.constant = !!token.constant; + } + + var next, context; + while ((next = this.expect('(', '[', '.'))) { + if (next.text === '(') { + primary = this.functionCall(primary, context); + context = null; + } else if (next.text === '[') { + context = primary; + primary = this.objectIndex(primary); + } else if (next.text === '.') { + context = primary; + primary = this.fieldAccess(primary); + } else { + this.throwError('IMPOSSIBLE'); + } + } + return primary; + }, + + throwError: function(msg, token) { + throw $parseMinErr('syntax', + 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', + token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); + }, + + peekToken: function() { + if (this.tokens.length === 0) + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + return this.tokens[0]; + }, + + peek: function(e1, e2, e3, e4) { + if (this.tokens.length > 0) { + var token = this.tokens[0]; + var t = token.text; + if (t === e1 || t === e2 || t === e3 || t === e4 || + (!e1 && !e2 && !e3 && !e4)) { + return token; + } + } + return false; + }, + + expect: function(e1, e2, e3, e4){ + var token = this.peek(e1, e2, e3, e4); + if (token) { + this.tokens.shift(); + return token; + } + return false; + }, + + consume: function(e1){ + if (!this.expect(e1)) { + this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); + } + }, + + unaryFn: function(fn, right) { + return extend(function(self, locals) { + return fn(self, locals, right); + }, { + constant:right.constant + }); + }, + + ternaryFn: function(left, middle, right){ + return extend(function(self, locals){ + return left(self, locals) ? middle(self, locals) : right(self, locals); + }, { + constant: left.constant && middle.constant && right.constant + }); + }, + + binaryFn: function(left, fn, right) { + return extend(function(self, locals) { + return fn(self, locals, left, right); + }, { + constant:left.constant && right.constant + }); + }, + + statements: function() { + var statements = []; + while (true) { + if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) + statements.push(this.filterChain()); + if (!this.expect(';')) { + // optimize for the common case where there is only one statement. + // TODO(size): maybe we should not support multiple statements? + return (statements.length === 1) + ? statements[0] + : function(self, locals) { + var value; + for (var i = 0; i < statements.length; i++) { + var statement = statements[i]; + if (statement) { + value = statement(self, locals); + } + } + return value; + }; + } + } + }, + + filterChain: function() { + var left = this.expression(); + var token; + while (true) { + if ((token = this.expect('|'))) { + left = this.binaryFn(left, token.fn, this.filter()); + } else { + return left; + } + } + }, + + filter: function() { + var token = this.expect(); + var fn = this.$filter(token.text); + var argsFn = []; + while (true) { + if ((token = this.expect(':'))) { + argsFn.push(this.expression()); + } else { + var fnInvoke = function(self, locals, input) { + var args = [input]; + for (var i = 0; i < argsFn.length; i++) { + args.push(argsFn[i](self, locals)); + } + return fn.apply(self, args); + }; + return function() { + return fnInvoke; + }; + } + } + }, + + expression: function() { + return this.assignment(); + }, + + assignment: function() { + var left = this.ternary(); + var right; + var token; + if ((token = this.expect('='))) { + if (!left.assign) { + this.throwError('implies assignment but [' + + this.text.substring(0, token.index) + '] can not be assigned to', token); + } + right = this.ternary(); + return function(scope, locals) { + return left.assign(scope, right(scope, locals), locals); + }; + } + return left; + }, + + ternary: function() { + var left = this.logicalOR(); + var middle; + var token; + if ((token = this.expect('?'))) { + middle = this.ternary(); + if ((token = this.expect(':'))) { + return this.ternaryFn(left, middle, this.ternary()); + } else { + this.throwError('expected :', token); + } + } else { + return left; + } + }, + + logicalOR: function() { + var left = this.logicalAND(); + var token; + while (true) { + if ((token = this.expect('||'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); + } else { + return left; + } + } + }, + + logicalAND: function() { + var left = this.equality(); + var token; + if ((token = this.expect('&&'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); + } + return left; + }, + + equality: function() { + var left = this.relational(); + var token; + if ((token = this.expect('==','!=','===','!=='))) { + left = this.binaryFn(left, token.fn, this.equality()); + } + return left; + }, + + relational: function() { + var left = this.additive(); + var token; + if ((token = this.expect('<', '>', '<=', '>='))) { + left = this.binaryFn(left, token.fn, this.relational()); + } + return left; + }, + + additive: function() { + var left = this.multiplicative(); + var token; + while ((token = this.expect('+','-'))) { + left = this.binaryFn(left, token.fn, this.multiplicative()); + } + return left; + }, + + multiplicative: function() { + var left = this.unary(); + var token; + while ((token = this.expect('*','/','%'))) { + left = this.binaryFn(left, token.fn, this.unary()); + } + return left; + }, + + unary: function() { + var token; + if (this.expect('+')) { + return this.primary(); + } else if ((token = this.expect('-'))) { + return this.binaryFn(Parser.ZERO, token.fn, this.unary()); + } else if ((token = this.expect('!'))) { + return this.unaryFn(token.fn, this.unary()); + } else { + return this.primary(); + } + }, + + fieldAccess: function(object) { + var parser = this; + var field = this.expect().text; + var getter = getterFn(field, this.options, this.text); + + return extend(function(scope, locals, self) { + return getter(self || object(scope, locals)); + }, { + assign: function(scope, value, locals) { + return setter(object(scope, locals), field, value, parser.text, parser.options); + } + }); + }, + + objectIndex: function(obj) { + var parser = this; + + var indexFn = this.expression(); + this.consume(']'); + + return extend(function(self, locals) { + var o = obj(self, locals), + i = indexFn(self, locals), + v, p; + + if (!o) return undefined; + v = ensureSafeObject(o[i], parser.text); + if (v && v.then && parser.options.unwrapPromises) { + p = v; + if (!('$$v' in v)) { + p.$$v = undefined; + p.then(function(val) { p.$$v = val; }); + } + v = v.$$v; + } + return v; + }, { + assign: function(self, value, locals) { + var key = indexFn(self, locals); + // prevent overwriting of Function.constructor which would break ensureSafeObject check + var safe = ensureSafeObject(obj(self, locals), parser.text); + return safe[key] = value; + } + }); + }, + + functionCall: function(fn, contextGetter) { + var argsFn = []; + if (this.peekToken().text !== ')') { + do { + argsFn.push(this.expression()); + } while (this.expect(',')); + } + this.consume(')'); + + var parser = this; + + return function(scope, locals) { + var args = []; + var context = contextGetter ? contextGetter(scope, locals) : scope; + + for (var i = 0; i < argsFn.length; i++) { + args.push(argsFn[i](scope, locals)); + } + var fnPtr = fn(scope, locals, context) || noop; + + ensureSafeObject(context, parser.text); + ensureSafeObject(fnPtr, parser.text); + + // IE stupidity! (IE doesn't have apply for some native functions) + var v = fnPtr.apply + ? fnPtr.apply(context, args) + : fnPtr(args[0], args[1], args[2], args[3], args[4]); + + return ensureSafeObject(v, parser.text); + }; + }, + + // This is used with json array declaration + arrayDeclaration: function () { + var elementFns = []; + var allConstant = true; + if (this.peekToken().text !== ']') { + do { + if (this.peek(']')) { + // Support trailing commas per ES5.1. + break; + } + var elementFn = this.expression(); + elementFns.push(elementFn); + if (!elementFn.constant) { + allConstant = false; + } + } while (this.expect(',')); + } + this.consume(']'); + + return extend(function(self, locals) { + var array = []; + for (var i = 0; i < elementFns.length; i++) { + array.push(elementFns[i](self, locals)); + } + return array; + }, { + literal: true, + constant: allConstant + }); + }, + + object: function () { + var keyValues = []; + var allConstant = true; + if (this.peekToken().text !== '}') { + do { + if (this.peek('}')) { + // Support trailing commas per ES5.1. + break; + } + var token = this.expect(), + key = token.string || token.text; + this.consume(':'); + var value = this.expression(); + keyValues.push({key: key, value: value}); + if (!value.constant) { + allConstant = false; + } + } while (this.expect(',')); + } + this.consume('}'); + + return extend(function(self, locals) { + var object = {}; + for (var i = 0; i < keyValues.length; i++) { + var keyValue = keyValues[i]; + object[keyValue.key] = keyValue.value(self, locals); + } + return object; + }, { + literal: true, + constant: allConstant + }); + } +}; + + +////////////////////////////////////////////////// +// Parser helper functions +////////////////////////////////////////////////// + +function setter(obj, path, setValue, fullExp, options) { + //needed? + options = options || {}; + + var element = path.split('.'), key; + for (var i = 0; element.length > 1; i++) { + key = ensureSafeMemberName(element.shift(), fullExp); + var propertyObj = obj[key]; + if (!propertyObj) { + propertyObj = {}; + obj[key] = propertyObj; + } + obj = propertyObj; + if (obj.then && options.unwrapPromises) { + promiseWarning(fullExp); + if (!("$$v" in obj)) { + (function(promise) { + promise.then(function(val) { promise.$$v = val; }); } + )(obj); + } + if (obj.$$v === undefined) { + obj.$$v = {}; + } + obj = obj.$$v; + } + } + key = ensureSafeMemberName(element.shift(), fullExp); + obj[key] = setValue; + return setValue; +} + +var getterFnCache = {}; + +/** + * Implementation of the "Black Hole" variant from: + * - http://jsperf.com/angularjs-parse-getter/4 + * - http://jsperf.com/path-evaluation-simplified/7 + */ +function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { + ensureSafeMemberName(key0, fullExp); + ensureSafeMemberName(key1, fullExp); + ensureSafeMemberName(key2, fullExp); + ensureSafeMemberName(key3, fullExp); + ensureSafeMemberName(key4, fullExp); + + return !options.unwrapPromises + ? function cspSafeGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; + + if (pathVal == null) return pathVal; + pathVal = pathVal[key0]; + + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key1]; + + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key2]; + + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key3]; + + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key4]; + + return pathVal; + } + : function cspSafePromiseEnabledGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, + promise; + + if (pathVal == null) return pathVal; + + pathVal = pathVal[key0]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key1]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key2]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key3]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key4]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + return pathVal; + }; +} + +function simpleGetterFn1(key0, fullExp) { + ensureSafeMemberName(key0, fullExp); + + return function simpleGetterFn1(scope, locals) { + if (scope == null) return undefined; + return ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; + }; +} + +function simpleGetterFn2(key0, key1, fullExp) { + ensureSafeMemberName(key0, fullExp); + ensureSafeMemberName(key1, fullExp); + + return function simpleGetterFn2(scope, locals) { + if (scope == null) return undefined; + scope = ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; + return scope == null ? undefined : scope[key1]; + }; +} + +function getterFn(path, options, fullExp) { + // Check whether the cache has this getter already. + // We can use hasOwnProperty directly on the cache because we ensure, + // see below, that the cache never stores a path called 'hasOwnProperty' + if (getterFnCache.hasOwnProperty(path)) { + return getterFnCache[path]; + } + + var pathKeys = path.split('.'), + pathKeysLength = pathKeys.length, + fn; + + // When we have only 1 or 2 tokens, use optimized special case closures. + // http://jsperf.com/angularjs-parse-getter/6 + if (!options.unwrapPromises && pathKeysLength === 1) { + fn = simpleGetterFn1(pathKeys[0], fullExp); + } else if (!options.unwrapPromises && pathKeysLength === 2) { + fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp); + } else if (options.csp) { + if (pathKeysLength < 6) { + fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, + options); + } else { + fn = function(scope, locals) { + var i = 0, val; + do { + val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], + pathKeys[i++], fullExp, options)(scope, locals); + + locals = undefined; // clear after first iteration + scope = val; + } while (i < pathKeysLength); + return val; + }; + } + } else { + var code = 'var p;\n'; + forEach(pathKeys, function(key, index) { + ensureSafeMemberName(key, fullExp); + code += 'if(s == null) return undefined;\n' + + 's='+ (index + // we simply dereference 's' on any .dot notation + ? 's' + // but if we are first then we check locals first, and if so read it first + : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + + (options.unwrapPromises + ? 'if (s && s.then) {\n' + + ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' + + ' if (!("$$v" in s)) {\n' + + ' p=s;\n' + + ' p.$$v = undefined;\n' + + ' p.then(function(v) {p.$$v=v;});\n' + + '}\n' + + ' s=s.$$v\n' + + '}\n' + : ''); + }); + code += 'return s;'; + + /* jshint -W054 */ + var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning + /* jshint +W054 */ + evaledFnGetter.toString = valueFn(code); + fn = options.unwrapPromises ? function(scope, locals) { + return evaledFnGetter(scope, locals, promiseWarning); + } : evaledFnGetter; + } + + // Only cache the value if it's not going to mess up the cache object + // This is more performant that using Object.prototype.hasOwnProperty.call + if (path !== 'hasOwnProperty') { + getterFnCache[path] = fn; + } + return fn; +} + +/////////////////////////////////// + +/** + * @ngdoc service + * @name $parse + * @kind function + * + * @description + * + * Converts Angular {@link guide/expression expression} into a function. + * + * ```js + * var getter = $parse('user.name'); + * var setter = getter.assign; + * var context = {user:{name:'angular'}}; + * var locals = {user:{name:'local'}}; + * + * expect(getter(context)).toEqual('angular'); + * setter(context, 'newValue'); + * expect(context.user.name).toEqual('newValue'); + * expect(getter(context, locals)).toEqual('local'); + * ``` + * + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + * + * The returned function also has the following properties: + * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript + * literal. + * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript + * constant literals. + * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be + * set to a function to change its value on the given context. + * + */ + + +/** + * @ngdoc provider + * @name $parseProvider + * @kind function + * + * @description + * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} + * service. + */ +function $ParseProvider() { + var cache = {}; + + var $parseOptions = { + csp: false, + unwrapPromises: false, + logPromiseWarnings: true + }; + + + /** + * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. + * + * @ngdoc method + * @name $parseProvider#unwrapPromises + * @description + * + * **This feature is deprecated, see deprecation notes below for more info** + * + * If set to true (default is false), $parse will unwrap promises automatically when a promise is + * found at any part of the expression. In other words, if set to true, the expression will always + * result in a non-promise value. + * + * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, + * the fulfillment value is used in place of the promise while evaluating the expression. + * + * **Deprecation notice** + * + * This is a feature that didn't prove to be wildly useful or popular, primarily because of the + * dichotomy between data access in templates (accessed as raw values) and controller code + * (accessed as promises). + * + * In most code we ended up resolving promises manually in controllers anyway and thus unifying + * the model access there. + * + * Other downsides of automatic promise unwrapping: + * + * - when building components it's often desirable to receive the raw promises + * - adds complexity and slows down expression evaluation + * - makes expression code pre-generation unattractive due to the amount of code that needs to be + * generated + * - makes IDE auto-completion and tool support hard + * + * **Warning Logs** + * + * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a + * promise (to reduce the noise, each expression is logged only once). To disable this logging use + * `$parseProvider.logPromiseWarnings(false)` api. + * + * + * @param {boolean=} value New value. + * @returns {boolean|self} Returns the current setting when used as getter and self if used as + * setter. + */ + this.unwrapPromises = function(value) { + if (isDefined(value)) { + $parseOptions.unwrapPromises = !!value; + return this; + } else { + return $parseOptions.unwrapPromises; + } + }; + + + /** + * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. + * + * @ngdoc method + * @name $parseProvider#logPromiseWarnings + * @description + * + * Controls whether Angular should log a warning on any encounter of a promise in an expression. + * + * The default is set to `true`. + * + * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. + * + * @param {boolean=} value New value. + * @returns {boolean|self} Returns the current setting when used as getter and self if used as + * setter. + */ + this.logPromiseWarnings = function(value) { + if (isDefined(value)) { + $parseOptions.logPromiseWarnings = value; + return this; + } else { + return $parseOptions.logPromiseWarnings; + } + }; + + + this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { + $parseOptions.csp = $sniffer.csp; + + promiseWarning = function promiseWarningFn(fullExp) { + if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; + promiseWarningCache[fullExp] = true; + $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + + 'Automatic unwrapping of promises in Angular expressions is deprecated.'); + }; + + return function(exp) { + var parsedExpression; + + switch (typeof exp) { + case 'string': + + if (cache.hasOwnProperty(exp)) { + return cache[exp]; + } + + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + parsedExpression = parser.parse(exp); + + if (exp !== 'hasOwnProperty') { + // Only cache the value if it's not going to mess up the cache object + // This is more performant that using Object.prototype.hasOwnProperty.call + cache[exp] = parsedExpression; + } + + return parsedExpression; + + case 'function': + return exp; + + default: + return noop; + } + }; + }]; +} + +/** + * @ngdoc service + * @name $q + * @requires $rootScope + * + * @description + * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). + * + * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an + * interface for interacting with an object that represents the result of an action that is + * performed asynchronously, and may or may not be finished at any given point in time. + * + * From the perspective of dealing with error handling, deferred and promise APIs are to + * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. + * + * ```js + * // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet` + * // are available in the current lexical scope (they could have been injected or passed in). + * + * function asyncGreet(name) { + * var deferred = $q.defer(); + * + * setTimeout(function() { + * // since this fn executes async in a future turn of the event loop, we need to wrap + * // our code into an $apply call so that the model changes are properly observed. + * scope.$apply(function() { + * deferred.notify('About to greet ' + name + '.'); + * + * if (okToGreet(name)) { + * deferred.resolve('Hello, ' + name + '!'); + * } else { + * deferred.reject('Greeting ' + name + ' is not allowed.'); + * } + * }); + * }, 1000); + * + * return deferred.promise; + * } + * + * var promise = asyncGreet('Robin Hood'); + * promise.then(function(greeting) { + * alert('Success: ' + greeting); + * }, function(reason) { + * alert('Failed: ' + reason); + * }, function(update) { + * alert('Got notification: ' + update); + * }); + * ``` + * + * At first it might not be obvious why this extra complexity is worth the trouble. The payoff + * comes in the way of guarantees that promise and deferred APIs make, see + * https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md. + * + * Additionally the promise api allows for composition that is very hard to do with the + * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. + * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the + * section on serial or parallel joining of promises. + * + * + * # The Deferred API + * + * A new instance of deferred is constructed by calling `$q.defer()`. + * + * The purpose of the deferred object is to expose the associated Promise instance as well as APIs + * that can be used for signaling the successful or unsuccessful completion, as well as the status + * of the task. + * + * **Methods** + * + * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection + * constructed via `$q.reject`, the promise will be rejected instead. + * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to + * resolving it with a rejection constructed via `$q.reject`. + * - `notify(value)` - provides updates on the status of the promise's execution. This may be called + * multiple times before the promise is either resolved or rejected. + * + * **Properties** + * + * - promise – `{Promise}` – promise object associated with this deferred. + * + * + * # The Promise API + * + * A new promise instance is created when a deferred instance is created and can be retrieved by + * calling `deferred.promise`. + * + * The purpose of the promise object is to allow for interested parties to get access to the result + * of the deferred task when it completes. + * + * **Methods** + * + * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or + * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously + * as soon as the result is available. The callbacks are called with a single argument: the result + * or rejection reason. Additionally, the notify callback may be called zero or more times to + * provide a progress indication, before the promise is resolved or rejected. + * + * This method *returns a new promise* which is resolved or rejected via the return value of the + * `successCallback`, `errorCallback`. It also notifies via the return value of the + * `notifyCallback` method. The promise can not be resolved or rejected from the notifyCallback + * method. + * + * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` + * + * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise, + * but to do so without modifying the final value. This is useful to release resources or do some + * clean-up that needs to be done whether the promise was rejected or resolved. See the [full + * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for + * more information. + * + * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as + * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to + * make your code IE8 and Android 2.x compatible. + * + * # Chaining promises + * + * Because calling the `then` method of a promise returns a new derived promise, it is easily + * possible to create a chain of promises: + * + * ```js + * promiseB = promiseA.then(function(result) { + * return result + 1; + * }); + * + * // promiseB will be resolved immediately after promiseA is resolved and its value + * // will be the result of promiseA incremented by 1 + * ``` + * + * It is possible to create chains of any length and since a promise can be resolved with another + * promise (which will defer its resolution further), it is possible to pause/defer resolution of + * the promises at any point in the chain. This makes it possible to implement powerful APIs like + * $http's response interceptors. + * + * + * # Differences between Kris Kowal's Q and $q + * + * There are two main differences: + * + * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation + * mechanism in angular, which means faster propagation of resolution or rejection into your + * models and avoiding unnecessary browser repaints, which would result in flickering UI. + * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains + * all the important functionality needed for common async tasks. + * + * # Testing + * + * ```js + * it('should simulate promise', inject(function($q, $rootScope) { + * var deferred = $q.defer(); + * var promise = deferred.promise; + * var resolvedValue; + * + * promise.then(function(value) { resolvedValue = value; }); + * expect(resolvedValue).toBeUndefined(); + * + * // Simulate resolving of promise + * deferred.resolve(123); + * // Note that the 'then' function does not get called synchronously. + * // This is because we want the promise API to always be async, whether or not + * // it got called synchronously or asynchronously. + * expect(resolvedValue).toBeUndefined(); + * + * // Propagate promise resolution to 'then' functions using $apply(). + * $rootScope.$apply(); + * expect(resolvedValue).toEqual(123); + * })); + * ``` + */ +function $QProvider() { + + this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { + return qFactory(function(callback) { + $rootScope.$evalAsync(callback); + }, $exceptionHandler); + }]; +} + + +/** + * Constructs a promise manager. + * + * @param {function(Function)} nextTick Function for executing functions in the next turn. + * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for + * debugging purposes. + * @returns {object} Promise manager. + */ +function qFactory(nextTick, exceptionHandler) { + + /** + * @ngdoc method + * @name $q#defer + * @kind function + * + * @description + * Creates a `Deferred` object which represents a task which will finish in the future. + * + * @returns {Deferred} Returns a new instance of deferred. + */ + var defer = function() { + var pending = [], + value, deferred; + + deferred = { + + resolve: function(val) { + if (pending) { + var callbacks = pending; + pending = undefined; + value = ref(val); + + if (callbacks.length) { + nextTick(function() { + var callback; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callback = callbacks[i]; + value.then(callback[0], callback[1], callback[2]); + } + }); + } + } + }, + + + reject: function(reason) { + deferred.resolve(createInternalRejectedPromise(reason)); + }, + + + notify: function(progress) { + if (pending) { + var callbacks = pending; + + if (pending.length) { + nextTick(function() { + var callback; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callback = callbacks[i]; + callback[2](progress); + } + }); + } + } + }, + + + promise: { + then: function(callback, errback, progressback) { + var result = defer(); + + var wrappedCallback = function(value) { + try { + result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); + } catch(e) { + result.reject(e); + exceptionHandler(e); + } + }; + + var wrappedErrback = function(reason) { + try { + result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); + } catch(e) { + result.reject(e); + exceptionHandler(e); + } + }; + + var wrappedProgressback = function(progress) { + try { + result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); + } catch(e) { + exceptionHandler(e); + } + }; + + if (pending) { + pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); + } else { + value.then(wrappedCallback, wrappedErrback, wrappedProgressback); + } + + return result.promise; + }, + + "catch": function(callback) { + return this.then(null, callback); + }, + + "finally": function(callback) { + + function makePromise(value, resolved) { + var result = defer(); + if (resolved) { + result.resolve(value); + } else { + result.reject(value); + } + return result.promise; + } + + function handleCallback(value, isResolved) { + var callbackOutput = null; + try { + callbackOutput = (callback ||defaultCallback)(); + } catch(e) { + return makePromise(e, false); + } + if (callbackOutput && isFunction(callbackOutput.then)) { + return callbackOutput.then(function() { + return makePromise(value, isResolved); + }, function(error) { + return makePromise(error, false); + }); + } else { + return makePromise(value, isResolved); + } + } + + return this.then(function(value) { + return handleCallback(value, true); + }, function(error) { + return handleCallback(error, false); + }); + } + } + }; + + return deferred; + }; + + + var ref = function(value) { + if (value && isFunction(value.then)) return value; + return { + then: function(callback) { + var result = defer(); + nextTick(function() { + result.resolve(callback(value)); + }); + return result.promise; + } + }; + }; + + + /** + * @ngdoc method + * @name $q#reject + * @kind function + * + * @description + * Creates a promise that is resolved as rejected with the specified `reason`. This api should be + * used to forward rejection in a chain of promises. If you are dealing with the last promise in + * a promise chain, you don't need to worry about it. + * + * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of + * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via + * a promise error callback and you want to forward the error to the promise derived from the + * current promise, you have to "rethrow" the error by returning a rejection constructed via + * `reject`. + * + * ```js + * promiseB = promiseA.then(function(result) { + * // success: do something and resolve promiseB + * // with the old or a new result + * return result; + * }, function(reason) { + * // error: handle the error if possible and + * // resolve promiseB with newPromiseOrValue, + * // otherwise forward the rejection to promiseB + * if (canHandle(reason)) { + * // handle the error and recover + * return newPromiseOrValue; + * } + * return $q.reject(reason); + * }); + * ``` + * + * @param {*} reason Constant, message, exception or an object representing the rejection reason. + * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. + */ + var reject = function(reason) { + var result = defer(); + result.reject(reason); + return result.promise; + }; + + var createInternalRejectedPromise = function(reason) { + return { + then: function(callback, errback) { + var result = defer(); + nextTick(function() { + try { + result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); + } catch(e) { + result.reject(e); + exceptionHandler(e); + } + }); + return result.promise; + } + }; + }; + + + /** + * @ngdoc method + * @name $q#when + * @kind function + * + * @description + * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. + * This is useful when you are dealing with an object that might or might not be a promise, or if + * the promise comes from a source that can't be trusted. + * + * @param {*} value Value or a promise + * @returns {Promise} Returns a promise of the passed value or promise + */ + var when = function(value, callback, errback, progressback) { + var result = defer(), + done; + + var wrappedCallback = function(value) { + try { + return (isFunction(callback) ? callback : defaultCallback)(value); + } catch (e) { + exceptionHandler(e); + return reject(e); + } + }; + + var wrappedErrback = function(reason) { + try { + return (isFunction(errback) ? errback : defaultErrback)(reason); + } catch (e) { + exceptionHandler(e); + return reject(e); + } + }; + + var wrappedProgressback = function(progress) { + try { + return (isFunction(progressback) ? progressback : defaultCallback)(progress); + } catch (e) { + exceptionHandler(e); + } + }; + + nextTick(function() { + ref(value).then(function(value) { + if (done) return; + done = true; + result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); + }, function(reason) { + if (done) return; + done = true; + result.resolve(wrappedErrback(reason)); + }, function(progress) { + if (done) return; + result.notify(wrappedProgressback(progress)); + }); + }); + + return result.promise; + }; + + + function defaultCallback(value) { + return value; + } + + + function defaultErrback(reason) { + return reject(reason); + } + + + /** + * @ngdoc method + * @name $q#all + * @kind function + * + * @description + * Combines multiple promises into a single promise that is resolved when all of the input + * promises are resolved. + * + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, + * each value corresponding to the promise at the same index/key in the `promises` array/hash. + * If any of the promises is resolved with a rejection, this resulting promise will be rejected + * with the same rejection value. + */ + function all(promises) { + var deferred = defer(), + counter = 0, + results = isArray(promises) ? [] : {}; + + forEach(promises, function(promise, key) { + counter++; + ref(promise).then(function(value) { + if (results.hasOwnProperty(key)) return; + results[key] = value; + if (!(--counter)) deferred.resolve(results); + }, function(reason) { + if (results.hasOwnProperty(key)) return; + deferred.reject(reason); + }); + }); + + if (counter === 0) { + deferred.resolve(results); + } + + return deferred.promise; + } + + return { + defer: defer, + reject: reject, + when: when, + all: all + }; +} + +function $$RAFProvider(){ //rAF + this.$get = ['$window', '$timeout', function($window, $timeout) { + var requestAnimationFrame = $window.requestAnimationFrame || + $window.webkitRequestAnimationFrame || + $window.mozRequestAnimationFrame; + + var cancelAnimationFrame = $window.cancelAnimationFrame || + $window.webkitCancelAnimationFrame || + $window.mozCancelAnimationFrame || + $window.webkitCancelRequestAnimationFrame; + + var rafSupported = !!requestAnimationFrame; + var raf = rafSupported + ? function(fn) { + var id = requestAnimationFrame(fn); + return function() { + cancelAnimationFrame(id); + }; + } + : function(fn) { + var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 + return function() { + $timeout.cancel(timer); + }; + }; + + raf.supported = rafSupported; + + return raf; + }]; +} + +/** + * DESIGN NOTES + * + * The design decisions behind the scope are heavily favored for speed and memory consumption. + * + * The typical use of scope is to watch the expressions, which most of the time return the same + * value as last time so we optimize the operation. + * + * Closures construction is expensive in terms of speed as well as memory: + * - No closures, instead use prototypical inheritance for API + * - Internal state needs to be stored on scope directly, which means that private state is + * exposed as $$____ properties + * + * Loop operations are optimized by using while(count--) { ... } + * - this means that in order to keep the same order of execution as addition we have to add + * items to the array at the beginning (unshift) instead of at the end (push) + * + * Child scopes are created and removed often + * - Using an array would be slow since inserts in middle are expensive so we use linked list + * + * There are few watches then a lot of observers. This is why you don't want the observer to be + * implemented in the same way as watch. Watch requires return of initialization function which + * are expensive to construct. + */ + + +/** + * @ngdoc provider + * @name $rootScopeProvider + * @description + * + * Provider for the $rootScope service. + */ + +/** + * @ngdoc method + * @name $rootScopeProvider#digestTtl + * @description + * + * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * In complex applications it's possible that the dependencies between `$watch`s will result in + * several digest iterations. However if an application needs more than the default 10 digest + * iterations for its model to stabilize then you should investigate what is causing the model to + * continuously change during the digest. + * + * Increasing the TTL could have performance implications, so you should not change it without + * proper justification. + * + * @param {number} limit The number of digest iterations. + */ + + +/** + * @ngdoc service + * @name $rootScope + * @description + * + * Every application has a single root {@link ng.$rootScope.Scope scope}. + * All other scopes are descendant scopes of the root scope. Scopes provide separation + * between the model and the view, via a mechanism for watching the model for changes. + * They also provide an event emission/broadcast and subscription facility. See the + * {@link guide/scope developer guide on scopes}. + */ +function $RootScopeProvider(){ + var TTL = 10; + var $rootScopeMinErr = minErr('$rootScope'); + var lastDirtyWatch = null; + + this.digestTtl = function(value) { + if (arguments.length) { + TTL = value; + } + return TTL; + }; + + this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', + function( $injector, $exceptionHandler, $parse, $browser) { + + /** + * @ngdoc type + * @name $rootScope.Scope + * + * @description + * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the + * {@link auto.$injector $injector}. Child scopes are created using the + * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when + * compiled HTML template is executed.) + * + * Here is a simple scope snippet to show how you can interact with the scope. + * ```html + * + * ``` + * + * # Inheritance + * A scope can inherit from a parent scope, as in this example: + * ```js + var parent = $rootScope; + var child = parent.$new(); + + parent.salutation = "Hello"; + child.name = "World"; + expect(child.salutation).toEqual('Hello'); + + child.salutation = "Welcome"; + expect(child.salutation).toEqual('Welcome'); + expect(parent.salutation).toEqual('Hello'); + * ``` + * + * + * @param {Object.=} providers Map of service factory which need to be + * provided for the current scope. Defaults to {@link ng}. + * @param {Object.=} instanceCache Provides pre-instantiated services which should + * append/override services provided by `providers`. This is handy + * when unit-testing and having the need to override a default + * service. + * @returns {Object} Newly created scope. + * + */ + function Scope() { + this.$id = nextUid(); + this.$$phase = this.$parent = this.$$watchers = + this.$$nextSibling = this.$$prevSibling = + this.$$childHead = this.$$childTail = null; + this['this'] = this.$root = this; + this.$$destroyed = false; + this.$$asyncQueue = []; + this.$$postDigestQueue = []; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$$isolateBindings = {}; + } + + /** + * @ngdoc property + * @name $rootScope.Scope#$id + * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for + * debugging. + */ + + + Scope.prototype = { + constructor: Scope, + /** + * @ngdoc method + * @name $rootScope.Scope#$new + * @kind function + * + * @description + * Creates a new child {@link ng.$rootScope.Scope scope}. + * + * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and + * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the + * scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. + * + * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is + * desired for the scope and its child scopes to be permanently detached from the parent and + * thus stop participating in model change detection and listener notification by invoking. + * + * @param {boolean} isolate If true, then the scope does not prototypically inherit from the + * parent scope. The scope is isolated, as it can not see parent scope properties. + * When creating widgets, it is useful for the widget to not accidentally read parent + * state. + * + * @returns {Object} The newly created child scope. + * + */ + $new: function(isolate) { + var ChildScope, + child; + + if (isolate) { + child = new Scope(); + child.$root = this.$root; + // ensure that there is just one async queue per $rootScope and its children + child.$$asyncQueue = this.$$asyncQueue; + child.$$postDigestQueue = this.$$postDigestQueue; + } else { + // Only create a child scope class if somebody asks for one, + // but cache it to allow the VM to optimize lookups. + if (!this.$$childScopeClass) { + this.$$childScopeClass = function() { + this.$$watchers = this.$$nextSibling = + this.$$childHead = this.$$childTail = null; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$id = nextUid(); + this.$$childScopeClass = null; + }; + this.$$childScopeClass.prototype = this; + } + child = new this.$$childScopeClass(); + } + child['this'] = child; + child.$parent = this; + child.$$prevSibling = this.$$childTail; + if (this.$$childHead) { + this.$$childTail.$$nextSibling = child; + this.$$childTail = child; + } else { + this.$$childHead = this.$$childTail = child; + } + return child; + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$watch + * @kind function + * + * @description + * Registers a `listener` callback to be executed whenever the `watchExpression` changes. + * + * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest + * $digest()} and should return the value that will be watched. (Since + * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the + * `watchExpression` can execute multiple times per + * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) + * - The `listener` is called only when the value from the current `watchExpression` and the + * previous call to `watchExpression` are not equal (with the exception of the initial run, + * see below). Inequality is determined according to reference inequality, + * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) + * via the `!==` Javascript operator, unless `objectEquality == true` + * (see next point) + * - When `objectEquality == true`, inequality of the `watchExpression` is determined + * according to the {@link angular.equals} function. To save the value of the object for + * later comparison, the {@link angular.copy} function is used. This therefore means that + * watching complex objects will have adverse memory and performance implications. + * - The watch `listener` may change the model, which may trigger other `listener`s to fire. + * This is achieved by rerunning the watchers until no changes are detected. The rerun + * iteration limit is 10 to prevent an infinite loop deadlock. + * + * + * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, + * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` + * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a + * change is detected, be prepared for multiple calls to your listener.) + * + * After a watcher is registered with the scope, the `listener` fn is called asynchronously + * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the + * watcher. In rare cases, this is undesirable because the listener is called when the result + * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you + * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the + * listener was called due to initialization. + * + * The example below contains an illustration of using a function as your $watch listener + * + * + * # Example + * ```js + // let's assume that scope was dependency injected as the $rootScope + var scope = $rootScope; + scope.name = 'misko'; + scope.counter = 0; + + expect(scope.counter).toEqual(0); + scope.$watch('name', function(newValue, oldValue) { + scope.counter = scope.counter + 1; + }); + expect(scope.counter).toEqual(0); + + scope.$digest(); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); + + scope.$digest(); + // but now it will not be called unless the value changes + expect(scope.counter).toEqual(1); + + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); + + + + // Using a listener function + var food; + scope.foodCounter = 0; + expect(scope.foodCounter).toEqual(0); + scope.$watch( + // This is the listener function + function() { return food; }, + // This is the change handler + function(newValue, oldValue) { + if ( newValue !== oldValue ) { + // Only increment the counter if the value changed + scope.foodCounter = scope.foodCounter + 1; + } + } + ); + // No digest has been run so the counter will be zero + expect(scope.foodCounter).toEqual(0); + + // Run the digest but since food has not changed count will still be zero + scope.$digest(); + expect(scope.foodCounter).toEqual(0); + + // Update food and run digest. Now the counter will increment + food = 'cheeseburger'; + scope.$digest(); + expect(scope.foodCounter).toEqual(1); + + * ``` + * + * + * + * @param {(function()|string)} watchExpression Expression that is evaluated on each + * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers + * a call to the `listener`. + * + * - `string`: Evaluated as {@link guide/expression expression} + * - `function(scope)`: called with current `scope` as a parameter. + * @param {(function()|string)=} listener Callback called whenever the return value of + * the `watchExpression` changes. + * + * - `string`: Evaluated as {@link guide/expression expression} + * - `function(newValue, oldValue, scope)`: called with current and previous values as + * parameters. + * + * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of + * comparing for reference equality. + * @returns {function()} Returns a deregistration function for this listener. + */ + $watch: function(watchExp, listener, objectEquality) { + var scope = this, + get = compileToFn(watchExp, 'watch'), + array = scope.$$watchers, + watcher = { + fn: listener, + last: initWatchVal, + get: get, + exp: watchExp, + eq: !!objectEquality + }; + + lastDirtyWatch = null; + + // in the case user pass string, we need to compile it, do we really need this ? + if (!isFunction(listener)) { + var listenFn = compileToFn(listener || noop, 'listener'); + watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; + } + + if (typeof watchExp == 'string' && get.constant) { + var originalFn = watcher.fn; + watcher.fn = function(newVal, oldVal, scope) { + originalFn.call(this, newVal, oldVal, scope); + arrayRemove(array, watcher); + }; + } + + if (!array) { + array = scope.$$watchers = []; + } + // we use unshift since we use a while loop in $digest for speed. + // the while loop reads in reverse order. + array.unshift(watcher); + + return function deregisterWatch() { + arrayRemove(array, watcher); + lastDirtyWatch = null; + }; + }, + + + /** + * @ngdoc method + * @name $rootScope.Scope#$watchCollection + * @kind function + * + * @description + * Shallow watches the properties of an object and fires whenever any of the properties change + * (for arrays, this implies watching the array items; for object maps, this implies watching + * the properties). If a change is detected, the `listener` callback is fired. + * + * - The `obj` collection is observed via standard $watch operation and is examined on every + * call to $digest() to see if any items have been added, removed, or moved. + * - The `listener` is called whenever anything within the `obj` has changed. Examples include + * adding, removing, and moving items belonging to an object or array. + * + * + * # Example + * ```js + $scope.names = ['igor', 'matias', 'misko', 'james']; + $scope.dataCount = 4; + + $scope.$watchCollection('names', function(newNames, oldNames) { + $scope.dataCount = newNames.length; + }); + + expect($scope.dataCount).toEqual(4); + $scope.$digest(); + + //still at 4 ... no changes + expect($scope.dataCount).toEqual(4); + + $scope.names.pop(); + $scope.$digest(); + + //now there's been a change + expect($scope.dataCount).toEqual(3); + * ``` + * + * + * @param {string|function(scope)} obj Evaluated as {@link guide/expression expression}. The + * expression value should evaluate to an object or an array which is observed on each + * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the + * collection will trigger a call to the `listener`. + * + * @param {function(newCollection, oldCollection, scope)} listener a callback function called + * when a change is detected. + * - The `newCollection` object is the newly modified data obtained from the `obj` expression + * - The `oldCollection` object is a copy of the former collection data. + * Due to performance considerations, the`oldCollection` value is computed only if the + * `listener` function declares two or more arguments. + * - The `scope` argument refers to the current scope. + * + * @returns {function()} Returns a de-registration function for this listener. When the + * de-registration function is executed, the internal watch operation is terminated. + */ + $watchCollection: function(obj, listener) { + var self = this; + // the current value, updated on each dirty-check run + var newValue; + // a shallow copy of the newValue from the last dirty-check run, + // updated to match newValue during dirty-check run + var oldValue; + // a shallow copy of the newValue from when the last change happened + var veryOldValue; + // only track veryOldValue if the listener is asking for it + var trackVeryOldValue = (listener.length > 1); + var changeDetected = 0; + var objGetter = $parse(obj); + var internalArray = []; + var internalObject = {}; + var initRun = true; + var oldLength = 0; + + function $watchCollectionWatch() { + newValue = objGetter(self); + var newLength, key; + + if (!isObject(newValue)) { // if primitive + if (oldValue !== newValue) { + oldValue = newValue; + changeDetected++; + } + } else if (isArrayLike(newValue)) { + if (oldValue !== internalArray) { + // we are transitioning from something which was not an array into array. + oldValue = internalArray; + oldLength = oldValue.length = 0; + changeDetected++; + } + + newLength = newValue.length; + + if (oldLength !== newLength) { + // if lengths do not match we need to trigger change notification + changeDetected++; + oldValue.length = oldLength = newLength; + } + // copy the items to oldValue and look for changes. + for (var i = 0; i < newLength; i++) { + var bothNaN = (oldValue[i] !== oldValue[i]) && + (newValue[i] !== newValue[i]); + if (!bothNaN && (oldValue[i] !== newValue[i])) { + changeDetected++; + oldValue[i] = newValue[i]; + } + } + } else { + if (oldValue !== internalObject) { + // we are transitioning from something which was not an object into object. + oldValue = internalObject = {}; + oldLength = 0; + changeDetected++; + } + // copy the items to oldValue and look for changes. + newLength = 0; + for (key in newValue) { + if (newValue.hasOwnProperty(key)) { + newLength++; + if (oldValue.hasOwnProperty(key)) { + if (oldValue[key] !== newValue[key]) { + changeDetected++; + oldValue[key] = newValue[key]; + } + } else { + oldLength++; + oldValue[key] = newValue[key]; + changeDetected++; + } + } + } + if (oldLength > newLength) { + // we used to have more keys, need to find them and destroy them. + changeDetected++; + for(key in oldValue) { + if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { + oldLength--; + delete oldValue[key]; + } + } + } + } + return changeDetected; + } + + function $watchCollectionAction() { + if (initRun) { + initRun = false; + listener(newValue, newValue, self); + } else { + listener(newValue, veryOldValue, self); + } + + // make a copy for the next time a collection is changed + if (trackVeryOldValue) { + if (!isObject(newValue)) { + //primitive + veryOldValue = newValue; + } else if (isArrayLike(newValue)) { + veryOldValue = new Array(newValue.length); + for (var i = 0; i < newValue.length; i++) { + veryOldValue[i] = newValue[i]; + } + } else { // if object + veryOldValue = {}; + for (var key in newValue) { + if (hasOwnProperty.call(newValue, key)) { + veryOldValue[key] = newValue[key]; + } + } + } + } + } + + return this.$watch($watchCollectionWatch, $watchCollectionAction); + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$digest + * @kind function + * + * @description + * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and + * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change + * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} + * until no more listeners are firing. This means that it is possible to get into an infinite + * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of + * iterations exceeds 10. + * + * Usually, you don't call `$digest()` directly in + * {@link ng.directive:ngController controllers} or in + * {@link ng.$compileProvider#directive directives}. + * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within + * a {@link ng.$compileProvider#directive directives}), which will force a `$digest()`. + * + * If you want to be notified whenever `$digest()` is called, + * you can register a `watchExpression` function with + * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. + * + * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. + * + * # Example + * ```js + var scope = ...; + scope.name = 'misko'; + scope.counter = 0; + + expect(scope.counter).toEqual(0); + scope.$watch('name', function(newValue, oldValue) { + scope.counter = scope.counter + 1; + }); + expect(scope.counter).toEqual(0); + + scope.$digest(); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); + + scope.$digest(); + // but now it will not be called unless the value changes + expect(scope.counter).toEqual(1); + + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); + * ``` + * + */ + $digest: function() { + var watch, value, last, + watchers, + asyncQueue = this.$$asyncQueue, + postDigestQueue = this.$$postDigestQueue, + length, + dirty, ttl = TTL, + next, current, target = this, + watchLog = [], + logIdx, logMsg, asyncTask; + + beginPhase('$digest'); + + lastDirtyWatch = null; + + do { // "while dirty" loop + dirty = false; + current = target; + + while(asyncQueue.length) { + try { + asyncTask = asyncQueue.shift(); + asyncTask.scope.$eval(asyncTask.expression); + } catch (e) { + clearPhase(); + $exceptionHandler(e); + } + lastDirtyWatch = null; + } + + traverseScopesLoop: + do { // "traverse the scopes" loop + if ((watchers = current.$$watchers)) { + // process our watches + length = watchers.length; + while (length--) { + try { + watch = watchers[length]; + // Most common watches are on primitives, in which case we can short + // circuit it with === operator, only when === fails do we use .equals + if (watch) { + if ((value = watch.get(current)) !== (last = watch.last) && + !(watch.eq + ? equals(value, last) + : (typeof value == 'number' && typeof last == 'number' + && isNaN(value) && isNaN(last)))) { + dirty = true; + lastDirtyWatch = watch; + watch.last = watch.eq ? copy(value, null) : value; + watch.fn(value, ((last === initWatchVal) ? value : last), current); + if (ttl < 5) { + logIdx = 4 - ttl; + if (!watchLog[logIdx]) watchLog[logIdx] = []; + logMsg = (isFunction(watch.exp)) + ? 'fn: ' + (watch.exp.name || watch.exp.toString()) + : watch.exp; + logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); + watchLog[logIdx].push(logMsg); + } + } else if (watch === lastDirtyWatch) { + // If the most recently dirty watcher is now clean, short circuit since the remaining watchers + // have already been tested. + dirty = false; + break traverseScopesLoop; + } + } + } catch (e) { + clearPhase(); + $exceptionHandler(e); + } + } + } + + // Insanity Warning: scope depth-first traversal + // yes, this code is a bit crazy, but it works and we have tests to prove it! + // this piece should be kept in sync with the traversal in $broadcast + if (!(next = (current.$$childHead || + (current !== target && current.$$nextSibling)))) { + while(current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; + } + } + } while ((current = next)); + + // `break traverseScopesLoop;` takes us to here + + if((dirty || asyncQueue.length) && !(ttl--)) { + clearPhase(); + throw $rootScopeMinErr('infdig', + '{0} $digest() iterations reached. Aborting!\n' + + 'Watchers fired in the last 5 iterations: {1}', + TTL, toJson(watchLog)); + } + + } while (dirty || asyncQueue.length); + + clearPhase(); + + while(postDigestQueue.length) { + try { + postDigestQueue.shift()(); + } catch (e) { + $exceptionHandler(e); + } + } + }, + + + /** + * @ngdoc event + * @name $rootScope.Scope#$destroy + * @eventType broadcast on scope being destroyed + * + * @description + * Broadcasted when a scope and its children are being destroyed. + * + * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to + * clean up DOM bindings before an element is removed from the DOM. + */ + + /** + * @ngdoc method + * @name $rootScope.Scope#$destroy + * @kind function + * + * @description + * Removes the current scope (and all of its children) from the parent scope. Removal implies + * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer + * propagate to the current scope and its children. Removal also implies that the current + * scope is eligible for garbage collection. + * + * The `$destroy()` is usually used by directives such as + * {@link ng.directive:ngRepeat ngRepeat} for managing the + * unrolling of the loop. + * + * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. + * Application code can register a `$destroy` event handler that will give it a chance to + * perform any necessary cleanup. + * + * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to + * clean up DOM bindings before an element is removed from the DOM. + */ + $destroy: function() { + // we can't destroy the root scope or a scope that has been already destroyed + if (this.$$destroyed) return; + var parent = this.$parent; + + this.$broadcast('$destroy'); + this.$$destroyed = true; + if (this === $rootScope) return; + + forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); + + // sever all the references to parent scopes (after this cleanup, the current scope should + // not be retained by any of our references and should be eligible for garbage collection) + if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; + if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; + if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; + if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; + + + // All of the code below is bogus code that works around V8's memory leak via optimized code + // and inline caches. + // + // see: + // - https://code.google.com/p/v8/issues/detail?id=2073#c26 + // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 + // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + + this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = + this.$$childTail = this.$root = null; + + // don't reset these to null in case some async task tries to register a listener/watch/task + this.$$listeners = {}; + this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; + + // prevent NPEs since these methods have references to properties we nulled out + this.$destroy = this.$digest = this.$apply = noop; + this.$on = this.$watch = function() { return noop; }; + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$eval + * @kind function + * + * @description + * Executes the `expression` on the current scope and returns the result. Any exceptions in + * the expression are propagated (uncaught). This is useful when evaluating Angular + * expressions. + * + * # Example + * ```js + var scope = ng.$rootScope.Scope(); + scope.a = 1; + scope.b = 2; + + expect(scope.$eval('a+b')).toEqual(3); + expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); + * ``` + * + * @param {(string|function())=} expression An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + * @param {(object)=} locals Local variables object, useful for overriding values in scope. + * @returns {*} The result of evaluating the expression. + */ + $eval: function(expr, locals) { + return $parse(expr)(this, locals); + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$evalAsync + * @kind function + * + * @description + * Executes the expression on the current scope at a later point in time. + * + * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only + * that: + * + * - it will execute after the function that scheduled the evaluation (preferably before DOM + * rendering). + * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after + * `expression` execution. + * + * Any exceptions from the execution of the expression are forwarded to the + * {@link ng.$exceptionHandler $exceptionHandler} service. + * + * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle + * will be scheduled. However, it is encouraged to always call code that changes the model + * from within an `$apply` call. That includes code evaluated via `$evalAsync`. + * + * @param {(string|function())=} expression An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + */ + $evalAsync: function(expr) { + // if we are outside of an $digest loop and this is the first time we are scheduling async + // task also schedule async auto-flush + if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { + $browser.defer(function() { + if ($rootScope.$$asyncQueue.length) { + $rootScope.$digest(); + } + }); + } + + this.$$asyncQueue.push({scope: this, expression: expr}); + }, + + $$postDigest : function(fn) { + this.$$postDigestQueue.push(fn); + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$apply + * @kind function + * + * @description + * `$apply()` is used to execute an expression in angular from outside of the angular + * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). + * Because we are calling into the angular framework we need to perform proper scope life + * cycle of {@link ng.$exceptionHandler exception handling}, + * {@link ng.$rootScope.Scope#$digest executing watches}. + * + * ## Life cycle + * + * # Pseudo-Code of `$apply()` + * ```js + function $apply(expr) { + try { + return $eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + $root.$digest(); + } + } + * ``` + * + * + * Scope's `$apply()` method transitions through the following stages: + * + * 1. The {@link guide/expression expression} is executed using the + * {@link ng.$rootScope.Scope#$eval $eval()} method. + * 2. Any exceptions from the execution of the expression are forwarded to the + * {@link ng.$exceptionHandler $exceptionHandler} service. + * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the + * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. + * + * + * @param {(string|function())=} exp An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + * + * @returns {*} The result of evaluating the expression. + */ + $apply: function(expr) { + try { + beginPhase('$apply'); + return this.$eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + clearPhase(); + try { + $rootScope.$digest(); + } catch (e) { + $exceptionHandler(e); + throw e; + } + } + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$on + * @kind function + * + * @description + * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for + * discussion of event life cycle. + * + * The event listener function format is: `function(event, args...)`. The `event` object + * passed into the listener has the following attributes: + * + * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or + * `$broadcast`-ed. + * - `currentScope` - `{Scope}`: the current scope which is handling the event. + * - `name` - `{string}`: name of the event. + * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel + * further event propagation (available only for events that were `$emit`-ed). + * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag + * to true. + * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. + * + * @param {string} name Event name to listen on. + * @param {function(event, ...args)} listener Function to call when the event is emitted. + * @returns {function()} Returns a deregistration function for this listener. + */ + $on: function(name, listener) { + var namedListeners = this.$$listeners[name]; + if (!namedListeners) { + this.$$listeners[name] = namedListeners = []; + } + namedListeners.push(listener); + + var current = this; + do { + if (!current.$$listenerCount[name]) { + current.$$listenerCount[name] = 0; + } + current.$$listenerCount[name]++; + } while ((current = current.$parent)); + + var self = this; + return function() { + namedListeners[indexOf(namedListeners, listener)] = null; + decrementListenerCount(self, 1, name); + }; + }, + + + /** + * @ngdoc method + * @name $rootScope.Scope#$emit + * @kind function + * + * @description + * Dispatches an event `name` upwards through the scope hierarchy notifying the + * registered {@link ng.$rootScope.Scope#$on} listeners. + * + * The event life cycle starts at the scope on which `$emit` was called. All + * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get + * notified. Afterwards, the event traverses upwards toward the root scope and calls all + * registered listeners along the way. The event will stop propagating if one of the listeners + * cancels it. + * + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed + * onto the {@link ng.$exceptionHandler $exceptionHandler} service. + * + * @param {string} name Event name to emit. + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. + * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). + */ + $emit: function(name, args) { + var empty = [], + namedListeners, + scope = this, + stopPropagation = false, + event = { + name: name, + targetScope: scope, + stopPropagation: function() {stopPropagation = true;}, + preventDefault: function() { + event.defaultPrevented = true; + }, + defaultPrevented: false + }, + listenerArgs = concat([event], arguments, 1), + i, length; + + do { + namedListeners = scope.$$listeners[name] || empty; + event.currentScope = scope; + for (i=0, length=namedListeners.length; i= 8 ) { + normalizedVal = urlResolve(uri).href; + if (normalizedVal !== '' && !normalizedVal.match(regex)) { + return 'unsafe:'+normalizedVal; + } + } + return uri; + }; + }; +} + +var $sceMinErr = minErr('$sce'); + +var SCE_CONTEXTS = { + HTML: 'html', + CSS: 'css', + URL: 'url', + // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a + // url. (e.g. ng-include, script src, templateUrl) + RESOURCE_URL: 'resourceUrl', + JS: 'js' +}; + +// Helper functions follow. + +// Copied from: +// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962 +// Prereq: s is a string. +function escapeForRegexp(s) { + return s.replace(/([-()\[\]{}+?*.$\^|,:# -1) { + throw $sceMinErr('iwcard', + 'Illegal sequence *** in string matcher. String: {0}', matcher); + } + matcher = escapeForRegexp(matcher). + replace('\\*\\*', '.*'). + replace('\\*', '[^:/.?&;]*'); + return new RegExp('^' + matcher + '$'); + } else if (isRegExp(matcher)) { + // The only other type of matcher allowed is a Regexp. + // Match entire URL / disallow partial matches. + // Flags are reset (i.e. no global, ignoreCase or multiline) + return new RegExp('^' + matcher.source + '$'); + } else { + throw $sceMinErr('imatcher', + 'Matchers may only be "self", string patterns or RegExp objects'); + } +} + + +function adjustMatchers(matchers) { + var adjustedMatchers = []; + if (isDefined(matchers)) { + forEach(matchers, function(matcher) { + adjustedMatchers.push(adjustMatcher(matcher)); + }); + } + return adjustedMatchers; +} + + +/** + * @ngdoc service + * @name $sceDelegate + * @kind function + * + * @description + * + * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict + * Contextual Escaping (SCE)} services to AngularJS. + * + * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of + * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is + * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to + * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things + * work because `$sce` delegates to `$sceDelegate` for these operations. + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. + * + * The default instance of `$sceDelegate` should work out of the box with little pain. While you + * can override it completely to change the behavior of `$sce`, the common case would + * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting + * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as + * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist + * $sceDelegateProvider.resourceUrlWhitelist} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + */ + +/** + * @ngdoc provider + * @name $sceDelegateProvider + * @description + * + * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate + * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure + * that the URLs used for sourcing Angular templates are safe. Refer {@link + * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and + * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * + * For the general details about this service in Angular, read the main page for {@link ng.$sce + * Strict Contextual Escaping (SCE)}. + * + * **Example**: Consider the following case. + * + * - your app is hosted at url `http://myapp.example.com/` + * - but some of your templates are hosted on other domains you control such as + * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc. + * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. + * + * Here is what a secure configuration for this scenario might look like: + * + *
+ *    angular.module('myApp', []).config(function($sceDelegateProvider) {
+ *      $sceDelegateProvider.resourceUrlWhitelist([
+ *        // Allow same origin resource loads.
+ *        'self',
+ *        // Allow loading from our assets domain.  Notice the difference between * and **.
+ *        'http://srv*.assets.example.com/**']);
+ *
+ *      // The blacklist overrides the whitelist so the open redirect here is blocked.
+ *      $sceDelegateProvider.resourceUrlBlacklist([
+ *        'http://myapp.example.com/clickThru**']);
+ *      });
+ * 
+ */ + +function $SceDelegateProvider() { + this.SCE_CONTEXTS = SCE_CONTEXTS; + + // Resource URLs can also be trusted by policy. + var resourceUrlWhitelist = ['self'], + resourceUrlBlacklist = []; + + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlWhitelist + * @kind function + * + * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. + * + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. + * + * Note: **an empty whitelist array will block all URLs**! + * + * @return {Array} the currently set whitelist array. + * + * The **default value** when no whitelist has been explicitly set is `['self']` allowing only + * same origin resource requests. + * + * @description + * Sets/Gets the whitelist of trusted resource URLs. + */ + this.resourceUrlWhitelist = function (value) { + if (arguments.length) { + resourceUrlWhitelist = adjustMatchers(value); + } + return resourceUrlWhitelist; + }; + + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlBlacklist + * @kind function + * + * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. + * + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. + * + * The typical usage for the blacklist is to **block + * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as + * these would otherwise be trusted but actually return content from the redirected domain. + * + * Finally, **the blacklist overrides the whitelist** and has the final say. + * + * @return {Array} the currently set blacklist array. + * + * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there + * is no blacklist.) + * + * @description + * Sets/Gets the blacklist of trusted resource URLs. + */ + + this.resourceUrlBlacklist = function (value) { + if (arguments.length) { + resourceUrlBlacklist = adjustMatchers(value); + } + return resourceUrlBlacklist; + }; + + this.$get = ['$injector', function($injector) { + + var htmlSanitizer = function htmlSanitizer(html) { + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); + }; + + if ($injector.has('$sanitize')) { + htmlSanitizer = $injector.get('$sanitize'); + } + + + function matchUrl(matcher, parsedUrl) { + if (matcher === 'self') { + return urlIsSameOrigin(parsedUrl); + } else { + // definitely a regex. See adjustMatchers() + return !!matcher.exec(parsedUrl.href); + } + } + + function isResourceUrlAllowedByPolicy(url) { + var parsedUrl = urlResolve(url.toString()); + var i, n, allowed = false; + // Ensure that at least one item from the whitelist allows this url. + for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { + if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { + allowed = true; + break; + } + } + if (allowed) { + // Ensure that no item from the blacklist blocked this url. + for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { + if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { + allowed = false; + break; + } + } + } + return allowed; + } + + function generateHolderType(Base) { + var holderType = function TrustedValueHolderType(trustedValue) { + this.$$unwrapTrustedValue = function() { + return trustedValue; + }; + }; + if (Base) { + holderType.prototype = new Base(); + } + holderType.prototype.valueOf = function sceValueOf() { + return this.$$unwrapTrustedValue(); + }; + holderType.prototype.toString = function sceToString() { + return this.$$unwrapTrustedValue().toString(); + }; + return holderType; + } + + var trustedValueHolderBase = generateHolderType(), + byType = {}; + + byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); + + /** + * @ngdoc method + * @name $sceDelegate#trustAs + * + * @description + * Returns an object that is trusted by angular for use in specified strict + * contextual escaping contexts (such as ng-bind-html, ng-include, any src + * attribute interpolation, any dom event binding attribute interpolation + * such as for onclick, etc.) that uses the provided value. + * See {@link ng.$sce $sce} for enabling strict contextual escaping. + * + * @param {string} type The kind of context in which this value is safe for use. e.g. url, + * resourceUrl, html, js and css. + * @param {*} value The value that that should be considered trusted/safe. + * @returns {*} A value that can be used to stand in for the provided `value` in places + * where Angular expects a $sce.trustAs() return value. + */ + function trustAs(type, trustedValue) { + var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + if (!Constructor) { + throw $sceMinErr('icontext', + 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', + type, trustedValue); + } + if (trustedValue === null || trustedValue === undefined || trustedValue === '') { + return trustedValue; + } + // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting + // mutable objects, we ensure here that the value passed in is actually a string. + if (typeof trustedValue !== 'string') { + throw $sceMinErr('itype', + 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', + type); + } + return new Constructor(trustedValue); + } + + /** + * @ngdoc method + * @name $sceDelegate#valueOf + * + * @description + * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. + * + * If the passed parameter is not a value that had been returned by {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. + * + * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} + * call or anything else. + * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns + * `value` unchanged. + */ + function valueOf(maybeTrusted) { + if (maybeTrusted instanceof trustedValueHolderBase) { + return maybeTrusted.$$unwrapTrustedValue(); + } else { + return maybeTrusted; + } + } + + /** + * @ngdoc method + * @name $sceDelegate#getTrusted + * + * @description + * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and + * returns the originally supplied value if the queried context type is a supertype of the + * created type. If this condition isn't satisfied, throws an exception. + * + * @param {string} type The kind of context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} call. + * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. + */ + function getTrusted(type, maybeTrusted) { + if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { + return maybeTrusted; + } + var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + if (constructor && maybeTrusted instanceof constructor) { + return maybeTrusted.$$unwrapTrustedValue(); + } + // If we get here, then we may only take one of two actions. + // 1. sanitize the value for the requested type, or + // 2. throw an exception. + if (type === SCE_CONTEXTS.RESOURCE_URL) { + if (isResourceUrlAllowedByPolicy(maybeTrusted)) { + return maybeTrusted; + } else { + throw $sceMinErr('insecurl', + 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', + maybeTrusted.toString()); + } + } else if (type === SCE_CONTEXTS.HTML) { + return htmlSanitizer(maybeTrusted); + } + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); + } + + return { trustAs: trustAs, + getTrusted: getTrusted, + valueOf: valueOf }; + }]; +} + + +/** + * @ngdoc provider + * @name $sceProvider + * @description + * + * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. + * - enable/disable Strict Contextual Escaping (SCE) in a module + * - override the default implementation with a custom delegate + * + * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. + */ + +/* jshint maxlen: false*/ + +/** + * @ngdoc service + * @name $sce + * @kind function + * + * @description + * + * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. + * + * # Strict Contextual Escaping + * + * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain + * contexts to result in a value that is marked as safe to use for that context. One example of + * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer + * to these contexts as privileged or SCE contexts. + * + * As of version 1.2, Angular ships with SCE enabled by default. + * + * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows + * one to execute arbitrary javascript by the use of the expression() syntax. Refer + * to learn more about them. + * You can ensure your document is in standards mode and not quirks mode by adding `` + * to the top of your HTML document. + * + * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for + * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * + * Here's an example of a binding in a privileged context: + * + *
+ *     
+ *     
+ *
+ * + * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE + * disabled, this application allows the user to render arbitrary HTML into the DIV. + * In a more realistic example, one may be rendering user comments, blog articles, etc. via + * bindings. (HTML is just one example of a context where rendering user controlled input creates + * security vulnerabilities.) + * + * For the case of HTML, you might use a library, either on the client side, or on the server side, + * to sanitize unsafe HTML before binding to the value and rendering it in the document. + * + * How would you ensure that every place that used these types of bindings was bound to a value that + * was sanitized by your library (or returned as safe for rendering by your server?) How can you + * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some + * properties/fields and forgot to update the binding to the sanitized value? + * + * To be secure by default, you want to ensure that any such bindings are disallowed unless you can + * determine that something explicitly says it's safe to use a value for binding in that + * context. You can then audit your code (a simple grep would do) to ensure that this is only done + * for those values that you can easily tell are safe - because they were received from your server, + * sanitized by your library, etc. You can organize your codebase to help with this - perhaps + * allowing only the files in a specific directory to do this. Ensuring that the internal API + * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. + * + * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} + * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to + * obtain values that will be accepted by SCE / privileged contexts. + * + * + * ## How does it work? + * + * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted + * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link + * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the + * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. + * + * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link + * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly + * simplified): + * + *
+ *   var ngBindHtmlDirective = ['$sce', function($sce) {
+ *     return function(scope, element, attr) {
+ *       scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
+ *         element.html(value || '');
+ *       });
+ *     };
+ *   }];
+ * 
+ * + * ## Impact on loading templates + * + * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as + * `templateUrl`'s specified by {@link guide/directive directives}. + * + * By default, Angular only loads templates from the same domain and protocol as the application + * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or + * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist + * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. + * + * *Please note*: + * The browser's + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) + * policy apply in addition to this and may further restrict whether the template is successfully + * loaded. This means that without the right CORS policy, loading templates from a different domain + * won't work on all browsers. Also, loading templates from `file://` URL does not work on some + * browsers. + * + * ## This feels like too much overhead for the developer? + * + * It's important to remember that SCE only applies to interpolation expressions. + * + * If your expressions are constant literals, they're automatically trusted and you don't need to + * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. + * `
`) just works. + * + * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them + * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * + * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load + * templates in `ng-include` from your application's domain without having to even know about SCE. + * It blocks loading templates from other domains or loading templates over http from an https + * served document. You can change these by setting your own custom {@link + * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. + * + * This significantly reduces the overhead. It is far easier to pay the small overhead and have an + * application that's secure and can be audited to verify that with much more ease than bolting + * security onto an application later. + * + * + * ## What trusted context types are supported? + * + * | Context | Notes | + * |---------------------|----------------| + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | + * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | + * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | + * + * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
+ * + * Each element in these arrays must be one of the following: + * + * - **'self'** + * - The special **string**, `'self'`, can be used to match against all URLs of the **same + * domain** as the application document using the **same protocol**. + * - **String** (except the special value `'self'`) + * - The string is matched against the full *normalized / absolute URL* of the resource + * being tested (substring matches are not good enough.) + * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters + * match themselves. + * - `*`: matches zero or more occurrences of any character other than one of the following 6 + * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use + * in a whitelist. + * - `**`: matches zero or more occurrences of *any* character. As such, it's not + * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. + * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might + * not have been the intention.) Its usage at the very end of the path is ok. (e.g. + * http://foo.example.com/templates/**). + * - **RegExp** (*see caveat below*) + * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax + * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to + * accidentally introduce a bug when one updates a complex expression (imho, all regexes should + * have good test coverage.). For instance, the use of `.` in the regex is correct only in a + * small number of cases. A `.` character in the regex used when matching the scheme or a + * subdomain could be matched against a `:` or literal `.` that was likely not intended. It + * is highly recommended to use the string patterns and only fall back to regular expressions + * if they as a last resort. + * - The regular expression must be an instance of RegExp (i.e. not a string.) It is + * matched against the **entire** *normalized / absolute URL* of the resource being tested + * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags + * present on the RegExp (such as multiline, global, ignoreCase) are ignored. + * - If you are generating your JavaScript from some other templating engine (not + * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), + * remember to escape your regular expression (and be aware that you might need more than + * one level of escaping depending on your templating engine and the way you interpolated + * the value.) Do make use of your platform's escaping mechanism as it might be good + * enough before coding your own. e.g. Ruby has + * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) + * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). + * Javascript lacks a similar built in function for escaping. Take a look at Google + * Closure library's [goog.string.regExpEscape(s)]( + * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. + * + * ## Show me an example using SCE. + * + * @example + + +
+

+ User comments
+ By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when + $sanitize is available. If $sanitize isn't available, this results in an error instead of an + exploit. +
+
+ {{userComment.name}}: + +
+
+
+
+
+ + + var mySceApp = angular.module('mySceApp', ['ngSanitize']); + + mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { + var self = this; + $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { + self.userComments = userComments; + }); + self.explicitlyTrustedHtml = $sce.trustAsHtml( + 'Hover over this text.'); + }); + + + +[ + { "name": "Alice", + "htmlComment": + "Is anyone reading this?" + }, + { "name": "Bob", + "htmlComment": "Yes! Am I the only other one?" + } +] + + + + describe('SCE doc demo', function() { + it('should sanitize untrusted values', function() { + expect(element(by.css('.htmlComment')).getInnerHtml()) + .toBe('Is anyone reading this?'); + }); + + it('should NOT sanitize explicitly trusted values', function() { + expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + 'Hover over this text.'); + }); + }); + +
+ * + * + * + * ## Can I disable SCE completely? + * + * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits + * for little coding overhead. It will be much harder to take an SCE disabled application and + * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE + * for cases where you have a lot of existing code that was written before SCE was introduced and + * you're migrating them a module at a time. + * + * That said, here's how you can completely disable SCE: + * + *
+ *   angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
+ *     // Completely disable SCE.  For demonstration purposes only!
+ *     // Do not use in new projects.
+ *     $sceProvider.enabled(false);
+ *   });
+ * 
+ * + */ +/* jshint maxlen: 100 */ + +function $SceProvider() { + var enabled = true; + + /** + * @ngdoc method + * @name $sceProvider#enabled + * @kind function + * + * @param {boolean=} value If provided, then enables/disables SCE. + * @return {boolean} true if SCE is enabled, false otherwise. + * + * @description + * Enables/disables SCE and returns the current value. + */ + this.enabled = function (value) { + if (arguments.length) { + enabled = !!value; + } + return enabled; + }; + + + /* Design notes on the default implementation for SCE. + * + * The API contract for the SCE delegate + * ------------------------------------- + * The SCE delegate object must provide the following 3 methods: + * + * - trustAs(contextEnum, value) + * This method is used to tell the SCE service that the provided value is OK to use in the + * contexts specified by contextEnum. It must return an object that will be accepted by + * getTrusted() for a compatible contextEnum and return this value. + * + * - valueOf(value) + * For values that were not produced by trustAs(), return them as is. For values that were + * produced by trustAs(), return the corresponding input value to trustAs. Basically, if + * trustAs is wrapping the given values into some type, this operation unwraps it when given + * such a value. + * + * - getTrusted(contextEnum, value) + * This function should return the a value that is safe to use in the context specified by + * contextEnum or throw and exception otherwise. + * + * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be + * opaque or wrapped in some holder object. That happens to be an implementation detail. For + * instance, an implementation could maintain a registry of all trusted objects by context. In + * such a case, trustAs() would return the same object that was passed in. getTrusted() would + * return the same object passed in if it was found in the registry under a compatible context or + * throw an exception otherwise. An implementation might only wrap values some of the time based + * on some criteria. getTrusted() might return a value and not throw an exception for special + * constants or objects even if not wrapped. All such implementations fulfill this contract. + * + * + * A note on the inheritance model for SCE contexts + * ------------------------------------------------ + * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This + * is purely an implementation details. + * + * The contract is simply this: + * + * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) + * will also succeed. + * + * Inheritance happens to capture this in a natural way. In some future, we + * may not use inheritance anymore. That is OK because no code outside of + * sce.js and sceSpecs.js would need to be aware of this detail. + */ + + this.$get = ['$parse', '$sniffer', '$sceDelegate', function( + $parse, $sniffer, $sceDelegate) { + // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows + // the "expression(javascript expression)" syntax which is insecure. + if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { + throw $sceMinErr('iequirks', + 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + + 'mode. You can fix this by adding the text to the top of your HTML ' + + 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); + } + + var sce = shallowCopy(SCE_CONTEXTS); + + /** + * @ngdoc method + * @name $sce#isEnabled + * @kind function + * + * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you + * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. + * + * @description + * Returns a boolean indicating if SCE is enabled. + */ + sce.isEnabled = function () { + return enabled; + }; + sce.trustAs = $sceDelegate.trustAs; + sce.getTrusted = $sceDelegate.getTrusted; + sce.valueOf = $sceDelegate.valueOf; + + if (!enabled) { + sce.trustAs = sce.getTrusted = function(type, value) { return value; }; + sce.valueOf = identity; + } + + /** + * @ngdoc method + * @name $sce#parse + * + * @description + * Converts Angular {@link guide/expression expression} into a function. This is like {@link + * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it + * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, + * *result*)} + * + * @param {string} type The kind of SCE context in which this result will be used. + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + sce.parseAs = function sceParseAs(type, expr) { + var parsed = $parse(expr); + if (parsed.literal && parsed.constant) { + return parsed; + } else { + return function sceParseAsTrusted(self, locals) { + return sce.getTrusted(type, parsed(self, locals)); + }; + } + }; + + /** + * @ngdoc method + * @name $sce#trustAs + * + * @description + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, + * returns an object that is trusted by angular for use in specified strict contextual + * escaping contexts (such as ng-bind-html, ng-include, any src attribute + * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) + * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual + * escaping. + * + * @param {string} type The kind of context in which this value is safe for use. e.g. url, + * resource_url, html, js and css. + * @param {*} value The value that that should be considered trusted/safe. + * @returns {*} A value that can be used to stand in for the provided `value` in places + * where Angular expects a $sce.trustAs() return value. + */ + + /** + * @ngdoc method + * @name $sce#trustAsHtml + * + * @description + * Shorthand method. `$sce.trustAsHtml(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml + * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name $sce#trustAsUrl + * + * @description + * Shorthand method. `$sce.trustAsUrl(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl + * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name $sce#trustAsResourceUrl + * + * @description + * Shorthand method. `$sce.trustAsResourceUrl(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the return + * value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name $sce#trustAsJs + * + * @description + * Shorthand method. `$sce.trustAsJs(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs + * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name $sce#getTrusted + * + * @description + * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, + * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the + * originally supplied value if the queried context type is a supertype of the created type. + * If this condition isn't satisfied, throws an exception. + * + * @param {string} type The kind of context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} + * call. + * @returns {*} The value the was originally provided to + * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. + * Otherwise, throws an exception. + */ + + /** + * @ngdoc method + * @name $sce#getTrustedHtml + * + * @description + * Shorthand method. `$sce.getTrustedHtml(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedCss + * + * @description + * Shorthand method. `$sce.getTrustedCss(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedUrl + * + * @description + * Shorthand method. `$sce.getTrustedUrl(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedResourceUrl + * + * @description + * Shorthand method. `$sce.getTrustedResourceUrl(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} + * + * @param {*} value The value to pass to `$sceDelegate.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedJs + * + * @description + * Shorthand method. `$sce.getTrustedJs(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` + */ + + /** + * @ngdoc method + * @name $sce#parseAsHtml + * + * @description + * Shorthand method. `$sce.parseAsHtml(expression string)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsCss + * + * @description + * Shorthand method. `$sce.parseAsCss(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsUrl + * + * @description + * Shorthand method. `$sce.parseAsUrl(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsResourceUrl + * + * @description + * Shorthand method. `$sce.parseAsResourceUrl(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsJs + * + * @description + * Shorthand method. `$sce.parseAsJs(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + // Shorthand delegations. + var parse = sce.parseAs, + getTrusted = sce.getTrusted, + trustAs = sce.trustAs; + + forEach(SCE_CONTEXTS, function (enumValue, name) { + var lName = lowercase(name); + sce[camelCase("parse_as_" + lName)] = function (expr) { + return parse(enumValue, expr); + }; + sce[camelCase("get_trusted_" + lName)] = function (value) { + return getTrusted(enumValue, value); + }; + sce[camelCase("trust_as_" + lName)] = function (value) { + return trustAs(enumValue, value); + }; + }); + + return sce; + }]; +} + +/** + * !!! This is an undocumented "private" service !!! + * + * @name $sniffer + * @requires $window + * @requires $document + * + * @property {boolean} history Does the browser support html5 history api ? + * @property {boolean} hashchange Does the browser support hashchange event ? + * @property {boolean} transitions Does the browser support CSS transition events ? + * @property {boolean} animations Does the browser support CSS animation events ? + * + * @description + * This is very simple implementation of testing browser's features. + */ +function $SnifferProvider() { + this.$get = ['$window', '$document', function($window, $document) { + var eventSupport = {}, + android = + int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), + boxee = /Boxee/i.test(($window.navigator || {}).userAgent), + document = $document[0] || {}, + documentMode = document.documentMode, + vendorPrefix, + vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, + bodyStyle = document.body && document.body.style, + transitions = false, + animations = false, + match; + + if (bodyStyle) { + for(var prop in bodyStyle) { + if(match = vendorRegex.exec(prop)) { + vendorPrefix = match[0]; + vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); + break; + } + } + + if(!vendorPrefix) { + vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; + } + + transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); + animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); + + if (android && (!transitions||!animations)) { + transitions = isString(document.body.style.webkitTransition); + animations = isString(document.body.style.webkitAnimation); + } + } + + + return { + // Android has history.pushState, but it does not update location correctly + // so let's not use the history API at all. + // http://code.google.com/p/android/issues/detail?id=17471 + // https://github.com/angular/angular.js/issues/904 + + // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has + // so let's not use the history API also + // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined + // jshint -W018 + history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), + // jshint +W018 + hashchange: 'onhashchange' in $window && + // IE8 compatible mode lies + (!documentMode || documentMode > 7), + hasEvent: function(event) { + // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have + // it. In particular the event is not fired when backspace or delete key are pressed or + // when cut operation is performed. + if (event == 'input' && msie == 9) return false; + + if (isUndefined(eventSupport[event])) { + var divElm = document.createElement('div'); + eventSupport[event] = 'on' + event in divElm; + } + + return eventSupport[event]; + }, + csp: csp(), + vendorPrefix: vendorPrefix, + transitions : transitions, + animations : animations, + android: android, + msie : msie, + msieDocumentMode: documentMode + }; + }]; +} + +function $TimeoutProvider() { + this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler', + function($rootScope, $browser, $q, $exceptionHandler) { + var deferreds = {}; + + + /** + * @ngdoc service + * @name $timeout + * + * @description + * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch + * block and delegates any exceptions to + * {@link ng.$exceptionHandler $exceptionHandler} service. + * + * The return value of registering a timeout function is a promise, which will be resolved when + * the timeout is reached and the timeout function is executed. + * + * To cancel a timeout request, call `$timeout.cancel(promise)`. + * + * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to + * synchronously flush the queue of deferred functions. + * + * @param {function()} fn A function, whose execution should be delayed. + * @param {number=} [delay=0] Delay in milliseconds. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this + * promise will be resolved with is the return value of the `fn` function. + * + */ + function timeout(fn, delay, invokeApply) { + var deferred = $q.defer(), + promise = deferred.promise, + skipApply = (isDefined(invokeApply) && !invokeApply), + timeoutId; + + timeoutId = $browser.defer(function() { + try { + deferred.resolve(fn()); + } catch(e) { + deferred.reject(e); + $exceptionHandler(e); + } + finally { + delete deferreds[promise.$$timeoutId]; + } + + if (!skipApply) $rootScope.$apply(); + }, delay); + + promise.$$timeoutId = timeoutId; + deferreds[timeoutId] = deferred; + + return promise; + } + + + /** + * @ngdoc method + * @name $timeout#cancel + * + * @description + * Cancels a task associated with the `promise`. As a result of this, the promise will be + * resolved with a rejection. + * + * @param {Promise=} promise Promise returned by the `$timeout` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. + */ + timeout.cancel = function(promise) { + if (promise && promise.$$timeoutId in deferreds) { + deferreds[promise.$$timeoutId].reject('canceled'); + delete deferreds[promise.$$timeoutId]; + return $browser.defer.cancel(promise.$$timeoutId); + } + return false; + }; + + return timeout; + }]; +} + +// NOTE: The usage of window and document instead of $window and $document here is +// deliberate. This service depends on the specific behavior of anchor nodes created by the +// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and +// cause us to break tests. In addition, when the browser resolves a URL for XHR, it +// doesn't know about mocked locations and resolves URLs to the real document - which is +// exactly the behavior needed here. There is little value is mocking these out for this +// service. +var urlParsingNode = document.createElement("a"); +var originUrl = urlResolve(window.location.href, true); + + +/** + * + * Implementation Notes for non-IE browsers + * ---------------------------------------- + * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, + * results both in the normalizing and parsing of the URL. Normalizing means that a relative + * URL will be resolved into an absolute URL in the context of the application document. + * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related + * properties are all populated to reflect the normalized URL. This approach has wide + * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * + * Implementation Notes for IE + * --------------------------- + * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other + * browsers. However, the parsed components will not be set if the URL assigned did not specify + * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We + * work around that by performing the parsing in a 2nd step by taking a previously normalized + * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the + * properties such as protocol, hostname, port, etc. + * + * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one + * uses the inner HTML approach to assign the URL as part of an HTML snippet - + * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. + * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. + * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that + * method and IE < 8 is unsupported. + * + * References: + * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * http://url.spec.whatwg.org/#urlutils + * https://github.com/angular/angular.js/pull/2902 + * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ + * + * @kind function + * @param {string} url The URL to be parsed. + * @description Normalizes and parses a URL. + * @returns {object} Returns the normalized URL as a dictionary. + * + * | member name | Description | + * |---------------|----------------| + * | href | A normalized version of the provided URL if it was not an absolute URL | + * | protocol | The protocol including the trailing colon | + * | host | The host and port (if the port is non-default) of the normalizedUrl | + * | search | The search params, minus the question mark | + * | hash | The hash string, minus the hash symbol + * | hostname | The hostname + * | port | The port, without ":" + * | pathname | The pathname, beginning with "/" + * + */ +function urlResolve(url, base) { + var href = url; + + if (msie) { + // Normalize before parse. Refer Implementation Notes on why this is + // done in two steps on IE. + urlParsingNode.setAttribute("href", href); + href = urlParsingNode.href; + } + + urlParsingNode.setAttribute('href', href); + + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: (urlParsingNode.pathname.charAt(0) === '/') + ? urlParsingNode.pathname + : '/' + urlParsingNode.pathname + }; +} + +/** + * Parse a request URL and determine whether this is a same-origin request as the application document. + * + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. + * @returns {boolean} Whether the request is for the same origin as the application document. + */ +function urlIsSameOrigin(requestUrl) { + var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; + return (parsed.protocol === originUrl.protocol && + parsed.host === originUrl.host); +} + +/** + * @ngdoc service + * @name $window + * + * @description + * A reference to the browser's `window` object. While `window` + * is globally available in JavaScript, it causes testability problems, because + * it is a global variable. In angular we always refer to it through the + * `$window` service, so it may be overridden, removed or mocked for testing. + * + * Expressions, like the one defined for the `ngClick` directive in the example + * below, are evaluated with respect to the current scope. Therefore, there is + * no risk of inadvertently coding in a dependency on a global value in such an + * expression. + * + * @example + + + +
+ + +
+
+ + it('should display the greeting in the input box', function() { + element(by.model('greeting')).sendKeys('Hello, E2E Tests'); + // If we click the button it will block the test runner + // element(':button').click(); + }); + +
+ */ +function $WindowProvider(){ + this.$get = valueFn(window); +} + +/** + * @ngdoc provider + * @name $filterProvider + * @description + * + * Filters are just functions which transform input to an output. However filters need to be + * Dependency Injected. To achieve this a filter definition consists of a factory function which is + * annotated with dependencies and is responsible for creating a filter function. + * + * ```js + * // Filter registration + * function MyModule($provide, $filterProvider) { + * // create a service to demonstrate injection (not always needed) + * $provide.value('greet', function(name){ + * return 'Hello ' + name + '!'; + * }); + * + * // register a filter factory which uses the + * // greet service to demonstrate DI. + * $filterProvider.register('greet', function(greet){ + * // return the filter function which uses the greet service + * // to generate salutation + * return function(text) { + * // filters need to be forgiving so check input validity + * return text && greet(text) || text; + * }; + * }); + * } + * ``` + * + * The filter function is registered with the `$injector` under the filter name suffix with + * `Filter`. + * + * ```js + * it('should be the same instance', inject( + * function($filterProvider) { + * $filterProvider.register('reverse', function(){ + * return ...; + * }); + * }, + * function($filter, reverseFilter) { + * expect($filter('reverse')).toBe(reverseFilter); + * }); + * ``` + * + * + * For more information about how angular filters work, and how to create your own filters, see + * {@link guide/filter Filters} in the Angular Developer Guide. + */ +/** + * @ngdoc method + * @name $filterProvider#register + * @description + * Register filter factory function. + * + * @param {String} name Name of the filter. + * @param {Function} fn The filter factory function which is injectable. + */ + + +/** + * @ngdoc service + * @name $filter + * @kind function + * @description + * Filters are used for formatting data displayed to the user. + * + * The general syntax in templates is as follows: + * + * {{ expression [| filter_name[:parameter_value] ... ] }} + * + * @param {String} name Name of the filter function to retrieve + * @return {Function} the filter function + * @example + + +
+

{{ originalText }}

+

{{ filteredText }}

+
+
+ + + angular.module('filterExample', []) + .controller('MainCtrl', function($scope, $filter) { + $scope.originalText = 'hello'; + $scope.filteredText = $filter('uppercase')($scope.originalText); + }); + +
+ */ +$FilterProvider.$inject = ['$provide']; +function $FilterProvider($provide) { + var suffix = 'Filter'; + + /** + * @ngdoc method + * @name $controllerProvider#register + * @param {string|Object} name Name of the filter function, or an object map of filters where + * the keys are the filter names and the values are the filter factories. + * @returns {Object} Registered filter instance, or if a map of filters was provided then a map + * of the registered filter instances. + */ + function register(name, factory) { + if(isObject(name)) { + var filters = {}; + forEach(name, function(filter, key) { + filters[key] = register(key, filter); + }); + return filters; + } else { + return $provide.factory(name + suffix, factory); + } + } + this.register = register; + + this.$get = ['$injector', function($injector) { + return function(name) { + return $injector.get(name + suffix); + }; + }]; + + //////////////////////////////////////// + + /* global + currencyFilter: false, + dateFilter: false, + filterFilter: false, + jsonFilter: false, + limitToFilter: false, + lowercaseFilter: false, + numberFilter: false, + orderByFilter: false, + uppercaseFilter: false, + */ + + register('currency', currencyFilter); + register('date', dateFilter); + register('filter', filterFilter); + register('json', jsonFilter); + register('limitTo', limitToFilter); + register('lowercase', lowercaseFilter); + register('number', numberFilter); + register('orderBy', orderByFilter); + register('uppercase', uppercaseFilter); +} + +/** + * @ngdoc filter + * @name filter + * @kind function + * + * @description + * Selects a subset of items from `array` and returns it as a new array. + * + * @param {Array} array The source array. + * @param {string|Object|function()} expression The predicate to be used for selecting items from + * `array`. + * + * Can be one of: + * + * - `string`: The string is evaluated as an expression and the resulting value is used for substring match against + * the contents of the `array`. All strings or objects with string properties in `array` that contain this string + * will be returned. The predicate can be negated by prefixing the string with `!`. + * + * - `Object`: A pattern object can be used to filter specific properties on objects contained + * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items + * which have property `name` containing "M" and property `phone` containing "1". A special + * property name `$` can be used (as in `{$:"text"}`) to accept a match against any + * property of the object. That's equivalent to the simple substring match with a `string` + * as described above. + * + * - `function(value)`: A predicate function can be used to write arbitrary filters. The function is + * called for each element of `array`. The final result is an array of those elements that + * the predicate returned true for. + * + * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in + * determining if the expected value (from the filter expression) and actual value (from + * the object in the array) should be considered a match. + * + * Can be one of: + * + * - `function(actual, expected)`: + * The function will be given the object value and the predicate value to compare and + * should return true if the item should be included in filtered result. + * + * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. + * this is essentially strict comparison of expected and actual. + * + * - `false|undefined`: A short hand for a function which will look for a substring match in case + * insensitive way. + * + * @example + + +
+ + Search: + + + + + + +
NamePhone
{{friend.name}}{{friend.phone}}
+
+ Any:
+ Name only
+ Phone only
+ Equality
+ + + + + + +
NamePhone
{{friendObj.name}}{{friendObj.phone}}
+
+ + var expectFriendNames = function(expectedNames, key) { + element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { + arr.forEach(function(wd, i) { + expect(wd.getText()).toMatch(expectedNames[i]); + }); + }); + }; + + it('should search across all fields when filtering with a string', function() { + var searchText = element(by.model('searchText')); + searchText.clear(); + searchText.sendKeys('m'); + expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); + + searchText.clear(); + searchText.sendKeys('76'); + expectFriendNames(['John', 'Julie'], 'friend'); + }); + + it('should search in specific fields when filtering with a predicate object', function() { + var searchAny = element(by.model('search.$')); + searchAny.clear(); + searchAny.sendKeys('i'); + expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); + }); + it('should use a equal comparison when comparator is true', function() { + var searchName = element(by.model('search.name')); + var strict = element(by.model('strict')); + searchName.clear(); + searchName.sendKeys('Julie'); + strict.click(); + expectFriendNames(['Julie'], 'friendObj'); + }); + +
+ */ +function filterFilter() { + return function(array, expression, comparator) { + if (!isArray(array)) return array; + + var comparatorType = typeof(comparator), + predicates = []; + + predicates.check = function(value) { + for (var j = 0; j < predicates.length; j++) { + if(!predicates[j](value)) { + return false; + } + } + return true; + }; + + if (comparatorType !== 'function') { + if (comparatorType === 'boolean' && comparator) { + comparator = function(obj, text) { + return angular.equals(obj, text); + }; + } else { + comparator = function(obj, text) { + if (obj && text && typeof obj === 'object' && typeof text === 'object') { + for (var objKey in obj) { + if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) && + comparator(obj[objKey], text[objKey])) { + return true; + } + } + return false; + } + text = (''+text).toLowerCase(); + return (''+obj).toLowerCase().indexOf(text) > -1; + }; + } + } + + var search = function(obj, text){ + if (typeof text == 'string' && text.charAt(0) === '!') { + return !search(obj, text.substr(1)); + } + switch (typeof obj) { + case "boolean": + case "number": + case "string": + return comparator(obj, text); + case "object": + switch (typeof text) { + case "object": + return comparator(obj, text); + default: + for ( var objKey in obj) { + if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { + return true; + } + } + break; + } + return false; + case "array": + for ( var i = 0; i < obj.length; i++) { + if (search(obj[i], text)) { + return true; + } + } + return false; + default: + return false; + } + }; + switch (typeof expression) { + case "boolean": + case "number": + case "string": + // Set up expression object and fall through + expression = {$:expression}; + // jshint -W086 + case "object": + // jshint +W086 + for (var key in expression) { + (function(path) { + if (typeof expression[path] == 'undefined') return; + predicates.push(function(value) { + return search(path == '$' ? value : (value && value[path]), expression[path]); + }); + })(key); + } + break; + case 'function': + predicates.push(expression); + break; + default: + return array; + } + var filtered = []; + for ( var j = 0; j < array.length; j++) { + var value = array[j]; + if (predicates.check(value)) { + filtered.push(value); + } + } + return filtered; + }; +} + +/** + * @ngdoc filter + * @name currency + * @kind function + * + * @description + * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default + * symbol for current locale is used. + * + * @param {number} amount Input to filter. + * @param {string=} symbol Currency symbol or identifier to be displayed. + * @returns {string} Formatted number. + * + * + * @example + + + +
+
+ default currency symbol ($): {{amount | currency}}
+ custom currency identifier (USD$): {{amount | currency:"USD$"}} +
+
+ + it('should init with 1234.56', function() { + expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); + expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56'); + }); + it('should update', function() { + if (browser.params.browser == 'safari') { + // Safari does not understand the minus key. See + // https://github.com/angular/protractor/issues/481 + return; + } + element(by.model('amount')).clear(); + element(by.model('amount')).sendKeys('-1234'); + expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); + expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('(USD$1,234.00)'); + }); + +
+ */ +currencyFilter.$inject = ['$locale']; +function currencyFilter($locale) { + var formats = $locale.NUMBER_FORMATS; + return function(amount, currencySymbol){ + if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; + return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). + replace(/\u00A4/g, currencySymbol); + }; +} + +/** + * @ngdoc filter + * @name number + * @kind function + * + * @description + * Formats a number as text. + * + * If the input is not a number an empty string is returned. + * + * @param {number|string} number Number to format. + * @param {(number|string)=} fractionSize Number of decimal places to round the number to. + * If this is not provided then the fraction size is computed from the current locale's number + * formatting pattern. In the case of the default locale, it will be 3. + * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. + * + * @example + + + +
+ Enter number:
+ Default formatting: {{val | number}}
+ No fractions: {{val | number:0}}
+ Negative number: {{-val | number:4}} +
+
+ + it('should format numbers', function() { + expect(element(by.id('number-default')).getText()).toBe('1,234.568'); + expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); + }); + + it('should update', function() { + element(by.model('val')).clear(); + element(by.model('val')).sendKeys('3374.333'); + expect(element(by.id('number-default')).getText()).toBe('3,374.333'); + expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); + }); + +
+ */ + + +numberFilter.$inject = ['$locale']; +function numberFilter($locale) { + var formats = $locale.NUMBER_FORMATS; + return function(number, fractionSize) { + return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + fractionSize); + }; +} + +var DECIMAL_SEP = '.'; +function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { + if (number == null || !isFinite(number) || isObject(number)) return ''; + + var isNegative = number < 0; + number = Math.abs(number); + var numStr = number + '', + formatedText = '', + parts = []; + + var hasExponent = false; + if (numStr.indexOf('e') !== -1) { + var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); + if (match && match[2] == '-' && match[3] > fractionSize + 1) { + numStr = '0'; + } else { + formatedText = numStr; + hasExponent = true; + } + } + + if (!hasExponent) { + var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; + + // determine fractionSize if it is not specified + if (isUndefined(fractionSize)) { + fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); + } + + var pow = Math.pow(10, fractionSize + 1); + number = Math.floor(number * pow + 5) / pow; + var fraction = ('' + number).split(DECIMAL_SEP); + var whole = fraction[0]; + fraction = fraction[1] || ''; + + var i, pos = 0, + lgroup = pattern.lgSize, + group = pattern.gSize; + + if (whole.length >= (lgroup + group)) { + pos = whole.length - lgroup; + for (i = 0; i < pos; i++) { + if ((pos - i)%group === 0 && i !== 0) { + formatedText += groupSep; + } + formatedText += whole.charAt(i); + } + } + + for (i = pos; i < whole.length; i++) { + if ((whole.length - i)%lgroup === 0 && i !== 0) { + formatedText += groupSep; + } + formatedText += whole.charAt(i); + } + + // format fraction part. + while(fraction.length < fractionSize) { + fraction += '0'; + } + + if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); + } else { + + if (fractionSize > 0 && number > -1 && number < 1) { + formatedText = number.toFixed(fractionSize); + } + } + + parts.push(isNegative ? pattern.negPre : pattern.posPre); + parts.push(formatedText); + parts.push(isNegative ? pattern.negSuf : pattern.posSuf); + return parts.join(''); +} + +function padNumber(num, digits, trim) { + var neg = ''; + if (num < 0) { + neg = '-'; + num = -num; + } + num = '' + num; + while(num.length < digits) num = '0' + num; + if (trim) + num = num.substr(num.length - digits); + return neg + num; +} + + +function dateGetter(name, size, offset, trim) { + offset = offset || 0; + return function(date) { + var value = date['get' + name](); + if (offset > 0 || value > -offset) + value += offset; + if (value === 0 && offset == -12 ) value = 12; + return padNumber(value, size, trim); + }; +} + +function dateStrGetter(name, shortForm) { + return function(date, formats) { + var value = date['get' + name](); + var get = uppercase(shortForm ? ('SHORT' + name) : name); + + return formats[get][value]; + }; +} + +function timeZoneGetter(date) { + var zone = -1 * date.getTimezoneOffset(); + var paddedZone = (zone >= 0) ? "+" : ""; + + paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + + padNumber(Math.abs(zone % 60), 2); + + return paddedZone; +} + +function ampmGetter(date, formats) { + return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; +} + +var DATE_FORMATS = { + yyyy: dateGetter('FullYear', 4), + yy: dateGetter('FullYear', 2, 0, true), + y: dateGetter('FullYear', 1), + MMMM: dateStrGetter('Month'), + MMM: dateStrGetter('Month', true), + MM: dateGetter('Month', 2, 1), + M: dateGetter('Month', 1, 1), + dd: dateGetter('Date', 2), + d: dateGetter('Date', 1), + HH: dateGetter('Hours', 2), + H: dateGetter('Hours', 1), + hh: dateGetter('Hours', 2, -12), + h: dateGetter('Hours', 1, -12), + mm: dateGetter('Minutes', 2), + m: dateGetter('Minutes', 1), + ss: dateGetter('Seconds', 2), + s: dateGetter('Seconds', 1), + // while ISO 8601 requires fractions to be prefixed with `.` or `,` + // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions + sss: dateGetter('Milliseconds', 3), + EEEE: dateStrGetter('Day'), + EEE: dateStrGetter('Day', true), + a: ampmGetter, + Z: timeZoneGetter +}; + +var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, + NUMBER_STRING = /^\-?\d+$/; + +/** + * @ngdoc filter + * @name date + * @kind function + * + * @description + * Formats `date` to a string based on the requested `format`. + * + * `format` string can be composed of the following elements: + * + * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) + * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) + * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) + * * `'MMMM'`: Month in year (January-December) + * * `'MMM'`: Month in year (Jan-Dec) + * * `'MM'`: Month in year, padded (01-12) + * * `'M'`: Month in year (1-12) + * * `'dd'`: Day in month, padded (01-31) + * * `'d'`: Day in month (1-31) + * * `'EEEE'`: Day in Week,(Sunday-Saturday) + * * `'EEE'`: Day in Week, (Sun-Sat) + * * `'HH'`: Hour in day, padded (00-23) + * * `'H'`: Hour in day (0-23) + * * `'hh'`: Hour in am/pm, padded (01-12) + * * `'h'`: Hour in am/pm, (1-12) + * * `'mm'`: Minute in hour, padded (00-59) + * * `'m'`: Minute in hour (0-59) + * * `'ss'`: Second in minute, padded (00-59) + * * `'s'`: Second in minute (0-59) + * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) + * * `'a'`: am/pm marker + * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) + * + * `format` string can also be one of the following predefined + * {@link guide/i18n localizable formats}: + * + * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale + * (e.g. Sep 3, 2010 12:05:08 pm) + * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) + * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale + * (e.g. Friday, September 3, 2010) + * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) + * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) + * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) + * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) + * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) + * + * `format` string can contain literal values. These need to be quoted with single quotes (e.g. + * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence + * (e.g. `"h 'o''clock'"`). + * + * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or + * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its + * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is + * specified in the string input, the time is considered to be in the local timezone. + * @param {string=} format Formatting rules (see Description). If not specified, + * `mediumDate` is used. + * @returns {string} Formatted string or the input if input is not recognized as date/millis. + * + * @example + + + {{1288323623006 | date:'medium'}}: + {{1288323623006 | date:'medium'}}
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: + {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
+ {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: + {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
+
+ + it('should format date', function() { + expect(element(by.binding("1288323623006 | date:'medium'")).getText()). + toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); + expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). + toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); + expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). + toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + }); + +
+ */ +dateFilter.$inject = ['$locale']; +function dateFilter($locale) { + + + var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; + // 1 2 3 4 5 6 7 8 9 10 11 + function jsonStringToDate(string) { + var match; + if (match = string.match(R_ISO8601_STR)) { + var date = new Date(0), + tzHour = 0, + tzMin = 0, + dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, + timeSetter = match[8] ? date.setUTCHours : date.setHours; + + if (match[9]) { + tzHour = int(match[9] + match[10]); + tzMin = int(match[9] + match[11]); + } + dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); + var h = int(match[4]||0) - tzHour; + var m = int(match[5]||0) - tzMin; + var s = int(match[6]||0); + var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); + timeSetter.call(date, h, m, s, ms); + return date; + } + return string; + } + + + return function(date, format) { + var text = '', + parts = [], + fn, match; + + format = format || 'mediumDate'; + format = $locale.DATETIME_FORMATS[format] || format; + if (isString(date)) { + if (NUMBER_STRING.test(date)) { + date = int(date); + } else { + date = jsonStringToDate(date); + } + } + + if (isNumber(date)) { + date = new Date(date); + } + + if (!isDate(date)) { + return date; + } + + while(format) { + match = DATE_FORMATS_SPLIT.exec(format); + if (match) { + parts = concat(parts, match, 1); + format = parts.pop(); + } else { + parts.push(format); + format = null; + } + } + + forEach(parts, function(value){ + fn = DATE_FORMATS[value]; + text += fn ? fn(date, $locale.DATETIME_FORMATS) + : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + }); + + return text; + }; +} + + +/** + * @ngdoc filter + * @name json + * @kind function + * + * @description + * Allows you to convert a JavaScript object into JSON string. + * + * This filter is mostly useful for debugging. When using the double curly {{value}} notation + * the binding is automatically converted to JSON. + * + * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. + * @returns {string} JSON string. + * + * + * @example + + +
{{ {'name':'value'} | json }}
+
+ + it('should jsonify filtered objects', function() { + expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); + }); + +
+ * + */ +function jsonFilter() { + return function(object) { + return toJson(object, true); + }; +} + + +/** + * @ngdoc filter + * @name lowercase + * @kind function + * @description + * Converts string to lowercase. + * @see angular.lowercase + */ +var lowercaseFilter = valueFn(lowercase); + + +/** + * @ngdoc filter + * @name uppercase + * @kind function + * @description + * Converts string to uppercase. + * @see angular.uppercase + */ +var uppercaseFilter = valueFn(uppercase); + +/** + * @ngdoc filter + * @name limitTo + * @kind function + * + * @description + * Creates a new array or string containing only a specified number of elements. The elements + * are taken from either the beginning or the end of the source array or string, as specified by + * the value and sign (positive or negative) of `limit`. + * + * @param {Array|string} input Source array or string to be limited. + * @param {string|number} limit The length of the returned array or string. If the `limit` number + * is positive, `limit` number of items from the beginning of the source array/string are copied. + * If the number is negative, `limit` number of items from the end of the source array/string + * are copied. The `limit` will be trimmed if it exceeds `array.length` + * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array + * had less than `limit` elements. + * + * @example + + + +
+ Limit {{numbers}} to: +

Output numbers: {{ numbers | limitTo:numLimit }}

+ Limit {{letters}} to: +

Output letters: {{ letters | limitTo:letterLimit }}

+
+
+ + var numLimitInput = element(by.model('numLimit')); + var letterLimitInput = element(by.model('letterLimit')); + var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); + var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + + it('should limit the number array to first three items', function() { + expect(numLimitInput.getAttribute('value')).toBe('3'); + expect(letterLimitInput.getAttribute('value')).toBe('3'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); + expect(limitedLetters.getText()).toEqual('Output letters: abc'); + }); + + it('should update the output when -3 is entered', function() { + numLimitInput.clear(); + numLimitInput.sendKeys('-3'); + letterLimitInput.clear(); + letterLimitInput.sendKeys('-3'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); + expect(limitedLetters.getText()).toEqual('Output letters: ghi'); + }); + + it('should not exceed the maximum size of input array', function() { + numLimitInput.clear(); + numLimitInput.sendKeys('100'); + letterLimitInput.clear(); + letterLimitInput.sendKeys('100'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); + expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); + }); + +
+ */ +function limitToFilter(){ + return function(input, limit) { + if (!isArray(input) && !isString(input)) return input; + + if (Math.abs(Number(limit)) === Infinity) { + limit = Number(limit); + } else { + limit = int(limit); + } + + if (isString(input)) { + //NaN check on limit + if (limit) { + return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length); + } else { + return ""; + } + } + + var out = [], + i, n; + + // if abs(limit) exceeds maximum length, trim it + if (limit > input.length) + limit = input.length; + else if (limit < -input.length) + limit = -input.length; + + if (limit > 0) { + i = 0; + n = limit; + } else { + i = input.length + limit; + n = input.length; + } + + for (; i} expression A predicate to be + * used by the comparator to determine the order of elements. + * + * Can be one of: + * + * - `function`: Getter function. The result of this function will be sorted using the + * `<`, `=`, `>` operator. + * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' + * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control + * ascending or descending sort order (for example, +name or -name). + * - `Array`: An array of function or string predicates. The first predicate in the array + * is used for sorting, but when two items are equivalent, the next predicate is used. + * + * @param {boolean=} reverse Reverse the order of the array. + * @returns {Array} Sorted copy of the source array. + * + * @example + + + +
+
Sorting predicate = {{predicate}}; reverse = {{reverse}}
+
+ [ unsorted ] + + + + + + + + + + + +
Name + (^)Phone NumberAge
{{friend.name}}{{friend.phone}}{{friend.age}}
+
+
+
+ * + * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the + * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the + * desired parameters. + * + * Example: + * + * @example + + +
+ + + + + + + + + + + +
Name + (^)Phone NumberAge
{{friend.name}}{{friend.phone}}{{friend.age}}
+
+
+ + + function Ctrl($scope, $filter) { + var orderBy = $filter('orderBy'); + $scope.friends = [ + { name: 'John', phone: '555-1212', age: 10 }, + { name: 'Mary', phone: '555-9876', age: 19 }, + { name: 'Mike', phone: '555-4321', age: 21 }, + { name: 'Adam', phone: '555-5678', age: 35 }, + { name: 'Julie', phone: '555-8765', age: 29 } + ]; + + $scope.order = function(predicate, reverse) { + $scope.friends = orderBy($scope.friends, predicate, reverse); + }; + $scope.order('-age',false); + } + +
+ */ +orderByFilter.$inject = ['$parse']; +function orderByFilter($parse){ + return function(array, sortPredicate, reverseOrder) { + if (!isArray(array)) return array; + if (!sortPredicate) return array; + sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; + sortPredicate = map(sortPredicate, function(predicate){ + var descending = false, get = predicate || identity; + if (isString(predicate)) { + if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { + descending = predicate.charAt(0) == '-'; + predicate = predicate.substring(1); + } + get = $parse(predicate); + if (get.constant) { + var key = get(); + return reverseComparator(function(a,b) { + return compare(a[key], b[key]); + }, descending); + } + } + return reverseComparator(function(a,b){ + return compare(get(a),get(b)); + }, descending); + }); + var arrayCopy = []; + for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } + return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + + function comparator(o1, o2){ + for ( var i = 0; i < sortPredicate.length; i++) { + var comp = sortPredicate[i](o1, o2); + if (comp !== 0) return comp; + } + return 0; + } + function reverseComparator(comp, descending) { + return toBoolean(descending) + ? function(a,b){return comp(b,a);} + : comp; + } + function compare(v1, v2){ + var t1 = typeof v1; + var t2 = typeof v2; + if (t1 == t2) { + if (t1 == "string") { + v1 = v1.toLowerCase(); + v2 = v2.toLowerCase(); + } + if (v1 === v2) return 0; + return v1 < v2 ? -1 : 1; + } else { + return t1 < t2 ? -1 : 1; + } + } + }; +} + +function ngDirective(directive) { + if (isFunction(directive)) { + directive = { + link: directive + }; + } + directive.restrict = directive.restrict || 'AC'; + return valueFn(directive); +} + +/** + * @ngdoc directive + * @name a + * @restrict E + * + * @description + * Modifies the default behavior of the html A tag so that the default action is prevented when + * the href attribute is empty. + * + * This change permits the easy creation of action links with the `ngClick` directive + * without changing the location or causing page reloads, e.g.: + * `Add Item` + */ +var htmlAnchorDirective = valueFn({ + restrict: 'E', + compile: function(element, attr) { + + if (msie <= 8) { + + // turn link into a stylable link in IE + // but only if it doesn't have name attribute, in which case it's an anchor + if (!attr.href && !attr.name) { + attr.$set('href', ''); + } + + // add a comment node to anchors to workaround IE bug that causes element content to be reset + // to new attribute content if attribute is updated with value containing @ and element also + // contains value with @ + // see issue #1949 + element.append(document.createComment('IE fix')); + } + + if (!attr.href && !attr.xlinkHref && !attr.name) { + return function(scope, element) { + // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. + var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? + 'xlink:href' : 'href'; + element.on('click', function(event){ + // if we have no href url, then don't navigate anywhere. + if (!element.attr(href)) { + event.preventDefault(); + } + }); + }; + } + } +}); + +/** + * @ngdoc directive + * @name ngHref + * @restrict A + * @priority 99 + * + * @description + * Using Angular markup like `{{hash}}` in an href attribute will + * make the link go to the wrong URL if the user clicks it before + * Angular has a chance to replace the `{{hash}}` markup with its + * value. Until Angular replaces the markup the link will be broken + * and will most likely return a 404 error. + * + * The `ngHref` directive solves this problem. + * + * The wrong way to write it: + * ```html + * + * ``` + * + * The correct way to write it: + * ```html + * + * ``` + * + * @element A + * @param {template} ngHref any string which can contain `{{}}` markup. + * + * @example + * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes + * in links and their different behaviors: + + +
+
link 1 (link, don't reload)
+ link 2 (link, don't reload)
+ link 3 (link, reload!)
+ anchor (link, don't reload)
+ anchor (no link)
+ link (link, change location) +
+ + it('should execute ng-click but not reload when href without value', function() { + element(by.id('link-1')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('1'); + expect(element(by.id('link-1')).getAttribute('href')).toBe(''); + }); + + it('should execute ng-click but not reload when href empty string', function() { + element(by.id('link-2')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('2'); + expect(element(by.id('link-2')).getAttribute('href')).toBe(''); + }); + + it('should execute ng-click and change url when ng-href specified', function() { + expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/); + + element(by.id('link-3')).click(); + + // At this point, we navigate away from an Angular page, so we need + // to use browser.driver to get the base webdriver. + + browser.wait(function() { + return browser.driver.getCurrentUrl().then(function(url) { + return url.match(/\/123$/); + }); + }, 1000, 'page should navigate to /123'); + }); + + xit('should execute ng-click but not reload when href empty string and name specified', function() { + element(by.id('link-4')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('4'); + expect(element(by.id('link-4')).getAttribute('href')).toBe(''); + }); + + it('should execute ng-click but not reload when no href but name specified', function() { + element(by.id('link-5')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('5'); + expect(element(by.id('link-5')).getAttribute('href')).toBe(null); + }); + + it('should only change url when only ng-href', function() { + element(by.model('value')).clear(); + element(by.model('value')).sendKeys('6'); + expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/); + + element(by.id('link-6')).click(); + + // At this point, we navigate away from an Angular page, so we need + // to use browser.driver to get the base webdriver. + browser.wait(function() { + return browser.driver.getCurrentUrl().then(function(url) { + return url.match(/\/6$/); + }); + }, 1000, 'page should navigate to /6'); + }); + + + */ + +/** + * @ngdoc directive + * @name ngSrc + * @restrict A + * @priority 99 + * + * @description + * Using Angular markup like `{{hash}}` in a `src` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until Angular replaces the expression inside + * `{{hash}}`. The `ngSrc` directive solves this problem. + * + * The buggy way to write it: + * ```html + * + * ``` + * + * The correct way to write it: + * ```html + * + * ``` + * + * @element IMG + * @param {template} ngSrc any string which can contain `{{}}` markup. + */ + +/** + * @ngdoc directive + * @name ngSrcset + * @restrict A + * @priority 99 + * + * @description + * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until Angular replaces the expression inside + * `{{hash}}`. The `ngSrcset` directive solves this problem. + * + * The buggy way to write it: + * ```html + * + * ``` + * + * The correct way to write it: + * ```html + * + * ``` + * + * @element IMG + * @param {template} ngSrcset any string which can contain `{{}}` markup. + */ + +/** + * @ngdoc directive + * @name ngDisabled + * @restrict A + * @priority 100 + * + * @description + * + * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: + * ```html + *
+ * + *
+ * ``` + * + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as disabled. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngDisabled` directive solves this problem for the `disabled` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. + * + * @example + + + Click me to toggle:
+ +
+ + it('should toggle button', function() { + expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy(); + element(by.model('checked')).click(); + expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy(); + }); + +
+ * + * @element INPUT + * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, + * then special attribute "disabled" will be set on the element + */ + + +/** + * @ngdoc directive + * @name ngChecked + * @restrict A + * @priority 100 + * + * @description + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as checked. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngChecked` directive solves this problem for the `checked` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. + * @example + + + Check me to check both:
+ +
+ + it('should check both checkBoxes', function() { + expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy(); + element(by.model('master')).click(); + expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy(); + }); + +
+ * + * @element INPUT + * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, + * then special attribute "checked" will be set on the element + */ + + +/** + * @ngdoc directive + * @name ngReadonly + * @restrict A + * @priority 100 + * + * @description + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as readonly. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngReadonly` directive solves this problem for the `readonly` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. + * @example + + + Check me to make text readonly:
+ +
+ + it('should toggle readonly attr', function() { + expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy(); + element(by.model('checked')).click(); + expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy(); + }); + +
+ * + * @element INPUT + * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, + * then special attribute "readonly" will be set on the element + */ + + +/** + * @ngdoc directive + * @name ngSelected + * @restrict A + * @priority 100 + * + * @description + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as selected. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngSelected` directive solves this problem for the `selected` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. + * + * @example + + + Check me to select:
+ +
+ + it('should select Greetings!', function() { + expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy(); + element(by.model('selected')).click(); + expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy(); + }); + +
+ * + * @element OPTION + * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, + * then special attribute "selected" will be set on the element + */ + +/** + * @ngdoc directive + * @name ngOpen + * @restrict A + * @priority 100 + * + * @description + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as open. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngOpen` directive solves this problem for the `open` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. + * @example + + + Check me check multiple:
+
+ Show/Hide me +
+
+ + it('should toggle open', function() { + expect(element(by.id('details')).getAttribute('open')).toBeFalsy(); + element(by.model('open')).click(); + expect(element(by.id('details')).getAttribute('open')).toBeTruthy(); + }); + +
+ * + * @element DETAILS + * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, + * then special attribute "open" will be set on the element + */ + +var ngAttributeAliasDirectives = {}; + + +// boolean attrs are evaluated +forEach(BOOLEAN_ATTR, function(propName, attrName) { + // binding to multiple is not supported + if (propName == "multiple") return; + + var normalized = directiveNormalize('ng-' + attrName); + ngAttributeAliasDirectives[normalized] = function() { + return { + priority: 100, + link: function(scope, element, attr) { + scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { + attr.$set(attrName, !!value); + }); + } + }; + }; +}); + + +// ng-src, ng-srcset, ng-href are interpolated +forEach(['src', 'srcset', 'href'], function(attrName) { + var normalized = directiveNormalize('ng-' + attrName); + ngAttributeAliasDirectives[normalized] = function() { + return { + priority: 99, // it needs to run after the attributes are interpolated + link: function(scope, element, attr) { + var propName = attrName, + name = attrName; + + if (attrName === 'href' && + toString.call(element.prop('href')) === '[object SVGAnimatedString]') { + name = 'xlinkHref'; + attr.$attr[name] = 'xlink:href'; + propName = null; + } + + attr.$observe(normalized, function(value) { + if (!value) + return; + + attr.$set(name, value); + + // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist + // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need + // to set the property as well to achieve the desired effect. + // we use attr[attrName] value since $set can sanitize the url. + if (msie && propName) element.prop(propName, attr[name]); + }); + } + }; + }; +}); + +/* global -nullFormCtrl */ +var nullFormCtrl = { + $addControl: noop, + $removeControl: noop, + $setValidity: noop, + $setDirty: noop, + $setPristine: noop +}; + +/** + * @ngdoc type + * @name form.FormController + * + * @property {boolean} $pristine True if user has not interacted with the form yet. + * @property {boolean} $dirty True if user has already interacted with the form. + * @property {boolean} $valid True if all of the containing forms and controls are valid. + * @property {boolean} $invalid True if at least one containing control or form is invalid. + * + * @property {Object} $error Is an object hash, containing references to all invalid controls or + * forms, where: + * + * - keys are validation tokens (error names), + * - values are arrays of controls or forms that are invalid for given error name. + * + * + * Built-in validation tokens: + * + * - `email` + * - `max` + * - `maxlength` + * - `min` + * - `minlength` + * - `number` + * - `pattern` + * - `required` + * - `url` + * + * @description + * `FormController` keeps track of all its controls and nested forms as well as the state of them, + * such as being valid/invalid or dirty/pristine. + * + * Each {@link ng.directive:form form} directive creates an instance + * of `FormController`. + * + */ +//asks for $scope to fool the BC controller module +FormController.$inject = ['$element', '$attrs', '$scope', '$animate']; +function FormController(element, attrs, $scope, $animate) { + var form = this, + parentForm = element.parent().controller('form') || nullFormCtrl, + invalidCount = 0, // used to easily determine if we are valid + errors = form.$error = {}, + controls = []; + + // init state + form.$name = attrs.name || attrs.ngForm; + form.$dirty = false; + form.$pristine = true; + form.$valid = true; + form.$invalid = false; + + parentForm.$addControl(form); + + // Setup initial state of the control + element.addClass(PRISTINE_CLASS); + toggleValidCss(true); + + // convenience method for easy toggling of classes + function toggleValidCss(isValid, validationErrorKey) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + $animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); + $animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); + } + + /** + * @ngdoc method + * @name form.FormController#$addControl + * + * @description + * Register a control with the form. + * + * Input elements using ngModelController do this automatically when they are linked. + */ + form.$addControl = function(control) { + // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored + // and not added to the scope. Now we throw an error. + assertNotHasOwnProperty(control.$name, 'input'); + controls.push(control); + + if (control.$name) { + form[control.$name] = control; + } + }; + + /** + * @ngdoc method + * @name form.FormController#$removeControl + * + * @description + * Deregister a control from the form. + * + * Input elements using ngModelController do this automatically when they are destroyed. + */ + form.$removeControl = function(control) { + if (control.$name && form[control.$name] === control) { + delete form[control.$name]; + } + forEach(errors, function(queue, validationToken) { + form.$setValidity(validationToken, true, control); + }); + + arrayRemove(controls, control); + }; + + /** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Sets the validity of a form control. + * + * This method will also propagate to parent forms. + */ + form.$setValidity = function(validationToken, isValid, control) { + var queue = errors[validationToken]; + + if (isValid) { + if (queue) { + arrayRemove(queue, control); + if (!queue.length) { + invalidCount--; + if (!invalidCount) { + toggleValidCss(isValid); + form.$valid = true; + form.$invalid = false; + } + errors[validationToken] = false; + toggleValidCss(true, validationToken); + parentForm.$setValidity(validationToken, true, form); + } + } + + } else { + if (!invalidCount) { + toggleValidCss(isValid); + } + if (queue) { + if (includes(queue, control)) return; + } else { + errors[validationToken] = queue = []; + invalidCount++; + toggleValidCss(false, validationToken); + parentForm.$setValidity(validationToken, false, form); + } + queue.push(control); + + form.$valid = false; + form.$invalid = true; + } + }; + + /** + * @ngdoc method + * @name form.FormController#$setDirty + * + * @description + * Sets the form to a dirty state. + * + * This method can be called to add the 'ng-dirty' class and set the form to a dirty + * state (ng-dirty class). This method will also propagate to parent forms. + */ + form.$setDirty = function() { + $animate.removeClass(element, PRISTINE_CLASS); + $animate.addClass(element, DIRTY_CLASS); + form.$dirty = true; + form.$pristine = false; + parentForm.$setDirty(); + }; + + /** + * @ngdoc method + * @name form.FormController#$setPristine + * + * @description + * Sets the form to its pristine state. + * + * This method can be called to remove the 'ng-dirty' class and set the form to its pristine + * state (ng-pristine class). This method will also propagate to all the controls contained + * in this form. + * + * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after + * saving or resetting it. + */ + form.$setPristine = function () { + $animate.removeClass(element, DIRTY_CLASS); + $animate.addClass(element, PRISTINE_CLASS); + form.$dirty = false; + form.$pristine = true; + forEach(controls, function(control) { + control.$setPristine(); + }); + }; +} + + +/** + * @ngdoc directive + * @name ngForm + * @restrict EAC + * + * @description + * Nestable alias of {@link ng.directive:form `form`} directive. HTML + * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a + * sub-group of controls needs to be determined. + * + * Note: the purpose of `ngForm` is to group controls, + * but not to be a replacement for the `
` tag with all of its capabilities + * (e.g. posting to the server, ...). + * + * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into + * related scope, under this name. + * + */ + + /** + * @ngdoc directive + * @name form + * @restrict E + * + * @description + * Directive that instantiates + * {@link form.FormController FormController}. + * + * If the `name` attribute is specified, the form controller is published onto the current scope under + * this name. + * + * # Alias: {@link ng.directive:ngForm `ngForm`} + * + * In Angular forms can be nested. This means that the outer form is valid when all of the child + * forms are valid as well. However, browsers do not allow nesting of `` elements, so + * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to + * `` but can be nested. This allows you to have nested forms, which is very useful when + * using Angular validation directives in forms that are dynamically generated using the + * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` + * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an + * `ngForm` directive and nest these in an outer `form` element. + * + * + * # CSS classes + * - `ng-valid` is set if the form is valid. + * - `ng-invalid` is set if the form is invalid. + * - `ng-pristine` is set if the form is pristine. + * - `ng-dirty` is set if the form is dirty. + * + * Keep in mind that ngAnimate can detect each of these classes when added and removed. + * + * + * # Submitting a form and preventing the default action + * + * Since the role of forms in client-side Angular applications is different than in classical + * roundtrip apps, it is desirable for the browser not to translate the form submission into a full + * page reload that sends the data to the server. Instead some javascript logic should be triggered + * to handle the form submission in an application-specific way. + * + * For this reason, Angular prevents the default action (form submission to the server) unless the + * `` element has an `action` attribute specified. + * + * You can use one of the following two ways to specify what javascript method should be called when + * a form is submitted: + * + * - {@link ng.directive:ngSubmit ngSubmit} directive on the form element + * - {@link ng.directive:ngClick ngClick} directive on the first + * button or input field of type submit (input[type=submit]) + * + * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} + * or {@link ng.directive:ngClick ngClick} directives. + * This is because of the following form submission rules in the HTML specification: + * + * - If a form has only one input field then hitting enter in this field triggers form submit + * (`ngSubmit`) + * - if a form has 2+ input fields and no buttons or input[type=submit] then hitting enter + * doesn't trigger submit + * - if a form has one or more input fields and one or more buttons or input[type=submit] then + * hitting enter in any of the input fields will trigger the click handler on the *first* button or + * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) + * + * @param {string=} name Name of the form. If specified, the form controller will be published into + * related scope, under this name. + * + * ## Animation Hooks + * + * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. + * These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any + * other validations that are performed within the form. Animations in ngForm are similar to how + * they work in ngClass and animations can be hooked into using CSS transitions, keyframes as well + * as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style a form element + * that has been rendered as invalid after it has been validated: + * + *
+ * //be sure to include ngAnimate as a module to hook into more
+ * //advanced animations
+ * .my-form {
+ *   transition:0.5s linear all;
+ *   background: white;
+ * }
+ * .my-form.ng-invalid {
+ *   background: red;
+ *   color:white;
+ * }
+ * 
+ * + * @example + + + + + + userType: + Required!
+ userType = {{userType}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+ +
+ + it('should initialize to model', function() { + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + + expect(userType.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + var userInput = element(by.model('userType')); + + userInput.clear(); + userInput.sendKeys(''); + + expect(userType.getText()).toEqual('userType ='); + expect(valid.getText()).toContain('false'); + }); + +
+ * + */ +var formDirectiveFactory = function(isNgForm) { + return ['$timeout', function($timeout) { + var formDirective = { + name: 'form', + restrict: isNgForm ? 'EAC' : 'E', + controller: FormController, + compile: function() { + return { + pre: function(scope, formElement, attr, controller) { + if (!attr.action) { + // we can't use jq events because if a form is destroyed during submission the default + // action is not prevented. see #1238 + // + // IE 9 is not affected because it doesn't fire a submit event and try to do a full + // page reload if the form was destroyed by submission of the form via a click handler + // on a button in the form. Looks like an IE9 specific bug. + var preventDefaultListener = function(event) { + event.preventDefault + ? event.preventDefault() + : event.returnValue = false; // IE + }; + + addEventListenerFn(formElement[0], 'submit', preventDefaultListener); + + // unregister the preventDefault listener so that we don't not leak memory but in a + // way that will achieve the prevention of the default action. + formElement.on('$destroy', function() { + $timeout(function() { + removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); + }, 0, false); + }); + } + + var parentFormCtrl = formElement.parent().controller('form'), + alias = attr.name || attr.ngForm; + + if (alias) { + setter(scope, alias, controller, alias); + } + if (parentFormCtrl) { + formElement.on('$destroy', function() { + parentFormCtrl.$removeControl(controller); + if (alias) { + setter(scope, alias, undefined, alias); + } + extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + }); + } + } + }; + } + }; + + return formDirective; + }]; +}; + +var formDirective = formDirectiveFactory(); +var ngFormDirective = formDirectiveFactory(true); + +/* global + + -VALID_CLASS, + -INVALID_CLASS, + -PRISTINE_CLASS, + -DIRTY_CLASS +*/ + +var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; +var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i; +var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; + +var inputType = { + + /** + * @ngdoc input + * @name input[text] + * + * @description + * Standard HTML text input with angular data binding. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Adds `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. + * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the + * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + * patterns defined as scope expressions. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * + * @example + + + +
+ Single word: + + Required! + + Single word only! + + text = {{text}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + var text = element(by.binding('text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('text')); + + it('should initialize to model', function() { + expect(text.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); + }); + + it('should be invalid if multi word', function() { + input.clear(); + input.sendKeys('hello world'); + + expect(valid.getText()).toContain('false'); + }); + +
+ */ + 'text': textInputType, + + + /** + * @ngdoc input + * @name input[number] + * + * @description + * Text input with number validation and transformation. Sets the `number` validation + * error if not a valid number. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. + * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the + * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + * patterns defined as scope expressions. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ Number: + + Required! + + Not valid number! + value = {{value}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + var value = element(by.binding('value')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + it('should initialize to model', function() { + expect(value.getText()).toContain('12'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); + }); + + it('should be invalid if over max', function() { + input.clear(); + input.sendKeys('123'); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); + }); + +
+ */ + 'number': numberInputType, + + + /** + * @ngdoc input + * @name input[url] + * + * @description + * Text input with URL validation. Sets the `url` validation error key if the content is not a + * valid URL. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. + * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the + * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + * patterns defined as scope expressions. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ URL: + + Required! + + Not valid url! + text = {{text}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+ myForm.$error.url = {{!!myForm.$error.url}}
+
+
+ + var text = element(by.binding('text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('text')); + + it('should initialize to model', function() { + expect(text.getText()).toContain('http://google.com'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); + }); + + it('should be invalid if not url', function() { + input.clear(); + input.sendKeys('box'); + + expect(valid.getText()).toContain('false'); + }); + +
+ */ + 'url': urlInputType, + + + /** + * @ngdoc input + * @name input[email] + * + * @description + * Text input with email validation. Sets the `email` validation error key if not a valid email + * address. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. + * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the + * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + * patterns defined as scope expressions. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ Email: + + Required! + + Not valid email! + text = {{text}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+ myForm.$error.email = {{!!myForm.$error.email}}
+
+
+ + var text = element(by.binding('text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('text')); + + it('should initialize to model', function() { + expect(text.getText()).toContain('me@example.com'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); + }); + + it('should be invalid if not email', function() { + input.clear(); + input.sendKeys('xxx'); + + expect(valid.getText()).toContain('false'); + }); + +
+ */ + 'email': emailInputType, + + + /** + * @ngdoc input + * @name input[radio] + * + * @description + * HTML radio button. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string} value The value to which the expression should be set when selected. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * @param {string} ngValue Angular expression which sets the value to which the expression should + * be set when selected. + * + * @example + + + +
+ Red
+ Green
+ Blue
+ color = {{color | json}}
+
+ Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`. +
+ + it('should change state', function() { + var color = element(by.binding('color')); + + expect(color.getText()).toContain('blue'); + + element.all(by.model('color')).get(0).click(); + + expect(color.getText()).toContain('red'); + }); + +
+ */ + 'radio': radioInputType, + + + /** + * @ngdoc input + * @name input[checkbox] + * + * @description + * HTML checkbox. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} ngTrueValue The value to which the expression should be set when selected. + * @param {string=} ngFalseValue The value to which the expression should be set when not selected. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ Value1:
+ Value2:
+ value1 = {{value1}}
+ value2 = {{value2}}
+
+
+ + it('should change state', function() { + var value1 = element(by.binding('value1')); + var value2 = element(by.binding('value2')); + + expect(value1.getText()).toContain('true'); + expect(value2.getText()).toContain('YES'); + + element(by.model('value1')).click(); + element(by.model('value2')).click(); + + expect(value1.getText()).toContain('false'); + expect(value2.getText()).toContain('NO'); + }); + +
+ */ + 'checkbox': checkboxInputType, + + 'hidden': noop, + 'button': noop, + 'submit': noop, + 'reset': noop, + 'file': noop +}; + +// A helper function to call $setValidity and return the value / undefined, +// a pattern that is repeated a lot in the input validation logic. +function validate(ctrl, validatorName, validity, value){ + ctrl.$setValidity(validatorName, validity); + return validity ? value : undefined; +} + + +function addNativeHtml5Validators(ctrl, validatorName, element) { + var validity = element.prop('validity'); + if (isObject(validity)) { + var validator = function(value) { + // Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can + // perform the required validation) + if (!ctrl.$error[validatorName] && (validity.badInput || validity.customError || + validity.typeMismatch) && !validity.valueMissing) { + ctrl.$setValidity(validatorName, false); + return; + } + return value; + }; + ctrl.$parsers.push(validator); + } +} + +function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { + var validity = element.prop('validity'); + var placeholder = element[0].placeholder, noevent = {}; + + // In composition mode, users are still inputing intermediate text buffer, + // hold the listener until composition is done. + // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent + if (!$sniffer.android) { + var composing = false; + + element.on('compositionstart', function(data) { + composing = true; + }); + + element.on('compositionend', function() { + composing = false; + listener(); + }); + } + + var listener = function(ev) { + if (composing) return; + var value = element.val(); + + // IE (11 and under) seem to emit an 'input' event if the placeholder value changes. + // We don't want to dirty the value when this happens, so we abort here. Unfortunately, + // IE also sends input events for other non-input-related things, (such as focusing on a + // form control), so this change is not entirely enough to solve this. + if (msie && (ev || noevent).type === 'input' && element[0].placeholder !== placeholder) { + placeholder = element[0].placeholder; + return; + } + + // By default we will trim the value + // If the attribute ng-trim exists we will avoid trimming + // e.g. + if (toBoolean(attr.ngTrim || 'T')) { + value = trim(value); + } + + if (ctrl.$viewValue !== value || + // If the value is still empty/falsy, and there is no `required` error, run validators + // again. This enables HTML5 constraint validation errors to affect Angular validation + // even when the first character entered causes an error. + (validity && value === '' && !validity.valueMissing)) { + if (scope.$$phase) { + ctrl.$setViewValue(value); + } else { + scope.$apply(function() { + ctrl.$setViewValue(value); + }); + } + } + }; + + // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the + // input event on backspace, delete or cut + if ($sniffer.hasEvent('input')) { + element.on('input', listener); + } else { + var timeout; + + var deferListener = function() { + if (!timeout) { + timeout = $browser.defer(function() { + listener(); + timeout = null; + }); + } + }; + + element.on('keydown', function(event) { + var key = event.keyCode; + + // ignore + // command modifiers arrows + if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; + + deferListener(); + }); + + // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it + if ($sniffer.hasEvent('paste')) { + element.on('paste cut', deferListener); + } + } + + // if user paste into input using mouse on older browser + // or form autocomplete on newer browser, we need "change" event to catch it + element.on('change', listener); + + ctrl.$render = function() { + element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); + }; + + // pattern validator + var pattern = attr.ngPattern, + patternValidator, + match; + + if (pattern) { + var validateRegex = function(regexp, value) { + return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value); + }; + match = pattern.match(/^\/(.*)\/([gim]*)$/); + if (match) { + pattern = new RegExp(match[1], match[2]); + patternValidator = function(value) { + return validateRegex(pattern, value); + }; + } else { + patternValidator = function(value) { + var patternObj = scope.$eval(pattern); + + if (!patternObj || !patternObj.test) { + throw minErr('ngPattern')('noregexp', + 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern, + patternObj, startingTag(element)); + } + return validateRegex(patternObj, value); + }; + } + + ctrl.$formatters.push(patternValidator); + ctrl.$parsers.push(patternValidator); + } + + // min length validator + if (attr.ngMinlength) { + var minlength = int(attr.ngMinlength); + var minLengthValidator = function(value) { + return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value); + }; + + ctrl.$parsers.push(minLengthValidator); + ctrl.$formatters.push(minLengthValidator); + } + + // max length validator + if (attr.ngMaxlength) { + var maxlength = int(attr.ngMaxlength); + var maxLengthValidator = function(value) { + return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value); + }; + + ctrl.$parsers.push(maxLengthValidator); + ctrl.$formatters.push(maxLengthValidator); + } +} + +function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); + + ctrl.$parsers.push(function(value) { + var empty = ctrl.$isEmpty(value); + if (empty || NUMBER_REGEXP.test(value)) { + ctrl.$setValidity('number', true); + return value === '' ? null : (empty ? value : parseFloat(value)); + } else { + ctrl.$setValidity('number', false); + return undefined; + } + }); + + addNativeHtml5Validators(ctrl, 'number', element); + + ctrl.$formatters.push(function(value) { + return ctrl.$isEmpty(value) ? '' : '' + value; + }); + + if (attr.min) { + var minValidator = function(value) { + var min = parseFloat(attr.min); + return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value); + }; + + ctrl.$parsers.push(minValidator); + ctrl.$formatters.push(minValidator); + } + + if (attr.max) { + var maxValidator = function(value) { + var max = parseFloat(attr.max); + return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value); + }; + + ctrl.$parsers.push(maxValidator); + ctrl.$formatters.push(maxValidator); + } + + ctrl.$formatters.push(function(value) { + return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value); + }); +} + +function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var urlValidator = function(value) { + return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); + }; + + ctrl.$formatters.push(urlValidator); + ctrl.$parsers.push(urlValidator); +} + +function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var emailValidator = function(value) { + return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); + }; + + ctrl.$formatters.push(emailValidator); + ctrl.$parsers.push(emailValidator); +} + +function radioInputType(scope, element, attr, ctrl) { + // make the name unique, if not defined + if (isUndefined(attr.name)) { + element.attr('name', nextUid()); + } + + element.on('click', function() { + if (element[0].checked) { + scope.$apply(function() { + ctrl.$setViewValue(attr.value); + }); + } + }); + + ctrl.$render = function() { + var value = attr.value; + element[0].checked = (value == ctrl.$viewValue); + }; + + attr.$observe('value', ctrl.$render); +} + +function checkboxInputType(scope, element, attr, ctrl) { + var trueValue = attr.ngTrueValue, + falseValue = attr.ngFalseValue; + + if (!isString(trueValue)) trueValue = true; + if (!isString(falseValue)) falseValue = false; + + element.on('click', function() { + scope.$apply(function() { + ctrl.$setViewValue(element[0].checked); + }); + }); + + ctrl.$render = function() { + element[0].checked = ctrl.$viewValue; + }; + + // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox. + ctrl.$isEmpty = function(value) { + return value !== trueValue; + }; + + ctrl.$formatters.push(function(value) { + return value === trueValue; + }); + + ctrl.$parsers.push(function(value) { + return value ? trueValue : falseValue; + }); +} + + +/** + * @ngdoc directive + * @name textarea + * @restrict E + * + * @description + * HTML textarea element control with angular data-binding. The data-binding and validation + * properties of this element are exactly the same as those of the + * {@link ng.directive:input input element}. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. + * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the + * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + * patterns defined as scope expressions. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + */ + + +/** + * @ngdoc directive + * @name input + * @restrict E + * + * @description + * HTML input element control with angular data-binding. Input control follows HTML5 input types + * and polyfills the HTML5 validation behavior for older browsers. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {boolean=} ngRequired Sets `required` attribute if set to true + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. + * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the + * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + * patterns defined as scope expressions. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+
+ User name: + + Required!
+ Last name: + + Too short! + + Too long!
+
+
+ user = {{user}}
+ myForm.userName.$valid = {{myForm.userName.$valid}}
+ myForm.userName.$error = {{myForm.userName.$error}}
+ myForm.lastName.$valid = {{myForm.lastName.$valid}}
+ myForm.lastName.$error = {{myForm.lastName.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+ myForm.$error.minlength = {{!!myForm.$error.minlength}}
+ myForm.$error.maxlength = {{!!myForm.$error.maxlength}}
+
+
+ + var user = element(by.binding('{{user}}')); + var userNameValid = element(by.binding('myForm.userName.$valid')); + var lastNameValid = element(by.binding('myForm.lastName.$valid')); + var lastNameError = element(by.binding('myForm.lastName.$error')); + var formValid = element(by.binding('myForm.$valid')); + var userNameInput = element(by.model('user.name')); + var userLastInput = element(by.model('user.last')); + + it('should initialize to model', function() { + expect(user.getText()).toContain('{"name":"guest","last":"visitor"}'); + expect(userNameValid.getText()).toContain('true'); + expect(formValid.getText()).toContain('true'); + }); + + it('should be invalid if empty when required', function() { + userNameInput.clear(); + userNameInput.sendKeys(''); + + expect(user.getText()).toContain('{"last":"visitor"}'); + expect(userNameValid.getText()).toContain('false'); + expect(formValid.getText()).toContain('false'); + }); + + it('should be valid if empty when min length is set', function() { + userLastInput.clear(); + userLastInput.sendKeys(''); + + expect(user.getText()).toContain('{"name":"guest","last":""}'); + expect(lastNameValid.getText()).toContain('true'); + expect(formValid.getText()).toContain('true'); + }); + + it('should be invalid if less than required min length', function() { + userLastInput.clear(); + userLastInput.sendKeys('xx'); + + expect(user.getText()).toContain('{"name":"guest"}'); + expect(lastNameValid.getText()).toContain('false'); + expect(lastNameError.getText()).toContain('minlength'); + expect(formValid.getText()).toContain('false'); + }); + + it('should be invalid if longer than max length', function() { + userLastInput.clear(); + userLastInput.sendKeys('some ridiculously long name'); + + expect(user.getText()).toContain('{"name":"guest"}'); + expect(lastNameValid.getText()).toContain('false'); + expect(lastNameError.getText()).toContain('maxlength'); + expect(formValid.getText()).toContain('false'); + }); + +
+ */ +var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { + return { + restrict: 'E', + require: '?ngModel', + link: function(scope, element, attr, ctrl) { + if (ctrl) { + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, + $browser); + } + } + }; +}]; + +var VALID_CLASS = 'ng-valid', + INVALID_CLASS = 'ng-invalid', + PRISTINE_CLASS = 'ng-pristine', + DIRTY_CLASS = 'ng-dirty'; + +/** + * @ngdoc type + * @name ngModel.NgModelController + * + * @property {string} $viewValue Actual string value in the view. + * @property {*} $modelValue The value in the model, that the control is bound to. + * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever + the control reads value from the DOM. Each function is called, in turn, passing the value + through to the next. The last return value is used to populate the model. + Used to sanitize / convert the value as well as validation. For validation, + the parsers should update the validity state using + {@link ngModel.NgModelController#$setValidity $setValidity()}, + and return `undefined` for invalid values. + + * + * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever + the model value changes. Each function is called, in turn, passing the value through to the + next. Used to format / convert values for display in the control and validation. + * ```js + * function formatter(value) { + * if (value) { + * return value.toUpperCase(); + * } + * } + * ngModel.$formatters.push(formatter); + * ``` + * + * @property {Array.} $viewChangeListeners Array of functions to execute whenever the + * view value has changed. It is called with no arguments, and its return value is ignored. + * This can be used in place of additional $watches against the model value. + * + * @property {Object} $error An object hash with all errors as keys. + * + * @property {boolean} $pristine True if user has not interacted with the control yet. + * @property {boolean} $dirty True if user has already interacted with the control. + * @property {boolean} $valid True if there is no error. + * @property {boolean} $invalid True if at least one error on the control. + * + * @description + * + * `NgModelController` provides API for the `ng-model` directive. The controller contains + * services for data-binding, validation, CSS updates, and value formatting and parsing. It + * purposefully does not contain any logic which deals with DOM rendering or listening to + * DOM events. Such DOM related logic should be provided by other directives which make use of + * `NgModelController` for data-binding. + * + * ## Custom Control Example + * This example shows how to use `NgModelController` with a custom control to achieve + * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) + * collaborate together to achieve the desired result. + * + * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element + * contents be edited in place by the user. This will not work on older browsers. + * + * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize} + * module to automatically remove "bad" content like inline event listener (e.g. ``). + * However, as we are using `$sce` the model can still decide to to provide unsafe content if it marks + * that content using the `$sce` service. + * + * + + [contenteditable] { + border: 1px solid black; + background-color: white; + min-height: 20px; + } + + .ng-invalid { + border: 1px solid red; + } + + + + angular.module('customControl', ['ngSanitize']). + directive('contenteditable', ['$sce', function($sce) { + return { + restrict: 'A', // only activate on element attribute + require: '?ngModel', // get a hold of NgModelController + link: function(scope, element, attrs, ngModel) { + if(!ngModel) return; // do nothing if no ng-model + + // Specify how UI should be updated + ngModel.$render = function() { + element.html($sce.getTrustedHtml(ngModel.$viewValue || '')); + }; + + // Listen for change events to enable binding + element.on('blur keyup change', function() { + scope.$apply(read); + }); + read(); // initialize + + // Write data to the model + function read() { + var html = element.html(); + // When we clear the content editable the browser leaves a
behind + // If strip-br attribute is provided then we strip this out + if( attrs.stripBr && html == '
' ) { + html = ''; + } + ngModel.$setViewValue(html); + } + } + }; + }]); +
+ +
+
Change me!
+ Required! +
+ +
+
+ + it('should data-bind and become invalid', function() { + if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { + // SafariDriver can't handle contenteditable + // and Firefox driver can't clear contenteditables very well + return; + } + var contentEditable = element(by.css('[contenteditable]')); + var content = 'Change me!'; + + expect(contentEditable.getText()).toEqual(content); + + contentEditable.clear(); + contentEditable.sendKeys(protractor.Key.BACK_SPACE); + expect(contentEditable.getText()).toEqual(''); + expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/); + }); + + *
+ * + * + */ +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', + function($scope, $exceptionHandler, $attr, $element, $parse, $animate) { + this.$viewValue = Number.NaN; + this.$modelValue = Number.NaN; + this.$parsers = []; + this.$formatters = []; + this.$viewChangeListeners = []; + this.$pristine = true; + this.$dirty = false; + this.$valid = true; + this.$invalid = false; + this.$name = $attr.name; + + var ngModelGet = $parse($attr.ngModel), + ngModelSet = ngModelGet.assign; + + if (!ngModelSet) { + throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", + $attr.ngModel, startingTag($element)); + } + + /** + * @ngdoc method + * @name ngModel.NgModelController#$render + * + * @description + * Called when the view needs to be updated. It is expected that the user of the ng-model + * directive will implement this method. + */ + this.$render = noop; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$isEmpty + * + * @description + * This is called when we need to determine if the value of the input is empty. + * + * For instance, the required directive does this to work out if the input has data or not. + * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. + * + * You can override this for input directives whose concept of being empty is different to the + * default. The `checkboxInputType` directive does this because in its case a value of `false` + * implies empty. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is empty. + */ + this.$isEmpty = function(value) { + return isUndefined(value) || value === '' || value === null || value !== value; + }; + + var parentForm = $element.inheritedData('$formController') || nullFormCtrl, + invalidCount = 0, // used to easily determine if we are valid + $error = this.$error = {}; // keep invalid keys here + + + // Setup initial state of the control + $element.addClass(PRISTINE_CLASS); + toggleValidCss(true); + + // convenience method for easy toggling of classes + function toggleValidCss(isValid, validationErrorKey) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + $animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); + $animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); + } + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setValidity + * + * @description + * Change the validity state, and notifies the form when the control changes validity. (i.e. it + * does not notify form if given validator is already marked as invalid). + * + * This method should be called by validators - i.e. the parser or formatter functions. + * + * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign + * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * class and can be bound to as `{{someForm.someControl.$error.myError}}` . + * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). + */ + this.$setValidity = function(validationErrorKey, isValid) { + // Purposeful use of ! here to cast isValid to boolean in case it is undefined + // jshint -W018 + if ($error[validationErrorKey] === !isValid) return; + // jshint +W018 + + if (isValid) { + if ($error[validationErrorKey]) invalidCount--; + if (!invalidCount) { + toggleValidCss(true); + this.$valid = true; + this.$invalid = false; + } + } else { + toggleValidCss(false); + this.$invalid = true; + this.$valid = false; + invalidCount++; + } + + $error[validationErrorKey] = !isValid; + toggleValidCss(isValid, validationErrorKey); + + parentForm.$setValidity(validationErrorKey, isValid, this); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setPristine + * + * @description + * Sets the control to its pristine state. + * + * This method can be called to remove the 'ng-dirty' class and set the control to its pristine + * state (ng-pristine class). + */ + this.$setPristine = function () { + this.$dirty = false; + this.$pristine = true; + $animate.removeClass($element, DIRTY_CLASS); + $animate.addClass($element, PRISTINE_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setViewValue + * + * @description + * Update the view value. + * + * This method should be called when the view value changes, typically from within a DOM event handler. + * For example {@link ng.directive:input input} and + * {@link ng.directive:select select} directives call it. + * + * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, + * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to + * `$modelValue` and the **expression** specified in the `ng-model` attribute. + * + * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. + * + * Note that calling this function does not trigger a `$digest`. + * + * @param {string} value Value from the view. + */ + this.$setViewValue = function(value) { + this.$viewValue = value; + + // change to dirty + if (this.$pristine) { + this.$dirty = true; + this.$pristine = false; + $animate.removeClass($element, PRISTINE_CLASS); + $animate.addClass($element, DIRTY_CLASS); + parentForm.$setDirty(); + } + + forEach(this.$parsers, function(fn) { + value = fn(value); + }); + + if (this.$modelValue !== value) { + this.$modelValue = value; + ngModelSet($scope, value); + forEach(this.$viewChangeListeners, function(listener) { + try { + listener(); + } catch(e) { + $exceptionHandler(e); + } + }); + } + }; + + // model -> value + var ctrl = this; + + $scope.$watch(function ngModelWatch() { + var value = ngModelGet($scope); + + // if scope model value and ngModel value are out of sync + if (ctrl.$modelValue !== value) { + + var formatters = ctrl.$formatters, + idx = formatters.length; + + ctrl.$modelValue = value; + while(idx--) { + value = formatters[idx](value); + } + + if (ctrl.$viewValue !== value) { + ctrl.$viewValue = value; + ctrl.$render(); + } + } + + return value; + }); +}]; + + +/** + * @ngdoc directive + * @name ngModel + * + * @element input + * + * @description + * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a + * property on the scope using {@link ngModel.NgModelController NgModelController}, + * which is created and exposed by this directive. + * + * `ngModel` is responsible for: + * + * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` + * require. + * - Providing validation behavior (i.e. required, number, email, url). + * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations. + * - Registering the control with its parent {@link ng.directive:form form}. + * + * Note: `ngModel` will try to bind to the property given by evaluating the expression on the + * current scope. If the property doesn't already exist on this scope, it will be created + * implicitly and added to the scope. + * + * For best practices on using `ngModel`, see: + * + * - [https://github.com/angular/angular.js/wiki/Understanding-Scopes] + * + * For basic examples, how to use `ngModel`, see: + * + * - {@link ng.directive:input input} + * - {@link input[text] text} + * - {@link input[checkbox] checkbox} + * - {@link input[radio] radio} + * - {@link input[number] number} + * - {@link input[email] email} + * - {@link input[url] url} + * - {@link ng.directive:select select} + * - {@link ng.directive:textarea textarea} + * + * # CSS classes + * The following CSS classes are added and removed on the associated input/select/textarea element + * depending on the validity of the model. + * + * - `ng-valid` is set if the model is valid. + * - `ng-invalid` is set if the model is invalid. + * - `ng-pristine` is set if the model is pristine. + * - `ng-dirty` is set if the model is dirty. + * + * Keep in mind that ngAnimate can detect each of these classes when added and removed. + * + * ## Animation Hooks + * + * Animations within models are triggered when any of the associated CSS classes are added and removed + * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`, + * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. + * The animations that are triggered within ngModel are similar to how they work in ngClass and + * animations can be hooked into using CSS transitions, keyframes as well as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style an input element + * that has been rendered as invalid after it has been validated: + * + *
+ * //be sure to include ngAnimate as a module to hook into more
+ * //advanced animations
+ * .my-input {
+ *   transition:0.5s linear all;
+ *   background: white;
+ * }
+ * .my-input.ng-invalid {
+ *   background: red;
+ *   color:white;
+ * }
+ * 
+ * + * @example + * + + + + Update input to see transitions when valid/invalid. + Integer is a valid value. +
+ +
+
+ *
+ */ +var ngModelDirective = function() { + return { + require: ['ngModel', '^?form'], + controller: NgModelController, + link: function(scope, element, attr, ctrls) { + // notify others, especially parent forms + + var modelCtrl = ctrls[0], + formCtrl = ctrls[1] || nullFormCtrl; + + formCtrl.$addControl(modelCtrl); + + scope.$on('$destroy', function() { + formCtrl.$removeControl(modelCtrl); + }); + } + }; +}; + + +/** + * @ngdoc directive + * @name ngChange + * + * @description + * Evaluate the given expression when the user changes the input. + * The expression is evaluated immediately, unlike the JavaScript onchange event + * which only triggers at the end of a change (usually, when the user leaves the + * form element or presses the return key). + * The expression is not evaluated when the value change is coming from the model. + * + * Note, this directive requires `ngModel` to be present. + * + * @element input + * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change + * in input value. + * + * @example + * + * + * + *
+ * + * + *
+ * debug = {{confirmed}}
+ * counter = {{counter}}
+ *
+ *
+ * + * var counter = element(by.binding('counter')); + * var debug = element(by.binding('confirmed')); + * + * it('should evaluate the expression if changing from view', function() { + * expect(counter.getText()).toContain('0'); + * + * element(by.id('ng-change-example1')).click(); + * + * expect(counter.getText()).toContain('1'); + * expect(debug.getText()).toContain('true'); + * }); + * + * it('should not evaluate the expression if changing from model', function() { + * element(by.id('ng-change-example2')).click(); + + * expect(counter.getText()).toContain('0'); + * expect(debug.getText()).toContain('true'); + * }); + * + *
+ */ +var ngChangeDirective = valueFn({ + require: 'ngModel', + link: function(scope, element, attr, ctrl) { + ctrl.$viewChangeListeners.push(function() { + scope.$eval(attr.ngChange); + }); + } +}); + + +var requiredDirective = function() { + return { + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + attr.required = true; // force truthy in case we are on non input element + + var validator = function(value) { + if (attr.required && ctrl.$isEmpty(value)) { + ctrl.$setValidity('required', false); + return; + } else { + ctrl.$setValidity('required', true); + return value; + } + }; + + ctrl.$formatters.push(validator); + ctrl.$parsers.unshift(validator); + + attr.$observe('required', function() { + validator(ctrl.$viewValue); + }); + } + }; +}; + + +/** + * @ngdoc directive + * @name ngList + * + * @description + * Text input that converts between a delimited string and an array of strings. The delimiter + * can be a fixed string (by default a comma) or a regular expression. + * + * @element input + * @param {string=} ngList optional delimiter that should be used to split the value. If + * specified in form `/something/` then the value will be converted into a regular expression. + * + * @example + + + +
+ List: + + Required! +
+ names = {{names}}
+ myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
+ myForm.namesInput.$error = {{myForm.namesInput.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + var listInput = element(by.model('names')); + var names = element(by.binding('{{names}}')); + var valid = element(by.binding('myForm.namesInput.$valid')); + var error = element(by.css('span.error')); + + it('should initialize to model', function() { + expect(names.getText()).toContain('["igor","misko","vojta"]'); + expect(valid.getText()).toContain('true'); + expect(error.getCssValue('display')).toBe('none'); + }); + + it('should be invalid if empty', function() { + listInput.clear(); + listInput.sendKeys(''); + + expect(names.getText()).toContain(''); + expect(valid.getText()).toContain('false'); + expect(error.getCssValue('display')).not.toBe('none'); }); + +
+ */ +var ngListDirective = function() { + return { + require: 'ngModel', + link: function(scope, element, attr, ctrl) { + var match = /\/(.*)\//.exec(attr.ngList), + separator = match && new RegExp(match[1]) || attr.ngList || ','; + + var parse = function(viewValue) { + // If the viewValue is invalid (say required but empty) it will be `undefined` + if (isUndefined(viewValue)) return; + + var list = []; + + if (viewValue) { + forEach(viewValue.split(separator), function(value) { + if (value) list.push(trim(value)); + }); + } + + return list; + }; + + ctrl.$parsers.push(parse); + ctrl.$formatters.push(function(value) { + if (isArray(value)) { + return value.join(', '); + } + + return undefined; + }); + + // Override the standard $isEmpty because an empty array means the input is empty. + ctrl.$isEmpty = function(value) { + return !value || !value.length; + }; + } + }; +}; + + +var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; +/** + * @ngdoc directive + * @name ngValue + * + * @description + * Binds the given expression to the value of `input[select]` or `input[radio]`, so + * that when the element is selected, the `ngModel` of that element is set to the + * bound value. + * + * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as + * shown below. + * + * @element input + * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute + * of the `input` element + * + * @example + + + +
+

Which is your favorite?

+ +
You chose {{my.favorite}}
+
+
+ + var favorite = element(by.binding('my.favorite')); + + it('should initialize to model', function() { + expect(favorite.getText()).toContain('unicorns'); + }); + it('should bind the values to the inputs', function() { + element.all(by.model('my.favorite')).get(0).click(); + expect(favorite.getText()).toContain('pizza'); + }); + +
+ */ +var ngValueDirective = function() { + return { + priority: 100, + compile: function(tpl, tplAttr) { + if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { + return function ngValueConstantLink(scope, elm, attr) { + attr.$set('value', scope.$eval(attr.ngValue)); + }; + } else { + return function ngValueLink(scope, elm, attr) { + scope.$watch(attr.ngValue, function valueWatchAction(value) { + attr.$set('value', value); + }); + }; + } + } + }; +}; + +/** + * @ngdoc directive + * @name ngBind + * @restrict AC + * + * @description + * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element + * with the value of a given expression, and to update the text content when the value of that + * expression changes. + * + * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like + * `{{ expression }}` which is similar but less verbose. + * + * It is preferable to use `ngBind` instead of `{{ expression }}` when a template is momentarily + * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an + * element attribute, it makes the bindings invisible to the user while the page is loading. + * + * An alternative solution to this problem would be using the + * {@link ng.directive:ngCloak ngCloak} directive. + * + * + * @element ANY + * @param {expression} ngBind {@link guide/expression Expression} to evaluate. + * + * @example + * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. + + + +
+ Enter name:
+ Hello ! +
+
+ + it('should check ng-bind', function() { + var nameInput = element(by.model('name')); + + expect(element(by.binding('name')).getText()).toBe('Whirled'); + nameInput.clear(); + nameInput.sendKeys('world'); + expect(element(by.binding('name')).getText()).toBe('world'); + }); + +
+ */ +var ngBindDirective = ngDirective(function(scope, element, attr) { + element.addClass('ng-binding').data('$binding', attr.ngBind); + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { + // We are purposefully using == here rather than === because we want to + // catch when value is "null or undefined" + // jshint -W041 + element.text(value == undefined ? '' : value); + }); +}); + + +/** + * @ngdoc directive + * @name ngBindTemplate + * + * @description + * The `ngBindTemplate` directive specifies that the element + * text content should be replaced with the interpolation of the template + * in the `ngBindTemplate` attribute. + * Unlike `ngBind`, the `ngBindTemplate` can contain multiple `{{` `}}` + * expressions. This directive is needed since some HTML elements + * (such as TITLE and OPTION) cannot contain SPAN elements. + * + * @element ANY + * @param {string} ngBindTemplate template of form + * {{ expression }} to eval. + * + * @example + * Try it here: enter text in text box and watch the greeting change. + + + +
+ Salutation:
+ Name:
+

+       
+
+ + it('should check ng-bind', function() { + var salutationElem = element(by.binding('salutation')); + var salutationInput = element(by.model('salutation')); + var nameInput = element(by.model('name')); + + expect(salutationElem.getText()).toBe('Hello World!'); + + salutationInput.clear(); + salutationInput.sendKeys('Greetings'); + nameInput.clear(); + nameInput.sendKeys('user'); + + expect(salutationElem.getText()).toBe('Greetings user!'); + }); + +
+ */ +var ngBindTemplateDirective = ['$interpolate', function($interpolate) { + return function(scope, element, attr) { + // TODO: move this to scenario runner + var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); + element.addClass('ng-binding').data('$binding', interpolateFn); + attr.$observe('ngBindTemplate', function(value) { + element.text(value); + }); + }; +}]; + + +/** + * @ngdoc directive + * @name ngBindHtml + * + * @description + * Creates a binding that will innerHTML the result of evaluating the `expression` into the current + * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link + * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize` + * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in + * core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to + * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example + * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}. + * + * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you + * will have an exception (instead of an exploit.) + * + * @element ANY + * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. + * + * @example + Try it here: enter text in text box and watch the greeting change. + + + +
+

+
+
+ + + angular.module('ngBindHtmlExample', ['ngSanitize']) + + .controller('ngBindHtmlCtrl', ['$scope', function ngBindHtmlCtrl($scope) { + $scope.myHTML = + 'I am an HTMLstring with links! and other stuff'; + }]); + + + + it('should check ng-bind-html', function() { + expect(element(by.binding('myHTML')).getText()).toBe( + 'I am an HTMLstring with links! and other stuff'); + }); + +
+ */ +var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { + return function(scope, element, attr) { + element.addClass('ng-binding').data('$binding', attr.ngBindHtml); + + var parsed = $parse(attr.ngBindHtml); + function getStringValue() { return (parsed(scope) || '').toString(); } + + scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { + element.html($sce.getTrustedHtml(parsed(scope)) || ''); + }); + }; +}]; + +function classDirective(name, selector) { + name = 'ngClass' + name; + return ['$animate', function($animate) { + return { + restrict: 'AC', + link: function(scope, element, attr) { + var oldVal; + + scope.$watch(attr[name], ngClassWatchAction, true); + + attr.$observe('class', function(value) { + ngClassWatchAction(scope.$eval(attr[name])); + }); + + + if (name !== 'ngClass') { + scope.$watch('$index', function($index, old$index) { + // jshint bitwise: false + var mod = $index & 1; + if (mod !== (old$index & 1)) { + var classes = arrayClasses(scope.$eval(attr[name])); + mod === selector ? + addClasses(classes) : + removeClasses(classes); + } + }); + } + + function addClasses(classes) { + var newClasses = digestClassCounts(classes, 1); + attr.$addClass(newClasses); + } + + function removeClasses(classes) { + var newClasses = digestClassCounts(classes, -1); + attr.$removeClass(newClasses); + } + + function digestClassCounts (classes, count) { + var classCounts = element.data('$classCounts') || {}; + var classesToUpdate = []; + forEach(classes, function (className) { + if (count > 0 || classCounts[className]) { + classCounts[className] = (classCounts[className] || 0) + count; + if (classCounts[className] === +(count > 0)) { + classesToUpdate.push(className); + } + } + }); + element.data('$classCounts', classCounts); + return classesToUpdate.join(' '); + } + + function updateClasses (oldClasses, newClasses) { + var toAdd = arrayDifference(newClasses, oldClasses); + var toRemove = arrayDifference(oldClasses, newClasses); + toRemove = digestClassCounts(toRemove, -1); + toAdd = digestClassCounts(toAdd, 1); + + if (toAdd.length === 0) { + $animate.removeClass(element, toRemove); + } else if (toRemove.length === 0) { + $animate.addClass(element, toAdd); + } else { + $animate.setClass(element, toAdd, toRemove); + } + } + + function ngClassWatchAction(newVal) { + if (selector === true || scope.$index % 2 === selector) { + var newClasses = arrayClasses(newVal || []); + if (!oldVal) { + addClasses(newClasses); + } else if (!equals(newVal,oldVal)) { + var oldClasses = arrayClasses(oldVal); + updateClasses(oldClasses, newClasses); + } + } + oldVal = shallowCopy(newVal); + } + } + }; + + function arrayDifference(tokens1, tokens2) { + var values = []; + + outer: + for(var i = 0; i < tokens1.length; i++) { + var token = tokens1[i]; + for(var j = 0; j < tokens2.length; j++) { + if(token == tokens2[j]) continue outer; + } + values.push(token); + } + return values; + } + + function arrayClasses (classVal) { + if (isArray(classVal)) { + return classVal; + } else if (isString(classVal)) { + return classVal.split(' '); + } else if (isObject(classVal)) { + var classes = [], i = 0; + forEach(classVal, function(v, k) { + if (v) { + classes = classes.concat(k.split(' ')); + } + }); + return classes; + } + return classVal; + } + }]; +} + +/** + * @ngdoc directive + * @name ngClass + * @restrict AC + * + * @description + * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding + * an expression that represents all classes to be added. + * + * The directive operates in three different ways, depending on which of three types the expression + * evaluates to: + * + * 1. If the expression evaluates to a string, the string should be one or more space-delimited class + * names. + * + * 2. If the expression evaluates to an array, each element of the array should be a string that is + * one or more space-delimited class names. + * + * 3. If the expression evaluates to an object, then for each key-value pair of the + * object with a truthy value the corresponding key is used as a class name. + * + * The directive won't add duplicate classes if a particular class was already set. + * + * When the expression changes, the previously added classes are removed and only then the + * new classes are added. + * + * @animations + * add - happens just before the class is applied to the element + * remove - happens just before the class is removed from the element + * + * @element ANY + * @param {expression} ngClass {@link guide/expression Expression} to eval. The result + * of the evaluation can be a string representing space delimited class + * names, an array, or a map of class names to boolean values. In the case of a map, the + * names of the properties whose values are truthy will be added as css classes to the + * element. + * + * @example Example that demonstrates basic bindings via ngClass directive. + + +

Map Syntax Example

+ deleted (apply "strike" class)
+ important (apply "bold" class)
+ error (apply "red" class) +
+

Using String Syntax

+ +
+

Using Array Syntax

+
+
+
+
+ + .strike { + text-decoration: line-through; + } + .bold { + font-weight: bold; + } + .red { + color: red; + } + + + var ps = element.all(by.css('p')); + + it('should let you toggle the class', function() { + + expect(ps.first().getAttribute('class')).not.toMatch(/bold/); + expect(ps.first().getAttribute('class')).not.toMatch(/red/); + + element(by.model('important')).click(); + expect(ps.first().getAttribute('class')).toMatch(/bold/); + + element(by.model('error')).click(); + expect(ps.first().getAttribute('class')).toMatch(/red/); + }); + + it('should let you toggle string example', function() { + expect(ps.get(1).getAttribute('class')).toBe(''); + element(by.model('style')).clear(); + element(by.model('style')).sendKeys('red'); + expect(ps.get(1).getAttribute('class')).toBe('red'); + }); + + it('array example should have 3 classes', function() { + expect(ps.last().getAttribute('class')).toBe(''); + element(by.model('style1')).sendKeys('bold'); + element(by.model('style2')).sendKeys('strike'); + element(by.model('style3')).sendKeys('red'); + expect(ps.last().getAttribute('class')).toBe('bold strike red'); + }); + +
+ + ## Animations + + The example below demonstrates how to perform animations using ngClass. + + + + + +
+ Sample Text +
+ + .base-class { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + } + + .base-class.my-class { + color: red; + font-size:3em; + } + + + it('should check ng-class', function() { + expect(element(by.css('.base-class')).getAttribute('class')).not. + toMatch(/my-class/); + + element(by.id('setbtn')).click(); + + expect(element(by.css('.base-class')).getAttribute('class')). + toMatch(/my-class/); + + element(by.id('clearbtn')).click(); + + expect(element(by.css('.base-class')).getAttribute('class')).not. + toMatch(/my-class/); + }); + +
+ + + ## ngClass and pre-existing CSS3 Transitions/Animations + The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure. + Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder + any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure + to view the step by step details of {@link ngAnimate.$animate#addclass $animate.addClass} and + {@link ngAnimate.$animate#removeclass $animate.removeClass}. + */ +var ngClassDirective = classDirective('', true); + +/** + * @ngdoc directive + * @name ngClassOdd + * @restrict AC + * + * @description + * The `ngClassOdd` and `ngClassEven` directives work exactly as + * {@link ng.directive:ngClass ngClass}, except they work in + * conjunction with `ngRepeat` and take effect only on odd (even) rows. + * + * This directive can be applied only within the scope of an + * {@link ng.directive:ngRepeat ngRepeat}. + * + * @element ANY + * @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result + * of the evaluation can be a string representing space delimited class names or an array. + * + * @example + + +
    +
  1. + + {{name}} + +
  2. +
+
+ + .odd { + color: red; + } + .even { + color: blue; + } + + + it('should check ng-class-odd and ng-class-even', function() { + expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). + toMatch(/odd/); + expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). + toMatch(/even/); + }); + +
+ */ +var ngClassOddDirective = classDirective('Odd', 0); + +/** + * @ngdoc directive + * @name ngClassEven + * @restrict AC + * + * @description + * The `ngClassOdd` and `ngClassEven` directives work exactly as + * {@link ng.directive:ngClass ngClass}, except they work in + * conjunction with `ngRepeat` and take effect only on odd (even) rows. + * + * This directive can be applied only within the scope of an + * {@link ng.directive:ngRepeat ngRepeat}. + * + * @element ANY + * @param {expression} ngClassEven {@link guide/expression Expression} to eval. The + * result of the evaluation can be a string representing space delimited class names or an array. + * + * @example + + +
    +
  1. + + {{name}}       + +
  2. +
+
+ + .odd { + color: red; + } + .even { + color: blue; + } + + + it('should check ng-class-odd and ng-class-even', function() { + expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). + toMatch(/odd/); + expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). + toMatch(/even/); + }); + +
+ */ +var ngClassEvenDirective = classDirective('Even', 1); + +/** + * @ngdoc directive + * @name ngCloak + * @restrict AC + * + * @description + * The `ngCloak` directive is used to prevent the Angular html template from being briefly + * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this + * directive to avoid the undesirable flicker effect caused by the html template display. + * + * The directive can be applied to the `` element, but the preferred usage is to apply + * multiple `ngCloak` directives to small portions of the page to permit progressive rendering + * of the browser view. + * + * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and + * `angular.min.js`. + * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). + * + * ```css + * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { + * display: none !important; + * } + * ``` + * + * When this css rule is loaded by the browser, all html elements (including their children) that + * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive + * during the compilation of the template it deletes the `ngCloak` element attribute, making + * the compiled element visible. + * + * For the best result, the `angular.js` script must be loaded in the head section of the html + * document; alternatively, the css rule above must be included in the external stylesheet of the + * application. + * + * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they + * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css + * class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below. + * + * @element ANY + * + * @example + + +
{{ 'hello' }}
+
{{ 'hello IE7' }}
+
+ + it('should remove the template directive and css class', function() { + expect($('#template1').getAttribute('ng-cloak')). + toBeNull(); + expect($('#template2').getAttribute('ng-cloak')). + toBeNull(); + }); + +
+ * + */ +var ngCloakDirective = ngDirective({ + compile: function(element, attr) { + attr.$set('ngCloak', undefined); + element.removeClass('ng-cloak'); + } +}); + +/** + * @ngdoc directive + * @name ngController + * + * @description + * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular + * supports the principles behind the Model-View-Controller design pattern. + * + * MVC components in angular: + * + * * Model — The Model is scope properties; scopes are attached to the DOM where scope properties + * are accessed through bindings. + * * View — The template (HTML with data bindings) that is rendered into the View. + * * Controller — The `ngController` directive specifies a Controller class; the class contains business + * logic behind the application to decorate the scope with functions and values + * + * Note that you can also attach controllers to the DOM by declaring it in a route definition + * via the {@link ngRoute.$route $route} service. A common mistake is to declare the controller + * again using `ng-controller` in the template itself. This will cause the controller to be attached + * and executed twice. + * + * @element ANY + * @scope + * @param {expression} ngController Name of a globally accessible constructor function or an + * {@link guide/expression expression} that on the current scope evaluates to a + * constructor function. The controller instance can be published into a scope property + * by specifying `as propertyName`. + * + * @example + * Here is a simple form for editing user contact information. Adding, removing, clearing, and + * greeting are methods declared on the controller (see source tab). These methods can + * easily be called from the angular markup. Any changes to the data are automatically reflected + * in the View without the need for a manual update. + * + * Two different declaration styles are included below: + * + * * one binds methods and properties directly onto the controller using `this`: + * `ng-controller="SettingsController1 as settings"` + * * one injects `$scope` into the controller: + * `ng-controller="SettingsController2"` + * + * The second option is more common in the Angular community, and is generally used in boilerplates + * and in this guide. However, there are advantages to binding properties directly to the controller + * and avoiding scope. + * + * * Using `controller as` makes it obvious which controller you are accessing in the template when + * multiple controllers apply to an element. + * * If you are writing your controllers as classes you have easier access to the properties and + * methods, which will appear on the scope, from inside the controller code. + * * Since there is always a `.` in the bindings, you don't have to worry about prototypal + * inheritance masking primitives. + * + * This example demonstrates the `controller as` syntax. + * + * + * + *
+ * Name: + * [ greet ]
+ * Contact: + *
    + *
  • + * + * + * [ clear + * | X ] + *
  • + *
  • [ add ]
  • + *
+ *
+ *
+ * + * function SettingsController1() { + * this.name = "John Smith"; + * this.contacts = [ + * {type: 'phone', value: '408 555 1212'}, + * {type: 'email', value: 'john.smith@example.org'} ]; + * } + * + * SettingsController1.prototype.greet = function() { + * alert(this.name); + * }; + * + * SettingsController1.prototype.addContact = function() { + * this.contacts.push({type: 'email', value: 'yourname@example.org'}); + * }; + * + * SettingsController1.prototype.removeContact = function(contactToRemove) { + * var index = this.contacts.indexOf(contactToRemove); + * this.contacts.splice(index, 1); + * }; + * + * SettingsController1.prototype.clearContact = function(contact) { + * contact.type = 'phone'; + * contact.value = ''; + * }; + * + * + * it('should check controller as', function() { + * var container = element(by.id('ctrl-as-exmpl')); + * expect(container.findElement(by.model('settings.name')) + * .getAttribute('value')).toBe('John Smith'); + * + * var firstRepeat = + * container.findElement(by.repeater('contact in settings.contacts').row(0)); + * var secondRepeat = + * container.findElement(by.repeater('contact in settings.contacts').row(1)); + * + * expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * .toBe('408 555 1212'); + * + * expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * .toBe('john.smith@example.org'); + * + * firstRepeat.findElement(by.linkText('clear')).click(); + * + * expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * .toBe(''); + * + * container.findElement(by.linkText('add')).click(); + * + * expect(container.findElement(by.repeater('contact in settings.contacts').row(2)) + * .findElement(by.model('contact.value')) + * .getAttribute('value')) + * .toBe('yourname@example.org'); + * }); + * + *
+ * + * This example demonstrates the "attach to `$scope`" style of controller. + * + * + * + *
+ * Name: + * [ greet ]
+ * Contact: + *
    + *
  • + * + * + * [ clear + * | X ] + *
  • + *
  • [ add ]
  • + *
+ *
+ *
+ * + * function SettingsController2($scope) { + * $scope.name = "John Smith"; + * $scope.contacts = [ + * {type:'phone', value:'408 555 1212'}, + * {type:'email', value:'john.smith@example.org'} ]; + * + * $scope.greet = function() { + * alert($scope.name); + * }; + * + * $scope.addContact = function() { + * $scope.contacts.push({type:'email', value:'yourname@example.org'}); + * }; + * + * $scope.removeContact = function(contactToRemove) { + * var index = $scope.contacts.indexOf(contactToRemove); + * $scope.contacts.splice(index, 1); + * }; + * + * $scope.clearContact = function(contact) { + * contact.type = 'phone'; + * contact.value = ''; + * }; + * } + * + * + * it('should check controller', function() { + * var container = element(by.id('ctrl-exmpl')); + * + * expect(container.findElement(by.model('name')) + * .getAttribute('value')).toBe('John Smith'); + * + * var firstRepeat = + * container.findElement(by.repeater('contact in contacts').row(0)); + * var secondRepeat = + * container.findElement(by.repeater('contact in contacts').row(1)); + * + * expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * .toBe('408 555 1212'); + * expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * .toBe('john.smith@example.org'); + * + * firstRepeat.findElement(by.linkText('clear')).click(); + * + * expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) + * .toBe(''); + * + * container.findElement(by.linkText('add')).click(); + * + * expect(container.findElement(by.repeater('contact in contacts').row(2)) + * .findElement(by.model('contact.value')) + * .getAttribute('value')) + * .toBe('yourname@example.org'); + * }); + * + *
+ + */ +var ngControllerDirective = [function() { + return { + scope: true, + controller: '@', + priority: 500 + }; +}]; + +/** + * @ngdoc directive + * @name ngCsp + * + * @element html + * @description + * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. + * + * This is necessary when developing things like Google Chrome Extensions. + * + * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). + * For us to be compatible, we just need to implement the "getterFn" in $parse without violating + * any of these restrictions. + * + * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` + * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will + * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will + * be raised. + * + * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically + * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). + * To make those directives work in CSP mode, include the `angular-csp.css` manually. + * + * In order to use this feature put the `ngCsp` directive on the root element of the application. + * + * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* + * + * @example + * This example shows how to apply the `ngCsp` directive to the `html` tag. + ```html + + + ... + ... + + ``` + */ + +// ngCsp is not implemented as a proper directive any more, because we need it be processed while we bootstrap +// the system (before $parse is instantiated), for this reason we just have a csp() fn that looks for ng-csp attribute +// anywhere in the current doc + +/** + * @ngdoc directive + * @name ngClick + * + * @description + * The ngClick directive allows you to specify custom behavior when + * an element is clicked. + * + * @element ANY + * @priority 0 + * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon + * click. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + it('should check ng-click', function() { + expect(element(by.binding('count')).getText()).toMatch('0'); + element(by.css('button')).click(); + expect(element(by.binding('count')).getText()).toMatch('1'); + }); + + + */ +/* + * A directive that allows creation of custom onclick handlers that are defined as angular + * expressions and are compiled and executed within the current scope. + * + * Events that are handled via these handler are always configured not to propagate further. + */ +var ngEventDirectives = {}; +forEach( + 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), + function(name) { + var directiveName = directiveNormalize('ng-' + name); + ngEventDirectives[directiveName] = ['$parse', function($parse) { + return { + compile: function($element, attr) { + var fn = $parse(attr[directiveName]); + return function(scope, element, attr) { + element.on(lowercase(name), function(event) { + scope.$apply(function() { + fn(scope, {$event:event}); + }); + }); + }; + } + }; + }]; + } +); + +/** + * @ngdoc directive + * @name ngDblclick + * + * @description + * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event. + * + * @element ANY + * @priority 0 + * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon + * a dblclick. (The Event object is available as `$event`) + * + * @example + + + + count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngMousedown + * + * @description + * The ngMousedown directive allows you to specify custom behavior on mousedown event. + * + * @element ANY + * @priority 0 + * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon + * mousedown. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngMouseup + * + * @description + * Specify custom behavior on mouseup event. + * + * @element ANY + * @priority 0 + * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon + * mouseup. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + */ + +/** + * @ngdoc directive + * @name ngMouseover + * + * @description + * Specify custom behavior on mouseover event. + * + * @element ANY + * @priority 0 + * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon + * mouseover. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngMouseenter + * + * @description + * Specify custom behavior on mouseenter event. + * + * @element ANY + * @priority 0 + * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon + * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngMouseleave + * + * @description + * Specify custom behavior on mouseleave event. + * + * @element ANY + * @priority 0 + * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon + * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngMousemove + * + * @description + * Specify custom behavior on mousemove event. + * + * @element ANY + * @priority 0 + * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon + * mousemove. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngKeydown + * + * @description + * Specify custom behavior on keydown event. + * + * @element ANY + * @priority 0 + * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon + * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * + * @example + + + + key down count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngKeyup + * + * @description + * Specify custom behavior on keyup event. + * + * @element ANY + * @priority 0 + * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon + * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * + * @example + + +

Typing in the input box below updates the key count

+ key up count: {{count}} + +

Typing in the input box below updates the keycode

+ +

event keyCode: {{ event.keyCode }}

+

event altKey: {{ event.altKey }}

+
+
+ */ + + +/** + * @ngdoc directive + * @name ngKeypress + * + * @description + * Specify custom behavior on keypress event. + * + * @element ANY + * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon + * keypress. ({@link guide/expression#-event- Event object is available as `$event`} + * and can be interrogated for keyCode, altKey, etc.) + * + * @example + + + + key press count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngSubmit + * + * @description + * Enables binding angular expressions to onsubmit events. + * + * Additionally it prevents the default action (which for form means sending the request to the + * server and reloading the current page), but only if the form does not contain `action`, + * `data-action`, or `x-action` attributes. + * + * @element form + * @priority 0 + * @param {expression} ngSubmit {@link guide/expression Expression} to eval. + * ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + +
+ Enter text and hit enter: + + +
list={{list}}
+
+
+ + it('should check ng-submit', function() { + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + expect(element(by.input('text')).getAttribute('value')).toBe(''); + }); + it('should ignore empty strings', function() { + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + }); + +
+ */ + +/** + * @ngdoc directive + * @name ngFocus + * + * @description + * Specify custom behavior on focus event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon + * focus. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + * See {@link ng.directive:ngClick ngClick} + */ + +/** + * @ngdoc directive + * @name ngBlur + * + * @description + * Specify custom behavior on blur event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon + * blur. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + * See {@link ng.directive:ngClick ngClick} + */ + +/** + * @ngdoc directive + * @name ngCopy + * + * @description + * Specify custom behavior on copy event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon + * copy. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + copied: {{copied}} + + + */ + +/** + * @ngdoc directive + * @name ngCut + * + * @description + * Specify custom behavior on cut event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon + * cut. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + cut: {{cut}} + + + */ + +/** + * @ngdoc directive + * @name ngPaste + * + * @description + * Specify custom behavior on paste event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon + * paste. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + pasted: {{paste}} + + + */ + +/** + * @ngdoc directive + * @name ngIf + * @restrict A + * + * @description + * The `ngIf` directive removes or recreates a portion of the DOM tree based on an + * {expression}. If the expression assigned to `ngIf` evaluates to a false + * value then the element is removed from the DOM, otherwise a clone of the + * element is reinserted into the DOM. + * + * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the + * element in the DOM rather than changing its visibility via the `display` css property. A common + * case when this difference is significant is when using css selectors that rely on an element's + * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes. + * + * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope + * is created when the element is restored. The scope created within `ngIf` inherits from + * its parent scope using + * [prototypal inheritance](https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance). + * An important implication of this is if `ngModel` is used within `ngIf` to bind to + * a javascript primitive defined in the parent scope. In this case any modifications made to the + * variable within the child scope will override (hide) the value in the parent scope. + * + * Also, `ngIf` recreates elements using their compiled state. An example of this behavior + * is if an element's class attribute is directly modified after it's compiled, using something like + * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element + * the added class will be lost because the original compiled state is used to regenerate the element. + * + * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter` + * and `leave` effects. + * + * @animations + * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container + * leave - happens just before the ngIf contents are removed from the DOM + * + * @element ANY + * @scope + * @priority 600 + * @param {expression} ngIf If the {@link guide/expression expression} is falsy then + * the element is removed from the DOM tree. If it is truthy a copy of the compiled + * element is added to the DOM tree. + * + * @example + + + Click me:
+ Show when checked: + + I'm removed when the checkbox is unchecked. + +
+ + .animate-if { + background:white; + border:1px solid black; + padding:10px; + } + + .animate-if.ng-enter, .animate-if.ng-leave { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + } + + .animate-if.ng-enter, + .animate-if.ng-leave.ng-leave-active { + opacity:0; + } + + .animate-if.ng-leave, + .animate-if.ng-enter.ng-enter-active { + opacity:1; + } + +
+ */ +var ngIfDirective = ['$animate', function($animate) { + return { + transclude: 'element', + priority: 600, + terminal: true, + restrict: 'A', + $$tlb: true, + link: function ($scope, $element, $attr, ctrl, $transclude) { + var block, childScope, previousElements; + $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { + + if (toBoolean(value)) { + if (!childScope) { + childScope = $scope.$new(); + $transclude(childScope, function (clone) { + clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); + // Note: We only need the first/last node of the cloned nodes. + // However, we need to keep the reference to the jqlite wrapper as it might be changed later + // by a directive with templateUrl when its template arrives. + block = { + clone: clone + }; + $animate.enter(clone, $element.parent(), $element); + }); + } + } else { + if(previousElements) { + previousElements.remove(); + previousElements = null; + } + if(childScope) { + childScope.$destroy(); + childScope = null; + } + if(block) { + previousElements = getBlockElements(block.clone); + $animate.leave(previousElements, function() { + previousElements = null; + }); + block = null; + } + } + }); + } + }; +}]; + +/** + * @ngdoc directive + * @name ngInclude + * @restrict ECA + * + * @description + * Fetches, compiles and includes an external HTML fragment. + * + * By default, the template URL is restricted to the same domain and protocol as the + * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols + * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or + * [wrap them](ng.$sce#trustAsResourceUrl) as trusted values. Refer to Angular's {@link + * ng.$sce Strict Contextual Escaping}. + * + * In addition, the browser's + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) + * policy may further restrict whether the template is successfully loaded. + * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://` + * access on some browsers. + * + * @animations + * enter - animation is used to bring new content into the browser. + * leave - animation is used to animate existing content away. + * + * The enter and leave animation occur concurrently. + * + * @scope + * @priority 400 + * + * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant, + * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`. + * @param {string=} onload Expression to evaluate when a new partial is loaded. + * + * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll + * $anchorScroll} to scroll the viewport after the content is loaded. + * + * - If the attribute is not set, disable scrolling. + * - If the attribute is set without value, enable scrolling. + * - Otherwise enable scrolling only if the expression evaluates to truthy value. + * + * @example + + +
+ + url of the template: {{template.url}} +
+
+
+
+
+
+ + function Ctrl($scope) { + $scope.templates = + [ { name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'} ]; + $scope.template = $scope.templates[0]; + } + + + Content of template1.html + + + Content of template2.html + + + .slide-animate-container { + position:relative; + background:white; + border:1px solid black; + height:40px; + overflow:hidden; + } + + .slide-animate { + padding:10px; + } + + .slide-animate.ng-enter, .slide-animate.ng-leave { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + display:block; + padding:10px; + } + + .slide-animate.ng-enter { + top:-50px; + } + .slide-animate.ng-enter.ng-enter-active { + top:0; + } + + .slide-animate.ng-leave { + top:0; + } + .slide-animate.ng-leave.ng-leave-active { + top:50px; + } + + + var templateSelect = element(by.model('template')); + var includeElem = element(by.css('[ng-include]')); + + it('should load template1.html', function() { + expect(includeElem.getText()).toMatch(/Content of template1.html/); + }); + + it('should load template2.html', function() { + if (browser.params.browser == 'firefox') { + // Firefox can't handle using selects + // See https://github.com/angular/protractor/issues/480 + return; + } + templateSelect.click(); + templateSelect.element.all(by.css('option')).get(2).click(); + expect(includeElem.getText()).toMatch(/Content of template2.html/); + }); + + it('should change to blank', function() { + if (browser.params.browser == 'firefox') { + // Firefox can't handle using selects + return; + } + templateSelect.click(); + templateSelect.element.all(by.css('option')).get(0).click(); + expect(includeElem.isPresent()).toBe(false); + }); + +
+ */ + + +/** + * @ngdoc event + * @name ngInclude#$includeContentRequested + * @eventType emit on the scope ngInclude was declared in + * @description + * Emitted every time the ngInclude content is requested. + */ + + +/** + * @ngdoc event + * @name ngInclude#$includeContentLoaded + * @eventType emit on the current ngInclude scope + * @description + * Emitted every time the ngInclude content is reloaded. + */ +var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', + function($http, $templateCache, $anchorScroll, $animate, $sce) { + return { + restrict: 'ECA', + priority: 400, + terminal: true, + transclude: 'element', + controller: angular.noop, + compile: function(element, attr) { + var srcExp = attr.ngInclude || attr.src, + onloadExp = attr.onload || '', + autoScrollExp = attr.autoscroll; + + return function(scope, $element, $attr, ctrl, $transclude) { + var changeCounter = 0, + currentScope, + previousElement, + currentElement; + + var cleanupLastIncludeContent = function() { + if(previousElement) { + previousElement.remove(); + previousElement = null; + } + if(currentScope) { + currentScope.$destroy(); + currentScope = null; + } + if(currentElement) { + $animate.leave(currentElement, function() { + previousElement = null; + }); + previousElement = currentElement; + currentElement = null; + } + }; + + scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) { + var afterAnimation = function() { + if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); + } + }; + var thisChangeId = ++changeCounter; + + if (src) { + $http.get(src, {cache: $templateCache}).success(function(response) { + if (thisChangeId !== changeCounter) return; + var newScope = scope.$new(); + ctrl.template = response; + + // Note: This will also link all children of ng-include that were contained in the original + // html. If that content contains controllers, ... they could pollute/change the scope. + // However, using ng-include on an element with additional content does not make sense... + // Note: We can't remove them in the cloneAttchFn of $transclude as that + // function is called before linking the content, which would apply child + // directives to non existing elements. + var clone = $transclude(newScope, function(clone) { + cleanupLastIncludeContent(); + $animate.enter(clone, null, $element, afterAnimation); + }); + + currentScope = newScope; + currentElement = clone; + + currentScope.$emit('$includeContentLoaded'); + scope.$eval(onloadExp); + }).error(function() { + if (thisChangeId === changeCounter) cleanupLastIncludeContent(); + }); + scope.$emit('$includeContentRequested'); + } else { + cleanupLastIncludeContent(); + ctrl.template = null; + } + }); + }; + } + }; +}]; + +// This directive is called during the $transclude call of the first `ngInclude` directive. +// It will replace and compile the content of the element with the loaded template. +// We need this directive so that the element content is already filled when +// the link function of another directive on the same element as ngInclude +// is called. +var ngIncludeFillContentDirective = ['$compile', + function($compile) { + return { + restrict: 'ECA', + priority: -400, + require: 'ngInclude', + link: function(scope, $element, $attr, ctrl) { + $element.html(ctrl.template); + $compile($element.contents())(scope); + } + }; + }]; + +/** + * @ngdoc directive + * @name ngInit + * @restrict AC + * + * @description + * The `ngInit` directive allows you to evaluate an expression in the + * current scope. + * + *
+ * The only appropriate use of `ngInit` is for aliasing special properties of + * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you + * should use {@link guide/controller controllers} rather than `ngInit` + * to initialize values on a scope. + *
+ *
+ * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make + * sure you have parenthesis for correct precedence: + *
+ *   
+ *
+ *
+ * + * @priority 450 + * + * @element ANY + * @param {expression} ngInit {@link guide/expression Expression} to eval. + * + * @example + + + +
+
+
+ list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}}; +
+
+
+
+ + it('should alias index positions', function() { + var elements = element.all(by.css('.example-init')); + expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;'); + expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;'); + expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;'); + expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;'); + }); + +
+ */ +var ngInitDirective = ngDirective({ + priority: 450, + compile: function() { + return { + pre: function(scope, element, attrs) { + scope.$eval(attrs.ngInit); + } + }; + } +}); + +/** + * @ngdoc directive + * @name ngNonBindable + * @restrict AC + * @priority 1000 + * + * @description + * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current + * DOM element. This is useful if the element contains what appears to be Angular directives and + * bindings but which should be ignored by Angular. This could be the case if you have a site that + * displays snippets of code, for instance. + * + * @element ANY + * + * @example + * In this example there are two locations where a simple interpolation binding (`{{}}`) is present, + * but the one wrapped in `ngNonBindable` is left alone. + * + * @example + + +
Normal: {{1 + 2}}
+
Ignored: {{1 + 2}}
+
+ + it('should check ng-non-bindable', function() { + expect(element(by.binding('1 + 2')).getText()).toContain('3'); + expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/); + }); + +
+ */ +var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); + +/** + * @ngdoc directive + * @name ngPluralize + * @restrict EA + * + * @description + * `ngPluralize` is a directive that displays messages according to en-US localization rules. + * These rules are bundled with angular.js, but can be overridden + * (see {@link guide/i18n Angular i18n} dev guide). You configure ngPluralize directive + * by specifying the mappings between + * [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) + * and the strings to be displayed. + * + * # Plural categories and explicit number rules + * There are two + * [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) + * in Angular's default en-US locale: "one" and "other". + * + * While a plural category may match many numbers (for example, in en-US locale, "other" can match + * any number that is not 1), an explicit number rule can only match one number. For example, the + * explicit number rule for "3" matches the number 3. There are examples of plural categories + * and explicit number rules throughout the rest of this documentation. + * + * # Configuring ngPluralize + * You configure ngPluralize by providing 2 attributes: `count` and `when`. + * You can also provide an optional attribute, `offset`. + * + * The value of the `count` attribute can be either a string or an {@link guide/expression + * Angular expression}; these are evaluated on the current scope for its bound value. + * + * The `when` attribute specifies the mappings between plural categories and the actual + * string to be displayed. The value of the attribute should be a JSON object. + * + * The following example shows how to configure ngPluralize: + * + * ```html + * + * + *``` + * + * In the example, `"0: Nobody is viewing."` is an explicit number rule. If you did not + * specify this rule, 0 would be matched to the "other" category and "0 people are viewing" + * would be shown instead of "Nobody is viewing". You can specify an explicit number rule for + * other numbers, for example 12, so that instead of showing "12 people are viewing", you can + * show "a dozen people are viewing". + * + * You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted + * into pluralized strings. In the previous example, Angular will replace `{}` with + * `{{personCount}}`. The closed braces `{}` is a placeholder + * for {{numberExpression}}. + * + * # Configuring ngPluralize with offset + * The `offset` attribute allows further customization of pluralized text, which can result in + * a better user experience. For example, instead of the message "4 people are viewing this document", + * you might display "John, Kate and 2 others are viewing this document". + * The offset attribute allows you to offset a number by any desired value. + * Let's take a look at an example: + * + * ```html + * + * + * ``` + * + * Notice that we are still using two plural categories(one, other), but we added + * three explicit number rules 0, 1 and 2. + * When one person, perhaps John, views the document, "John is viewing" will be shown. + * When three people view the document, no explicit number rule is found, so + * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. + * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing" + * is shown. + * + * Note that when you specify offsets, you must provide explicit number rules for + * numbers from 0 up to and including the offset. If you use an offset of 3, for example, + * you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for + * plural categories "one" and "other". + * + * @param {string|expression} count The variable to be bound to. + * @param {string} when The mapping between plural category to its corresponding strings. + * @param {number=} offset Offset to deduct from the total number. + * + * @example + + + +
+ Person 1:
+ Person 2:
+ Number of People:
+ + + Without Offset: + +
+ + + With Offset(2): + + +
+
+ + it('should show correct pluralized string', function() { + var withoutOffset = element.all(by.css('ng-pluralize')).get(0); + var withOffset = element.all(by.css('ng-pluralize')).get(1); + var countInput = element(by.model('personCount')); + + expect(withoutOffset.getText()).toEqual('1 person is viewing.'); + expect(withOffset.getText()).toEqual('Igor is viewing.'); + + countInput.clear(); + countInput.sendKeys('0'); + + expect(withoutOffset.getText()).toEqual('Nobody is viewing.'); + expect(withOffset.getText()).toEqual('Nobody is viewing.'); + + countInput.clear(); + countInput.sendKeys('2'); + + expect(withoutOffset.getText()).toEqual('2 people are viewing.'); + expect(withOffset.getText()).toEqual('Igor and Misko are viewing.'); + + countInput.clear(); + countInput.sendKeys('3'); + + expect(withoutOffset.getText()).toEqual('3 people are viewing.'); + expect(withOffset.getText()).toEqual('Igor, Misko and one other person are viewing.'); + + countInput.clear(); + countInput.sendKeys('4'); + + expect(withoutOffset.getText()).toEqual('4 people are viewing.'); + expect(withOffset.getText()).toEqual('Igor, Misko and 2 other people are viewing.'); + }); + it('should show data-bound names', function() { + var withOffset = element.all(by.css('ng-pluralize')).get(1); + var personCount = element(by.model('personCount')); + var person1 = element(by.model('person1')); + var person2 = element(by.model('person2')); + personCount.clear(); + personCount.sendKeys('4'); + person1.clear(); + person1.sendKeys('Di'); + person2.clear(); + person2.sendKeys('Vojta'); + expect(withOffset.getText()).toEqual('Di, Vojta and 2 other people are viewing.'); + }); + +
+ */ +var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) { + var BRACE = /{}/g; + return { + restrict: 'EA', + link: function(scope, element, attr) { + var numberExp = attr.count, + whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs + offset = attr.offset || 0, + whens = scope.$eval(whenExp) || {}, + whensExpFns = {}, + startSymbol = $interpolate.startSymbol(), + endSymbol = $interpolate.endSymbol(), + isWhen = /^when(Minus)?(.+)$/; + + forEach(attr, function(expression, attributeName) { + if (isWhen.test(attributeName)) { + whens[lowercase(attributeName.replace('when', '').replace('Minus', '-'))] = + element.attr(attr.$attr[attributeName]); + } + }); + forEach(whens, function(expression, key) { + whensExpFns[key] = + $interpolate(expression.replace(BRACE, startSymbol + numberExp + '-' + + offset + endSymbol)); + }); + + scope.$watch(function ngPluralizeWatch() { + var value = parseFloat(scope.$eval(numberExp)); + + if (!isNaN(value)) { + //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, + //check it against pluralization rules in $locale service + if (!(value in whens)) value = $locale.pluralCat(value - offset); + return whensExpFns[value](scope, element, true); + } else { + return ''; + } + }, function ngPluralizeWatchAction(newVal) { + element.text(newVal); + }); + } + }; +}]; + +/** + * @ngdoc directive + * @name ngRepeat + * + * @description + * The `ngRepeat` directive instantiates a template once per item from a collection. Each template + * instance gets its own scope, where the given loop variable is set to the current collection item, + * and `$index` is set to the item index or key. + * + * Special properties are exposed on the local scope of each template instance, including: + * + * | Variable | Type | Details | + * |-----------|-----------------|-----------------------------------------------------------------------------| + * | `$index` | {@type number} | iterator offset of the repeated element (0..length-1) | + * | `$first` | {@type boolean} | true if the repeated element is first in the iterator. | + * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. | + * | `$last` | {@type boolean} | true if the repeated element is last in the iterator. | + * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). | + * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). | + * + * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}. + * This may be useful when, for instance, nesting ngRepeats. + * + * # Special repeat start and end points + * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending + * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively. + * The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on) + * up to and including the ending HTML tag where **ng-repeat-end** is placed. + * + * The example below makes use of this feature: + * ```html + *
+ * Header {{ item }} + *
+ *
+ * Body {{ item }} + *
+ *
+ * Footer {{ item }} + *
+ * ``` + * + * And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to: + * ```html + *
+ * Header A + *
+ *
+ * Body A + *
+ *
+ * Footer A + *
+ *
+ * Header B + *
+ *
+ * Body B + *
+ *
+ * Footer B + *
+ * ``` + * + * The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such + * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**). + * + * @animations + * **.enter** - when a new item is added to the list or when an item is revealed after a filter + * + * **.leave** - when an item is removed from the list or when an item is filtered out + * + * **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered + * + * @element ANY + * @scope + * @priority 1000 + * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These + * formats are currently supported: + * + * * `variable in expression` – where variable is the user defined loop variable and `expression` + * is a scope expression giving the collection to enumerate. + * + * For example: `album in artist.albums`. + * + * * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers, + * and `expression` is the scope expression giving the collection to enumerate. + * + * For example: `(name, age) in {'adam':10, 'amalie':12}`. + * + * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function + * which can be used to associate the objects in the collection with the DOM elements. If no tracking function + * is specified the ng-repeat associates elements by identity in the collection. It is an error to have + * more than one tracking function to resolve to the same key. (This would mean that two distinct objects are + * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression, + * before specifying a tracking expression. + * + * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements + * will be associated by item identity in the array. + * + * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique + * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements + * with the corresponding item in the array by identity. Moving the same object in array would move the DOM + * element in the same way in the DOM. + * + * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this + * case the object identity does not matter. Two objects are considered equivalent as long as their `id` + * property is same. + * + * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter + * to items in conjunction with a tracking expression. + * + * @example + * This example initializes the scope to a list of names and + * then uses `ngRepeat` to display every person: + + +
+ I have {{friends.length}} friends. They are: + +
    +
  • + [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. +
  • +
+
+
+ + .example-animate-container { + background:white; + border:1px solid black; + list-style:none; + margin:0; + padding:0 10px; + } + + .animate-repeat { + line-height:40px; + list-style:none; + box-sizing:border-box; + } + + .animate-repeat.ng-move, + .animate-repeat.ng-enter, + .animate-repeat.ng-leave { + -webkit-transition:all linear 0.5s; + transition:all linear 0.5s; + } + + .animate-repeat.ng-leave.ng-leave-active, + .animate-repeat.ng-move, + .animate-repeat.ng-enter { + opacity:0; + max-height:0; + } + + .animate-repeat.ng-leave, + .animate-repeat.ng-move.ng-move-active, + .animate-repeat.ng-enter.ng-enter-active { + opacity:1; + max-height:40px; + } + + + var friends = element.all(by.repeater('friend in friends')); + + it('should render initial data set', function() { + expect(friends.count()).toBe(10); + expect(friends.get(0).getText()).toEqual('[1] John who is 25 years old.'); + expect(friends.get(1).getText()).toEqual('[2] Jessie who is 30 years old.'); + expect(friends.last().getText()).toEqual('[10] Samantha who is 60 years old.'); + expect(element(by.binding('friends.length')).getText()) + .toMatch("I have 10 friends. They are:"); + }); + + it('should update repeater when filter predicate changes', function() { + expect(friends.count()).toBe(10); + + element(by.model('q')).sendKeys('ma'); + + expect(friends.count()).toBe(2); + expect(friends.get(0).getText()).toEqual('[1] Mary who is 28 years old.'); + expect(friends.last().getText()).toEqual('[2] Samantha who is 60 years old.'); + }); + +
+ */ +var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { + var NG_REMOVED = '$$NG_REMOVED'; + var ngRepeatMinErr = minErr('ngRepeat'); + return { + transclude: 'element', + priority: 1000, + terminal: true, + $$tlb: true, + link: function($scope, $element, $attr, ctrl, $transclude){ + var expression = $attr.ngRepeat; + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), + trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, + lhs, rhs, valueIdentifier, keyIdentifier, + hashFnLocals = {$id: hashKey}; + + if (!match) { + throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + expression); + } + + lhs = match[1]; + rhs = match[2]; + trackByExp = match[3]; + + if (trackByExp) { + trackByExpGetter = $parse(trackByExp); + trackByIdExpFn = function(key, value, index) { + // assign key, value, and $index to the locals so that they can be used in hash functions + if (keyIdentifier) hashFnLocals[keyIdentifier] = key; + hashFnLocals[valueIdentifier] = value; + hashFnLocals.$index = index; + return trackByExpGetter($scope, hashFnLocals); + }; + } else { + trackByIdArrayFn = function(key, value) { + return hashKey(value); + }; + trackByIdObjFn = function(key) { + return key; + }; + } + + match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); + if (!match) { + throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", + lhs); + } + valueIdentifier = match[3] || match[1]; + keyIdentifier = match[2]; + + // Store a list of elements from previous run. This is a hash where key is the item from the + // iterator, and the value is objects with following properties. + // - scope: bound scope + // - element: previous element. + // - index: position + var lastBlockMap = {}; + + //watch props + $scope.$watchCollection(rhs, function ngRepeatAction(collection){ + var index, length, + previousNode = $element[0], // current position of the node + nextNode, + // Same as lastBlockMap but it has the current state. It will become the + // lastBlockMap on the next iteration. + nextBlockMap = {}, + arrayLength, + childScope, + key, value, // key/value of iteration + trackById, + trackByIdFn, + collectionKeys, + block, // last object information {scope, element, id} + nextBlockOrder = [], + elementsToRemove; + + + if (isArrayLike(collection)) { + collectionKeys = collection; + trackByIdFn = trackByIdExpFn || trackByIdArrayFn; + } else { + trackByIdFn = trackByIdExpFn || trackByIdObjFn; + // if object, extract keys, sort them and use to determine order of iteration over obj props + collectionKeys = []; + for (key in collection) { + if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { + collectionKeys.push(key); + } + } + collectionKeys.sort(); + } + + arrayLength = collectionKeys.length; + + // locate existing items + length = nextBlockOrder.length = collectionKeys.length; + for(index = 0; index < length; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + trackById = trackByIdFn(key, value, index); + assertNotHasOwnProperty(trackById, '`track by` id'); + if(lastBlockMap.hasOwnProperty(trackById)) { + block = lastBlockMap[trackById]; + delete lastBlockMap[trackById]; + nextBlockMap[trackById] = block; + nextBlockOrder[index] = block; + } else if (nextBlockMap.hasOwnProperty(trackById)) { + // restore lastBlockMap + forEach(nextBlockOrder, function(block) { + if (block && block.scope) lastBlockMap[block.id] = block; + }); + // This is a duplicate and we need to throw an error + throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", + expression, trackById); + } else { + // new never before seen block + nextBlockOrder[index] = { id: trackById }; + nextBlockMap[trackById] = false; + } + } + + // remove existing items + for (key in lastBlockMap) { + // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn + if (lastBlockMap.hasOwnProperty(key)) { + block = lastBlockMap[key]; + elementsToRemove = getBlockElements(block.clone); + $animate.leave(elementsToRemove); + forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); + block.scope.$destroy(); + } + } + + // we are not using forEach for perf reasons (trying to avoid #call) + for (index = 0, length = collectionKeys.length; index < length; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + block = nextBlockOrder[index]; + if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]); + + if (block.scope) { + // if we have already seen this object, then we need to reuse the + // associated scope/element + childScope = block.scope; + + nextNode = previousNode; + do { + nextNode = nextNode.nextSibling; + } while(nextNode && nextNode[NG_REMOVED]); + + if (getBlockStart(block) != nextNode) { + // existing item which got moved + $animate.move(getBlockElements(block.clone), null, jqLite(previousNode)); + } + previousNode = getBlockEnd(block); + } else { + // new item which we don't know about + childScope = $scope.$new(); + } + + childScope[valueIdentifier] = value; + if (keyIdentifier) childScope[keyIdentifier] = key; + childScope.$index = index; + childScope.$first = (index === 0); + childScope.$last = (index === (arrayLength - 1)); + childScope.$middle = !(childScope.$first || childScope.$last); + // jshint bitwise: false + childScope.$odd = !(childScope.$even = (index&1) === 0); + // jshint bitwise: true + + if (!block.scope) { + $transclude(childScope, function(clone) { + clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); + $animate.enter(clone, null, jqLite(previousNode)); + previousNode = clone; + block.scope = childScope; + // Note: We only need the first/last node of the cloned nodes. + // However, we need to keep the reference to the jqlite wrapper as it might be changed later + // by a directive with templateUrl when its template arrives. + block.clone = clone; + nextBlockMap[block.id] = block; + }); + } + } + lastBlockMap = nextBlockMap; + }); + } + }; + + function getBlockStart(block) { + return block.clone[0]; + } + + function getBlockEnd(block) { + return block.clone[block.clone.length - 1]; + } +}]; + +/** + * @ngdoc directive + * @name ngShow + * + * @description + * The `ngShow` directive shows or hides the given HTML element based on the expression + * provided to the ngShow attribute. The element is shown or hidden by removing or adding + * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * in AngularJS and sets the display style to none (using an !important flag). + * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). + * + * ```html + * + *
+ * + * + *
+ * ``` + * + * When the ngShow expression evaluates to false then the ng-hide CSS class is added to the class attribute + * on the element causing it to become hidden. When true, the ng-hide CSS class is removed + * from the element causing the element not to appear hidden. + * + *
+ * **Note:** Here is a list of values that ngShow will consider as a falsy value (case insensitive):
+ * "f" / "0" / "false" / "no" / "n" / "[]" + *
+ * + * ## Why is !important used? + * + * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector + * can be easily overridden by heavier selectors. For example, something as simple + * as changing the display style on a HTML list item would make hidden elements appear visible. + * This also becomes a bigger issue when dealing with CSS frameworks. + * + * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector + * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the + * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. + * + * ### Overriding .ng-hide + * + * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class in CSS: + * + * ```css + * .ng-hide { + * //this is just another form of hiding an element + * display:block!important; + * position:absolute; + * top:-9999px; + * left:-9999px; + * } + * ``` + * + * By default you don't need to override in CSS anything and the animations will work around the display style. + * + * ## A note about animations with ngShow + * + * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression + * is true and false. This system works like the animation system present with ngClass except that + * you must also include the !important flag to override the display property + * so that you can perform an animation when the element is hidden during the time of the animation. + * + * ```css + * // + * //a working example can be found at the bottom of this page + * // + * .my-element.ng-hide-add, .my-element.ng-hide-remove { + * transition:0.5s linear all; + * } + * + * .my-element.ng-hide-add { ... } + * .my-element.ng-hide-add.ng-hide-add-active { ... } + * .my-element.ng-hide-remove { ... } + * .my-element.ng-hide-remove.ng-hide-remove-active { ... } + * ``` + * + * Keep in mind that, as of AngularJS version 1.2.17 (and 1.3.0-beta.11), there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * + * @animations + * addClass: .ng-hide - happens after the ngShow expression evaluates to a truthy value and the just before contents are set to visible + * removeClass: .ng-hide - happens after the ngShow expression evaluates to a non truthy value and just before the contents are set to hidden + * + * @element ANY + * @param {expression} ngShow If the {@link guide/expression expression} is truthy + * then the element is shown or hidden respectively. + * + * @example + + + Click me:
+
+ Show: +
+ I show up when your checkbox is checked. +
+
+
+ Hide: +
+ I hide when your checkbox is checked. +
+
+
+ + @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); + + + .animate-show { + -webkit-transition:all linear 0.5s; + transition:all linear 0.5s; + line-height:20px; + opacity:1; + padding:10px; + border:1px solid black; + background:white; + } + + .animate-show.ng-hide { + line-height:0; + opacity:0; + padding:0 10px; + } + + .check-element { + padding:10px; + border:1px solid black; + background:white; + } + + + var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); + var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); + + it('should check ng-show / ng-hide', function() { + expect(thumbsUp.isDisplayed()).toBeFalsy(); + expect(thumbsDown.isDisplayed()).toBeTruthy(); + + element(by.model('checked')).click(); + + expect(thumbsUp.isDisplayed()).toBeTruthy(); + expect(thumbsDown.isDisplayed()).toBeFalsy(); + }); + +
+ */ +var ngShowDirective = ['$animate', function($animate) { + return function(scope, element, attr) { + scope.$watch(attr.ngShow, function ngShowWatchAction(value){ + $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide'); + }); + }; +}]; + + +/** + * @ngdoc directive + * @name ngHide + * + * @description + * The `ngHide` directive shows or hides the given HTML element based on the expression + * provided to the ngHide attribute. The element is shown or hidden by removing or adding + * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * in AngularJS and sets the display style to none (using an !important flag). + * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). + * + * ```html + * + *
+ * + * + *
+ * ``` + * + * When the ngHide expression evaluates to true then the .ng-hide CSS class is added to the class attribute + * on the element causing it to become hidden. When false, the ng-hide CSS class is removed + * from the element causing the element not to appear hidden. + * + *
+ * **Note:** Here is a list of values that ngHide will consider as a falsy value (case insensitive):
+ * "f" / "0" / "false" / "no" / "n" / "[]" + *
+ * + * ## Why is !important used? + * + * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector + * can be easily overridden by heavier selectors. For example, something as simple + * as changing the display style on a HTML list item would make hidden elements appear visible. + * This also becomes a bigger issue when dealing with CSS frameworks. + * + * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector + * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the + * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. + * + * ### Overriding .ng-hide + * + * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class in CSS: + * + * ```css + * .ng-hide { + * //this is just another form of hiding an element + * display:block!important; + * position:absolute; + * top:-9999px; + * left:-9999px; + * } + * ``` + * + * By default you don't need to override in CSS anything and the animations will work around the display style. + * + * ## A note about animations with ngHide + * + * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression + * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide` + * CSS class is added and removed for you instead of your own CSS class. + * + * ```css + * // + * //a working example can be found at the bottom of this page + * // + * .my-element.ng-hide-add, .my-element.ng-hide-remove { + * transition:0.5s linear all; + * } + * + * .my-element.ng-hide-add { ... } + * .my-element.ng-hide-add.ng-hide-add-active { ... } + * .my-element.ng-hide-remove { ... } + * .my-element.ng-hide-remove.ng-hide-remove-active { ... } + * ``` + * + * Keep in mind that, as of AngularJS version 1.2.17 (and 1.3.0-beta.11), there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * + * @animations + * removeClass: .ng-hide - happens after the ngHide expression evaluates to a truthy value and just before the contents are set to hidden + * addClass: .ng-hide - happens after the ngHide expression evaluates to a non truthy value and just before the contents are set to visible + * + * @element ANY + * @param {expression} ngHide If the {@link guide/expression expression} is truthy then + * the element is shown or hidden respectively. + * + * @example + + + Click me:
+
+ Show: +
+ I show up when your checkbox is checked. +
+
+
+ Hide: +
+ I hide when your checkbox is checked. +
+
+
+ + @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); + + + .animate-hide { + -webkit-transition:all linear 0.5s; + transition:all linear 0.5s; + line-height:20px; + opacity:1; + padding:10px; + border:1px solid black; + background:white; + } + + .animate-hide.ng-hide { + line-height:0; + opacity:0; + padding:0 10px; + } + + .check-element { + padding:10px; + border:1px solid black; + background:white; + } + + + var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); + var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); + + it('should check ng-show / ng-hide', function() { + expect(thumbsUp.isDisplayed()).toBeFalsy(); + expect(thumbsDown.isDisplayed()).toBeTruthy(); + + element(by.model('checked')).click(); + + expect(thumbsUp.isDisplayed()).toBeTruthy(); + expect(thumbsDown.isDisplayed()).toBeFalsy(); + }); + +
+ */ +var ngHideDirective = ['$animate', function($animate) { + return function(scope, element, attr) { + scope.$watch(attr.ngHide, function ngHideWatchAction(value){ + $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide'); + }); + }; +}]; + +/** + * @ngdoc directive + * @name ngStyle + * @restrict AC + * + * @description + * The `ngStyle` directive allows you to set CSS style on an HTML element conditionally. + * + * @element ANY + * @param {expression} ngStyle + * + * {@link guide/expression Expression} which evals to an + * object whose keys are CSS style names and values are corresponding values for those CSS + * keys. + * + * Since some CSS style names are not valid keys for an object, they must be quoted. + * See the 'background-color' style in the example below. + * + * @example + + + + + +
+ Sample Text +
myStyle={{myStyle}}
+
+ + span { + color: black; + } + + + var colorSpan = element(by.css('span')); + + iit('should check ng-style', function() { + expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); + element(by.css('input[value=\'set color\']')).click(); + expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)'); + element(by.css('input[value=clear]')).click(); + expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); + }); + +
+ */ +var ngStyleDirective = ngDirective(function(scope, element, attr) { + scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) { + if (oldStyles && (newStyles !== oldStyles)) { + forEach(oldStyles, function(val, style) { element.css(style, '');}); + } + if (newStyles) element.css(newStyles); + }, true); +}); + +/** + * @ngdoc directive + * @name ngSwitch + * @restrict EA + * + * @description + * The `ngSwitch` directive is used to conditionally swap DOM structure on your template based on a scope expression. + * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be preserved at the location + * as specified in the template. + * + * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it + * from the template cache), `ngSwitch` simply chooses one of the nested elements and makes it visible based on which element + * matches the value obtained from the evaluated expression. In other words, you define a container element + * (where you place the directive), place an expression on the **`on="..."` attribute** + * (or the **`ng-switch="..."` attribute**), define any inner elements inside of the directive and place + * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on + * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default + * attribute is displayed. + * + *
+ * Be aware that the attribute values to match against cannot be expressions. They are interpreted + * as literal string values to match against. + * For example, **`ng-switch-when="someVal"`** will match against the string `"someVal"` not against the + * value of the expression `$scope.someVal`. + *
+ + * @animations + * enter - happens after the ngSwitch contents change and the matched child element is placed inside the container + * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM + * + * @usage + * + * ``` + * + * ... + * ... + * ... + * + * ``` + * + * + * @scope + * @priority 800 + * @param {*} ngSwitch|on expression to match against ng-switch-when. + * On child elements add: + * + * * `ngSwitchWhen`: the case statement to match against. If match then this + * case will be displayed. If the same match appears multiple times, all the + * elements will be displayed. + * * `ngSwitchDefault`: the default case when no other case match. If there + * are multiple default cases, all of them will be displayed when no other + * case match. + * + * + * @example + + +
+ + selection={{selection}} +
+
+
Settings Div
+
Home Span
+
default
+
+
+
+ + function Ctrl($scope) { + $scope.items = ['settings', 'home', 'other']; + $scope.selection = $scope.items[0]; + } + + + .animate-switch-container { + position:relative; + background:white; + border:1px solid black; + height:40px; + overflow:hidden; + } + + .animate-switch { + padding:10px; + } + + .animate-switch.ng-animate { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + } + + .animate-switch.ng-leave.ng-leave-active, + .animate-switch.ng-enter { + top:-50px; + } + .animate-switch.ng-leave, + .animate-switch.ng-enter.ng-enter-active { + top:0; + } + + + var switchElem = element(by.css('[ng-switch]')); + var select = element(by.model('selection')); + + it('should start in settings', function() { + expect(switchElem.getText()).toMatch(/Settings Div/); + }); + it('should change to home', function() { + select.element.all(by.css('option')).get(1).click(); + expect(switchElem.getText()).toMatch(/Home Span/); + }); + it('should select default', function() { + select.element.all(by.css('option')).get(2).click(); + expect(switchElem.getText()).toMatch(/default/); + }); + +
+ */ +var ngSwitchDirective = ['$animate', function($animate) { + return { + restrict: 'EA', + require: 'ngSwitch', + + // asks for $scope to fool the BC controller module + controller: ['$scope', function ngSwitchController() { + this.cases = {}; + }], + link: function(scope, element, attr, ngSwitchController) { + var watchExpr = attr.ngSwitch || attr.on, + selectedTranscludes = [], + selectedElements = [], + previousElements = [], + selectedScopes = []; + + scope.$watch(watchExpr, function ngSwitchWatchAction(value) { + var i, ii; + for (i = 0, ii = previousElements.length; i < ii; ++i) { + previousElements[i].remove(); + } + previousElements.length = 0; + + for (i = 0, ii = selectedScopes.length; i < ii; ++i) { + var selected = selectedElements[i]; + selectedScopes[i].$destroy(); + previousElements[i] = selected; + $animate.leave(selected, function() { + previousElements.splice(i, 1); + }); + } + + selectedElements.length = 0; + selectedScopes.length = 0; + + if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { + scope.$eval(attr.change); + forEach(selectedTranscludes, function(selectedTransclude) { + var selectedScope = scope.$new(); + selectedScopes.push(selectedScope); + selectedTransclude.transclude(selectedScope, function(caseElement) { + var anchor = selectedTransclude.element; + + selectedElements.push(caseElement); + $animate.enter(caseElement, anchor.parent(), anchor); + }); + }); + } + }); + } + }; +}]; + +var ngSwitchWhenDirective = ngDirective({ + transclude: 'element', + priority: 800, + require: '^ngSwitch', + link: function(scope, element, attrs, ctrl, $transclude) { + ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []); + ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element }); + } +}); + +var ngSwitchDefaultDirective = ngDirective({ + transclude: 'element', + priority: 800, + require: '^ngSwitch', + link: function(scope, element, attr, ctrl, $transclude) { + ctrl.cases['?'] = (ctrl.cases['?'] || []); + ctrl.cases['?'].push({ transclude: $transclude, element: element }); + } +}); + +/** + * @ngdoc directive + * @name ngTransclude + * @restrict AC + * + * @description + * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. + * + * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted. + * + * @element ANY + * + * @example + + + +
+
+
+ {{text}} +
+
+ + it('should have transcluded', function() { + var titleElement = element(by.model('title')); + titleElement.clear(); + titleElement.sendKeys('TITLE'); + var textElement = element(by.model('text')); + textElement.clear(); + textElement.sendKeys('TEXT'); + expect(element(by.binding('title')).getText()).toEqual('TITLE'); + expect(element(by.binding('text')).getText()).toEqual('TEXT'); + }); + +
+ * + */ +var ngTranscludeDirective = ngDirective({ + link: function($scope, $element, $attrs, controller, $transclude) { + if (!$transclude) { + throw minErr('ngTransclude')('orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element: {0}', + startingTag($element)); + } + + $transclude(function(clone) { + $element.empty(); + $element.append(clone); + }); + } +}); + +/** + * @ngdoc directive + * @name script + * @restrict E + * + * @description + * Load the content of a ` + + Load inlined template +
+ + + it('should load template defined inside script tag', function() { + element(by.css('#tpl-link')).click(); + expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); + }); + + + */ +var scriptDirective = ['$templateCache', function($templateCache) { + return { + restrict: 'E', + terminal: true, + compile: function(element, attr) { + if (attr.type == 'text/ng-template') { + var templateUrl = attr.id, + // IE is not consistent, in scripts we have to read .text but in other nodes we have to read .textContent + text = element[0].text; + + $templateCache.put(templateUrl, text); + } + } + }; +}]; + +var ngOptionsMinErr = minErr('ngOptions'); +/** + * @ngdoc directive + * @name select + * @restrict E + * + * @description + * HTML `SELECT` element with angular data-binding. + * + * # `ngOptions` + * + * The `ngOptions` attribute can be used to dynamically generate a list of `` + * DOM element. + * * `trackexpr`: Used when working with an array of objects. The result of this expression will be + * used to identify the objects in the array. The `trackexpr` will most likely refer to the + * `value` variable (e.g. `value.propertyName`). + * + * @example + + + +
+
    +
  • + Name: + [X] +
  • +
  • + [add] +
  • +
+
+ Color (null not allowed): +
+ + Color (null allowed): + + +
+ + Color grouped by shade: +
+ + + Select bogus.
+
+ Currently selected: {{ {selected_color:myColor} }} +
+
+
+
+ + it('should check ng-options', function() { + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); + element.all(by.select('myColor')).first().click(); + element.all(by.css('select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); + element(by.css('.nullable select[ng-model="myColor"]')).click(); + element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); + }); + +
+ */ + +var ngOptionsDirective = valueFn({ terminal: true }); +// jshint maxlen: false +var selectDirective = ['$compile', '$parse', function($compile, $parse) { + //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 + var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/, + nullModelCtrl = {$setViewValue: noop}; +// jshint maxlen: 100 + + return { + restrict: 'E', + require: ['select', '?ngModel'], + controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { + var self = this, + optionsMap = {}, + ngModelCtrl = nullModelCtrl, + nullOption, + unknownOption; + + + self.databound = $attrs.ngModel; + + + self.init = function(ngModelCtrl_, nullOption_, unknownOption_) { + ngModelCtrl = ngModelCtrl_; + nullOption = nullOption_; + unknownOption = unknownOption_; + }; + + + self.addOption = function(value) { + assertNotHasOwnProperty(value, '"option value"'); + optionsMap[value] = true; + + if (ngModelCtrl.$viewValue == value) { + $element.val(value); + if (unknownOption.parent()) unknownOption.remove(); + } + }; + + + self.removeOption = function(value) { + if (this.hasOption(value)) { + delete optionsMap[value]; + if (ngModelCtrl.$viewValue == value) { + this.renderUnknownOption(value); + } + } + }; + + + self.renderUnknownOption = function(val) { + var unknownVal = '? ' + hashKey(val) + ' ?'; + unknownOption.val(unknownVal); + $element.prepend(unknownOption); + $element.val(unknownVal); + unknownOption.prop('selected', true); // needed for IE + }; + + + self.hasOption = function(value) { + return optionsMap.hasOwnProperty(value); + }; + + $scope.$on('$destroy', function() { + // disable unknown option so that we don't do work when the whole select is being destroyed + self.renderUnknownOption = noop; + }); + }], + + link: function(scope, element, attr, ctrls) { + // if ngModel is not defined, we don't need to do anything + if (!ctrls[1]) return; + + var selectCtrl = ctrls[0], + ngModelCtrl = ctrls[1], + multiple = attr.multiple, + optionsExp = attr.ngOptions, + nullOption = false, // if false, user will not be able to select it (used by ngOptions) + emptyOption, + // we can't just jqLite('