From 6a32634bf1bdb0cae9fbf4c2b74e134684c5b379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Fri, 15 Feb 2019 13:36:54 +0100 Subject: [PATCH 01/20] Added more tasks to package.json --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 549b1619e4..b93dd3d53f 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "lintBasicExample": "./node_modules/.bin/eslint extras/basic_example/basicServer.js extras/basic_example/public", "lintNuve": "./node_modules/.bin/eslint nuve/", "lintSpine": "./node_modules/.bin/eslint spine/", - "lint": "npm run lintErizoController && npm run lintBasicExample && npm run lintNuve && npm run lintSpine" + "lint": "npm run lintErizoController && npm run lintBasicExample && npm run lintNuve && npm run lintSpine", + "lintErizoAPI": "./erizo/utils/cpplint.py --filter=-legal/copyright,-build/include --linelength=120 ./erizoAPI/*.cc *.h", + "buildErizoAPI": "export ERIZO_HOME=$(pwd)/erizo/ && echo $ERIZO_HOME && cd ./erizoAPI/ && env JOBS=4 ./build.sh" } } From f63e664faecadef8a553e7687cbb0474caa6361f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Fri, 15 Feb 2019 13:37:31 +0100 Subject: [PATCH 02/20] Added an option to use boost promises --- erizo/src/CMakeLists.txt | 1 + erizoAPI/binding.gyp | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/erizo/src/CMakeLists.txt b/erizo/src/CMakeLists.txt index c3b3495ba2..75d7610385 100644 --- a/erizo/src/CMakeLists.txt +++ b/erizo/src/CMakeLists.txt @@ -67,6 +67,7 @@ include_directories(${GLIB_INCLUDE_DIRS}) # BOOST set (BOOST_LIBS thread regex system) find_package(Boost COMPONENTS ${BOOST_LIBS} REQUIRED) +set(ERIZO_CMAKE_CXX_FLAGS "${ERIZO_CMAKE_CXX_FLAGS} -DBOOST_THREAD_PROVIDES_FUTURE -DBOOST_THREAD_PROVIDES_FUTURE_CONTINUATION -DBOOST_THREAD_PROVIDES_FUTURE_WHEN_ALL_WHEN_ANY") # GTHREAD find_library(GTHREAD gthread-2.0 HINTS "${THIRD_PARTY_LIB}") diff --git a/erizoAPI/binding.gyp b/erizoAPI/binding.gyp index ec3c91d9ca..317f74c442 100644 --- a/erizoAPI/binding.gyp +++ b/erizoAPI/binding.gyp @@ -1,6 +1,6 @@ { 'variables' : { - 'common_sources': [ 'addon.cc', 'IOThreadPool.cc', 'ThreadPool.cc', 'MediaStream.cc', 'WebRtcConnection.cc', 'OneToManyProcessor.cc', 'ExternalInput.cc', 'ExternalOutput.cc', 'SyntheticInput.cc', 'ConnectionDescription.cc'], + 'common_sources': [ 'addon.cc', 'IOThreadPool.cc', 'AsyncPromiseWorker.cc', 'ThreadPool.cc', 'MediaStream.cc', 'WebRtcConnection.cc', 'OneToManyProcessor.cc', 'ExternalInput.cc', 'ExternalOutput.cc', 'SyntheticInput.cc', 'ConnectionDescription.cc'], 'common_include_dirs' : [" Date: Fri, 15 Feb 2019 13:42:47 +0100 Subject: [PATCH 03/20] Initial version with auto subscription API --- erizo/src/erizo/MediaStream.cpp | 14 +- erizo/src/erizo/MediaStream.h | 4 +- erizo/src/erizo/NicerConnection.cpp | 5 +- erizo/src/erizo/SdpInfo.cpp | 22 ++- erizo/src/erizo/SdpInfo.h | 6 + erizo/src/erizo/WebRtcConnection.cpp | 181 +++++++++++------- erizo/src/erizo/WebRtcConnection.h | 25 ++- erizo/src/erizo/rtp/FecReceiverHandler.cpp | 2 +- erizoAPI/AsyncPromiseWorker.cc | 16 ++ erizoAPI/AsyncPromiseWorker.h | 17 ++ erizoAPI/ConnectionDescription.cc | 10 + erizoAPI/ConnectionDescription.h | 2 + erizoAPI/MediaStream.cc | 3 +- erizoAPI/WebRtcConnection.cc | 130 +++++++++++-- erizoAPI/WebRtcConnection.h | 2 + .../erizoClient/src/ErizoConnectionManager.js | 4 +- erizo_controller/erizoClient/src/Room.js | 44 ++++- erizo_controller/erizoClient/src/Socket.js | 2 + .../src/webrtc-stacks/BaseStack.js | 22 ++- .../erizoController/models/Client.js | 158 ++++++++++++++- .../erizoController/models/Stream.js | 24 +++ .../erizoController/roomController.js | 126 +++++++++++- erizo_controller/erizoJS/erizoJSController.js | 90 ++++++++- erizo_controller/erizoJS/models/Connection.js | 77 +++++--- erizo_controller/erizoJS/models/Node.js | 3 +- erizo_controller/erizoJS/models/Publisher.js | 4 +- erizo_controller/erizoJS/models/Subscriber.js | 29 ++- extras/basic_example/basicServer.js | 3 + extras/basic_example/public/script.js | 9 + 29 files changed, 881 insertions(+), 153 deletions(-) create mode 100644 erizoAPI/AsyncPromiseWorker.cc create mode 100644 erizoAPI/AsyncPromiseWorker.h diff --git a/erizo/src/erizo/MediaStream.cpp b/erizo/src/erizo/MediaStream.cpp index 3c4cb17c5a..0d9878247e 100644 --- a/erizo/src/erizo/MediaStream.cpp +++ b/erizo/src/erizo/MediaStream.cpp @@ -91,6 +91,7 @@ MediaStream::MediaStream(std::shared_ptr worker, rate_control_ = 0; sending_ = true; + ready_ = false; } MediaStream::~MediaStream() { @@ -125,6 +126,7 @@ void MediaStream::syncClose() { return; } sending_ = false; + ready_ = false; video_sink_ = nullptr; audio_sink_ = nullptr; fb_sink_ = nullptr; @@ -142,7 +144,10 @@ void MediaStream::close() { }); } -bool MediaStream::init() { +bool MediaStream::init(bool force) { + if (force) { + ready_ = true; + } return true; } @@ -155,7 +160,7 @@ bool MediaStream::isSinkSSRC(uint32_t ssrc) { } bool MediaStream::setRemoteSdp(std::shared_ptr sdp) { - ELOG_DEBUG("%s message: setting remote SDP", toLog()); + ELOG_DEBUG("%s message: setting remote SDP to Stream, sending: %d, initialized: %d", toLog(), sending_, pipeline_initialized_); if (!sending_) { return true; } @@ -165,6 +170,8 @@ bool MediaStream::setRemoteSdp(std::shared_ptr sdp) { this->rtcp_processor_->setMaxVideoBW(remote_sdp_->videoBandwidth*1000); } + ready_ = true; + if (pipeline_initialized_ && pipeline_) { pipeline_->notifyUpdate(); return true; @@ -347,6 +354,9 @@ void MediaStream::printStats() { } void MediaStream::initializePipeline() { + if (pipeline_initialized_) { + return; + } handler_manager_ = std::make_shared(shared_from_this()); pipeline_->addService(shared_from_this()); pipeline_->addService(handler_manager_); diff --git a/erizo/src/erizo/MediaStream.h b/erizo/src/erizo/MediaStream.h index 634cecabef..5ad04f26b0 100644 --- a/erizo/src/erizo/MediaStream.h +++ b/erizo/src/erizo/MediaStream.h @@ -68,7 +68,7 @@ class MediaStream: public MediaSink, public MediaSource, public FeedbackSink, * Destructor. */ virtual ~MediaStream(); - bool init(); + bool init(bool force); void close() override; virtual uint32_t getMaxVideoBW(); virtual uint32_t getBitrateFromMaxQualityLayer() { return bitrate_from_max_quality_layer_; } @@ -155,6 +155,7 @@ class MediaStream: public MediaSink, public MediaSource, public FeedbackSink, bool isPipelineInitialized() { return pipeline_initialized_; } bool isRunning() { return pipeline_initialized_ && sending_; } + bool isReady() { return ready_; } Pipeline::Ptr getPipeline() { return pipeline_; } bool isPublisher() { return is_publisher_; } void setBitrateFromMaxQualityLayer(uint64_t bitrate) { bitrate_from_max_quality_layer_ = bitrate; } @@ -186,6 +187,7 @@ class MediaStream: public MediaSink, public MediaSource, public FeedbackSink, bool should_send_feedback_; bool slide_show_mode_; bool sending_; + bool ready_; int bundle_; uint32_t rate_control_; // Target bitrate for hacky rate control in BPS diff --git a/erizo/src/erizo/NicerConnection.cpp b/erizo/src/erizo/NicerConnection.cpp index 8b6243fcdb..a5871a869a 100644 --- a/erizo/src/erizo/NicerConnection.cpp +++ b/erizo/src/erizo/NicerConnection.cpp @@ -162,6 +162,9 @@ void NicerConnection::async(function)> f) } void NicerConnection::start() { + ufrag_ = getNewUfrag(); + upass_ = getNewPwd(); + async([] (std::shared_ptr this_ptr) { this_ptr->startSync(); }); @@ -170,8 +173,6 @@ void NicerConnection::start() { void NicerConnection::startSync() { UINT4 flags = NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION; - ufrag_ = getNewUfrag(); - upass_ = getNewPwd(); if (ufrag_.empty() || upass_.empty()) { start_promise_.set_value(); return; diff --git a/erizo/src/erizo/SdpInfo.cpp b/erizo/src/erizo/SdpInfo.cpp index 25f9224727..8a87249fc9 100644 --- a/erizo/src/erizo/SdpInfo.cpp +++ b/erizo/src/erizo/SdpInfo.cpp @@ -54,6 +54,7 @@ namespace erizo { isRtcpMux = false; isFingerprint = false; dtlsRole = ACTPASS; + internal_dtls_role = ACTPASS; hasAudio = false; hasVideo = false; profile = SAVPF; @@ -472,8 +473,11 @@ namespace erizo { // TODO(pedro): Should provide hints void SdpInfo::createOfferSdp(bool videoEnabled, bool audioEnabled, bool bundle) { - ELOG_DEBUG("Creating offerSDP: video %d, audio %d, bundle %d", videoEnabled, audioEnabled, bundle); - this->payloadVector = internalPayloadVector_; + ELOG_DEBUG("Creating offerSDP: video %d, audio %d, bundle %d, payloadVector: %d, extSize: %d", videoEnabled, audioEnabled, bundle, payloadVector.size(), extMapVector.size()); + if (payloadVector.size() == 0) { + payloadVector = internalPayloadVector_; + } + this->isBundle = bundle; this->profile = SAVPF; this->isRtcpMux = true; @@ -482,8 +486,8 @@ namespace erizo { if (audioEnabled) this->audioSdpMLine = 0; - for (unsigned int it = 0; it < internalPayloadVector_.size(); it++) { - RtpMap& rtp = internalPayloadVector_[it]; + for (unsigned int it = 0; it < payloadVector.size(); it++) { + RtpMap& rtp = payloadVector[it]; if (rtp.media_type == VIDEO_TYPE) { videoCodecs++; } else if (rtp.media_type == AUDIO_TYPE) { @@ -498,6 +502,16 @@ namespace erizo { ELOG_DEBUG("Setting Offer SDP"); } + void SdpInfo::copyInfoFromSdp(std::shared_ptr offerSdp) { + payloadVector = offerSdp->payloadVector; + videoCodecs = offerSdp->videoCodecs; + audioCodecs = offerSdp->audioCodecs; + inOutPTMap = offerSdp->inOutPTMap; + outInPTMap = offerSdp->outInPTMap; + extMapVector = offerSdp->extMapVector; + ELOG_DEBUG("Offer SDP successfully copied, extSize: %d, payloadSize: %d, videoCodecs: %d, audioCodecs: %d", extMapVector.size(), payloadVector.size(), videoCodecs, audioCodecs); + } + void SdpInfo::setOfferSdp(std::shared_ptr offerSdp) { this->videoCodecs = offerSdp->videoCodecs; this->audioCodecs = offerSdp->audioCodecs; diff --git a/erizo/src/erizo/SdpInfo.h b/erizo/src/erizo/SdpInfo.h index 674f4fce00..416f40c5c0 100644 --- a/erizo/src/erizo/SdpInfo.h +++ b/erizo/src/erizo/SdpInfo.h @@ -238,6 +238,8 @@ class SdpInfo { bool supportPayloadType(const unsigned int payloadType); void createOfferSdp(bool videoEnabled, bool audioEnabled, bool bundle); + + void copyInfoFromSdp(std::shared_ptr offerSdp); /** * @brief copies relevant information from the offer sdp for which this will be an answer sdp * @param offerSdp The offer SDP as received via signaling and parsed @@ -295,6 +297,10 @@ class SdpInfo { */ DtlsRole dtlsRole; /** + * Internal DTLS Role + */ + DtlsRole internal_dtls_role; + /** * Mapping from internal PT (key) to external PT (value) */ std::map inOutPTMap; diff --git a/erizo/src/erizo/WebRtcConnection.cpp b/erizo/src/erizo/WebRtcConnection.cpp index 38fe2dba06..bd79701bdf 100644 --- a/erizo/src/erizo/WebRtcConnection.cpp +++ b/erizo/src/erizo/WebRtcConnection.cpp @@ -98,41 +98,62 @@ void WebRtcConnection::close() { } bool WebRtcConnection::init() { - maybeNotifyWebRtcConnectionEvent(global_state_, ""); - return true; + maybeNotifyWebRtcConnectionEvent(global_state_, ""); + return true; } -bool WebRtcConnection::createOffer(bool video_enabled, bool audioEnabled, bool bundle) { +std::shared_ptr> WebRtcConnection::createOffer(bool video_enabled, bool audio_enabled, bool bundle) { + return asyncTask([video_enabled, audio_enabled, bundle] (std::shared_ptr connection) { + connection->createOfferSync(video_enabled, audio_enabled, bundle); + }); +} + +bool WebRtcConnection::createOfferSync(bool video_enabled, bool audio_enabled, bool bundle) { boost::mutex::scoped_lock lock(update_state_mutex_); bundle_ = bundle; video_enabled_ = video_enabled; - audio_enabled_ = audioEnabled; + audio_enabled_ = audio_enabled; local_sdp_->createOfferSdp(video_enabled_, audio_enabled_, bundle_); + local_sdp_->dtlsRole = ACTPASS; + if (local_sdp_->internal_dtls_role == ACTPASS) { + local_sdp_->internal_dtls_role = PASSIVE; + } - ELOG_DEBUG("%s message: Creating sdp offer, isBundle: %d", toLog(), bundle_); + ELOG_DEBUG("%s message: Creating sdp offer, isBundle: %d, setup: %d", toLog(), bundle_, local_sdp_->internal_dtls_role); - if (video_enabled_) { - forEachMediaStream([this] (const std::shared_ptr &media_stream) { + forEachMediaStream([this] (const std::shared_ptr &media_stream) { + if (!media_stream->isReady() || media_stream->isPublisher()) { + ELOG_DEBUG("%s message: getting local SDPInfo stream not running, stream_id: %s", toLog(), media_stream->getId()); + return; + } + if (video_enabled_) { std::vector video_ssrc_list = std::vector(); - video_ssrc_list.push_back(media_stream->getVideoSinkSSRC()); - local_sdp_->video_ssrc_map[media_stream->getLabel()] = video_ssrc_list; - }); - } - if (audio_enabled_) { - forEachMediaStream([this] (const std::shared_ptr &media_stream) { - local_sdp_->audio_ssrc_map[media_stream->getLabel()] = media_stream->getAudioSinkSSRC(); - }); - } - + if (media_stream->getVideoSinkSSRC() != kDefaultVideoSinkSSRC && media_stream->getVideoSinkSSRC() != 0) { + video_ssrc_list.push_back(media_stream->getVideoSinkSSRC()); + } + ELOG_DEBUG("%s message: getting local SDPInfo, stream_id: %s, audio_ssrc: %u", + toLog(), media_stream->getId(), media_stream->getAudioSinkSSRC()); + if (!video_ssrc_list.empty()) { + local_sdp_->video_ssrc_map[media_stream->getLabel()] = video_ssrc_list; + } + } + if (audio_enabled_) { + if (media_stream->getAudioSinkSSRC() != kDefaultAudioSinkSSRC && media_stream->getAudioSinkSSRC() != 0) { + local_sdp_->audio_ssrc_map[media_stream->getLabel()] = media_stream->getAudioSinkSSRC(); + } + } + }); auto listener = std::dynamic_pointer_cast(shared_from_this()); if (bundle_) { - video_transport_.reset(new DtlsTransport(VIDEO_TYPE, "video", connection_id_, bundle_, true, - listener, ice_config_ , "", "", true, worker_, io_worker_)); - video_transport_->copyLogContextFrom(*this); - video_transport_->start(); + if (video_transport_.get() == nullptr && (video_enabled_ || audio_enabled_)) { + video_transport_.reset(new DtlsTransport(VIDEO_TYPE, "video", connection_id_, bundle_, true, + listener, ice_config_ , "", "", true, worker_, io_worker_)); + video_transport_->copyLogContextFrom(*this); + video_transport_->start(); + } } else { if (video_transport_.get() == nullptr && video_enabled_) { // For now we don't re/check transports, if they are already created we leave them there @@ -155,15 +176,15 @@ bool WebRtcConnection::createOffer(bool video_enabled, bool audioEnabled, bool b return true; } -void WebRtcConnection::addMediaStream(std::shared_ptr media_stream) { - asyncTask([media_stream] (std::shared_ptr connection) { +std::shared_ptr> WebRtcConnection::addMediaStream(std::shared_ptr media_stream) { + return asyncTask([media_stream] (std::shared_ptr connection) { ELOG_DEBUG("%s message: Adding mediaStream, id: %s", connection->toLog(), media_stream->getId().c_str()); connection->media_streams_.push_back(media_stream); }); } -void WebRtcConnection::removeMediaStream(const std::string& stream_id) { - asyncTask([stream_id] (std::shared_ptr connection) { +std::shared_ptr> WebRtcConnection::removeMediaStream(const std::string& stream_id) { + return asyncTask([stream_id] (std::shared_ptr connection) { boost::mutex::scoped_lock lock(connection->update_state_mutex_); ELOG_DEBUG("%s message: removing mediaStream, id: %s", connection->toLog(), stream_id.c_str()); connection->media_streams_.erase(std::remove_if(connection->media_streams_.begin(), @@ -189,17 +210,29 @@ void WebRtcConnection::forEachMediaStream(std::function&)> func) { +boost::future WebRtcConnection::forEachMediaStreamAsync(std::function&)> func) { + std::vector> futures; std::for_each(media_streams_.begin(), media_streams_.end(), - [func] (const std::shared_ptr &stream) { - stream->asyncTask([func] (const std::shared_ptr &stream) { - func(stream); - }); + [func, &futures] (const std::shared_ptr &stream) { + std::shared_ptr> p = std::make_shared>(); + futures.push_back(p->get_future()); + stream->asyncTask([func, p] (const std::shared_ptr &stream) { + func(stream); + p->set_value(); + }); + }); + std::shared_ptr> p = std::make_shared>(); + return boost::when_all(futures.begin(), futures.end()) + .then([p](boost::future, std::__1::allocator>>>) { + p->set_value(); }); + return p->get_future(); } -bool WebRtcConnection::setRemoteSdpInfo(std::shared_ptr sdp, std::string stream_id) { - asyncTask([sdp, stream_id] (std::shared_ptr connection) { +boost::future WebRtcConnection::setRemoteSdpInfo(std::shared_ptr sdp, std::vector stream_ids) { + std::shared_ptr> p = std::make_shared>(); + boost::future f = p->get_future(); + asyncTask([sdp, stream_ids, p] (std::shared_ptr connection) { ELOG_DEBUG("%s message: setting remote SDPInfo", connection->toLog()); if (!connection->sending_) { @@ -207,16 +240,28 @@ bool WebRtcConnection::setRemoteSdpInfo(std::shared_ptr sdp, std::strin } connection->remote_sdp_ = sdp; - connection->processRemoteSdp(stream_id); + boost::future f = connection->processRemoteSdp(stream_ids); + f.then([p](boost::future future) { + p->set_value(); + }); + }); + return f; +} + +void WebRtcConnection::copyDataToLocalSdpIndo(std::shared_ptr sdp_info) { + asyncTask([sdp_info] (std::shared_ptr connection) { + if (connection->sending_) { + connection->local_sdp_->copyInfoFromSdp(sdp_info); + connection->local_sdp_->updateSupportedExtensionMap(connection->extension_processor_.getSupportedExtensionMap()); + } }); - return true; } std::shared_ptr WebRtcConnection::getLocalSdpInfo() { boost::mutex::scoped_lock lock(update_state_mutex_); ELOG_DEBUG("%s message: getting local SDPInfo", toLog()); forEachMediaStream([this] (const std::shared_ptr &media_stream) { - if (!media_stream->isRunning() || media_stream->isPublisher()) { + if (!media_stream->isReady() || media_stream->isPublisher()) { ELOG_DEBUG("%s message: getting local SDPInfo stream not running, stream_id: %s", toLog(), media_stream->getId()); return; } @@ -262,41 +307,33 @@ std::shared_ptr WebRtcConnection::getLocalSdpInfo() { return local_sdp_; } -bool WebRtcConnection::setRemoteSdp(const std::string &sdp, std::string stream_id) { - asyncTask([sdp, stream_id] (std::shared_ptr connection) { +bool WebRtcConnection::setRemoteSdp(const std::string &sdp, std::vector stream_ids) { + asyncTask([sdp, stream_ids] (std::shared_ptr connection) { ELOG_DEBUG("%s message: setting remote SDP", connection->toLog()); if (!connection->sending_) { return; } connection->remote_sdp_->initWithSdp(sdp, ""); - connection->processRemoteSdp(stream_id); + connection->processRemoteSdp(stream_ids); }); return true; } -void WebRtcConnection::setRemoteSdpsToMediaStreams(std::string stream_id) { - ELOG_DEBUG("%s message: setting remote SDP, stream: %s", toLog(), stream_id); - - auto stream = std::find_if(media_streams_.begin(), media_streams_.end(), - [stream_id, this](const std::shared_ptr &media_stream) { - ELOG_DEBUG("%s message: setting remote SDP, stream: %s, stream_id: %s", - toLog(), media_stream->getId(), stream_id); - return media_stream->getId() == stream_id; - }); +boost::future WebRtcConnection::setRemoteSdpsToMediaStreams(std::vector stream_ids) { + ELOG_DEBUG("%s message: setting remote SDP", toLog()); + std::weak_ptr weak_this = shared_from_this(); - if (stream != media_streams_.end()) { - std::weak_ptr weak_this = shared_from_this(); - (*stream)->asyncTask([weak_this, stream_id] (const std::shared_ptr &media_stream) { - if (auto connection = weak_this.lock()) { - media_stream->setRemoteSdp(connection->remote_sdp_); - ELOG_DEBUG("%s message: setting remote SDP to stream, stream: %s", connection->toLog(), media_stream->getId()); - connection->onRemoteSdpsSetToMediaStreams(stream_id); - } - }); - } else { - onRemoteSdpsSetToMediaStreams(stream_id); - } + return forEachMediaStreamAsync([weak_this, stream_ids](std::shared_ptr media_stream) { + if (auto connection = weak_this.lock()) { + media_stream->setRemoteSdp(connection->remote_sdp_); + ELOG_DEBUG("%s message: setting remote SDP to stream, stream: %s", connection->toLog(), media_stream->getId()); + auto stream_it = std::find(stream_ids.begin(), stream_ids.end(), media_stream->getId()); + if (stream_it != stream_ids.end()) { + connection->onRemoteSdpsSetToMediaStreams(media_stream->getId()); + } + } + }); } void WebRtcConnection::onRemoteSdpsSetToMediaStreams(std::string stream_id) { @@ -307,11 +344,16 @@ void WebRtcConnection::onRemoteSdpsSetToMediaStreams(std::string stream_id) { }); } -bool WebRtcConnection::processRemoteSdp(std::string stream_id) { +boost::future WebRtcConnection::processRemoteSdp(std::vector stream_ids) { ELOG_DEBUG("%s message: processing remote SDP", toLog()); + if (!first_remote_sdp_processed_ && local_sdp_->internal_dtls_role == ACTPASS) { + local_sdp_->internal_dtls_role = ACTIVE; + } + local_sdp_->dtlsRole = local_sdp_->internal_dtls_role; + ELOG_DEBUG("%s message: process remote sdp, setup: %d", toLog(), local_sdp_->internal_dtls_role); + if (first_remote_sdp_processed_) { - setRemoteSdpsToMediaStreams(stream_id); - return true; + return setRemoteSdpsToMediaStreams(stream_ids); } bundle_ = remote_sdp_->isBundle; @@ -319,9 +361,6 @@ bool WebRtcConnection::processRemoteSdp(std::string stream_id) { extension_processor_.setSdpInfo(local_sdp_); local_sdp_->updateSupportedExtensionMap(extension_processor_.getSupportedExtensionMap()); - if (remote_sdp_->dtlsRole == ACTPASS) { - local_sdp_->dtlsRole = ACTIVE; - } audio_enabled_ = remote_sdp_->hasAudio; video_enabled_ = remote_sdp_->hasVideo; @@ -376,9 +415,10 @@ bool WebRtcConnection::processRemoteSdp(std::string stream_id) { } } } - setRemoteSdpsToMediaStreams(stream_id); + + boost::future f = setRemoteSdpsToMediaStreams(stream_ids); first_remote_sdp_processed_ = true; - return true; + return f; } @@ -547,13 +587,16 @@ void WebRtcConnection::maybeNotifyWebRtcConnectionEvent(const WebRTCEvent& event conn_event_listener_->notifyEvent(event, message, stream_id); } -void WebRtcConnection::asyncTask(std::function)> f) { +std::shared_ptr> WebRtcConnection::asyncTask(std::function)> f) { + auto task_promise = std::make_shared>(); std::weak_ptr weak_this = shared_from_this(); - worker_->task([weak_this, f] { + worker_->task([weak_this, f, task_promise] { if (auto this_ptr = weak_this.lock()) { f(this_ptr); } + task_promise->set_value(); }); + return task_promise; } void WebRtcConnection::updateState(TransportState state, Transport * transport) { diff --git a/erizo/src/erizo/WebRtcConnection.h b/erizo/src/erizo/WebRtcConnection.h index a202aae7a2..a4dede0b49 100644 --- a/erizo/src/erizo/WebRtcConnection.h +++ b/erizo/src/erizo/WebRtcConnection.h @@ -1,6 +1,8 @@ #ifndef ERIZO_SRC_ERIZO_WEBRTCCONNECTION_H_ #define ERIZO_SRC_ERIZO_WEBRTCCONNECTION_H_ +#include +#include #include #include @@ -82,15 +84,15 @@ class WebRtcConnection: public TransportListener, public LogContext, void close(); void syncClose(); - bool setRemoteSdpInfo(std::shared_ptr sdp, std::string stream_id); + boost::future setRemoteSdpInfo(std::shared_ptr sdp, std::vector stream_ids); /** * Sets the SDP of the remote peer. * @param sdp The SDP. * @return true if the SDP was received correctly. */ - bool setRemoteSdp(const std::string &sdp, std::string stream_id); + bool setRemoteSdp(const std::string &sdp, std::vector stream_ids); - bool createOffer(bool video_enabled, bool audio_enabled, bool bundle); + std::shared_ptr> createOffer(bool video_enabled, bool audio_enabled, bool bundle); /** * Add new remote candidate (from remote peer). * @param sdp The candidate in SDP format. @@ -102,6 +104,10 @@ class WebRtcConnection: public TransportListener, public LogContext, * @return The SDP as a SdpInfo. */ std::shared_ptr getLocalSdpInfo(); + /** + * Copy some SdpInfo data to local SdpInfo + */ + void copyDataToLocalSdpIndo(std::shared_ptr sdp_info); /** * Obtains the local SDP. * @return The SDP as a string. @@ -130,15 +136,15 @@ class WebRtcConnection: public TransportListener, public LogContext, void write(std::shared_ptr packet); void syncWrite(std::shared_ptr packet); - void asyncTask(std::function)> f); + std::shared_ptr> asyncTask(std::function)> f); bool isAudioMuted() { return audio_muted_; } bool isVideoMuted() { return video_muted_; } - void addMediaStream(std::shared_ptr media_stream); - void removeMediaStream(const std::string& stream_id); + std::shared_ptr> addMediaStream(std::shared_ptr media_stream); + std::shared_ptr> removeMediaStream(const std::string& stream_id); void forEachMediaStream(std::function&)> func); - void forEachMediaStreamAsync(std::function&)> func); + boost::future forEachMediaStreamAsync(std::function&)> func); void setTransport(std::shared_ptr transport); // Only for Testing purposes @@ -153,8 +159,9 @@ class WebRtcConnection: public TransportListener, public LogContext, } private: - bool processRemoteSdp(std::string stream_id); - void setRemoteSdpsToMediaStreams(std::string stream_id); + bool createOfferSync(bool video_enabled, bool audio_enabled, bool bundle); + boost::future processRemoteSdp(std::vector stream_ids); + boost::future setRemoteSdpsToMediaStreams(std::vector stream_ids); void onRemoteSdpsSetToMediaStreams(std::string stream_id); std::string getJSONCandidate(const std::string& mid, const std::string& sdp); void trackTransportInfo(); diff --git a/erizo/src/erizo/rtp/FecReceiverHandler.cpp b/erizo/src/erizo/rtp/FecReceiverHandler.cpp index e951161fc9..84bf5656e2 100644 --- a/erizo/src/erizo/rtp/FecReceiverHandler.cpp +++ b/erizo/src/erizo/rtp/FecReceiverHandler.cpp @@ -33,7 +33,7 @@ void FecReceiverHandler::notifyUpdate() { return; } bool is_slide_show_mode_active = stream->isSlideShowModeEnabled(); - if (!stream->getRemoteSdpInfo()->supportPayloadType(RED_90000_PT) || is_slide_show_mode_active) { + if ((stream->getRemoteSdpInfo() && !stream->getRemoteSdpInfo()->supportPayloadType(RED_90000_PT)) || is_slide_show_mode_active) { enable(); } else { disable(); diff --git a/erizoAPI/AsyncPromiseWorker.cc b/erizoAPI/AsyncPromiseWorker.cc new file mode 100644 index 0000000000..8fdfd1a461 --- /dev/null +++ b/erizoAPI/AsyncPromiseWorker.cc @@ -0,0 +1,16 @@ +#include +#include "AsyncPromiseWorker.h" + +AsyncPromiseWorker::AsyncPromiseWorker(Nan::Persistent *persistent) + : AsyncWorker(nullptr) { + _persistent = persistent; +} + +AsyncPromiseWorker::~AsyncPromiseWorker() {} + +void AsyncPromiseWorker::HandleOKCallback() { + Nan::HandleScope scope; + auto resolver = Nan::New(*_persistent); + resolver->Resolve(Nan::GetCurrentContext(), Nan::New("").ToLocalChecked()); +} + diff --git a/erizoAPI/AsyncPromiseWorker.h b/erizoAPI/AsyncPromiseWorker.h new file mode 100644 index 0000000000..dce82a33c3 --- /dev/null +++ b/erizoAPI/AsyncPromiseWorker.h @@ -0,0 +1,17 @@ +#ifndef ERIZOAPI_ASYNCPROMISEWORKER_H_ +#define ERIZOAPI_ASYNCPROMISEWORKER_H_ + +#include + +class AsyncPromiseWorker : public Nan::AsyncWorker { + public: + explicit AsyncPromiseWorker(Nan::Persistent *persistent); + ~AsyncPromiseWorker(); + virtual void Execute() = 0; + void HandleOKCallback(); + + private: + Nan::Persistent *_persistent; +}; + +#endif // ERIZOAPI_ASYNCPROMISEWORKER_H_ diff --git a/erizoAPI/ConnectionDescription.cc b/erizoAPI/ConnectionDescription.cc index f6b914d480..a43724978c 100644 --- a/erizoAPI/ConnectionDescription.cc +++ b/erizoAPI/ConnectionDescription.cc @@ -104,6 +104,7 @@ NAN_MODULE_INIT(ConnectionDescription::Init) { Nan::SetPrototypeMethod(tpl, "getRids", getRids); Nan::SetPrototypeMethod(tpl, "postProcessInfo", postProcessInfo); + Nan::SetPrototypeMethod(tpl, "copyInfoFromSdp", copyInfoFromSdp); constructor.Reset(tpl->GetFunction()); Nan::Set(target, Nan::New("ConnectionDescription").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked()); @@ -746,6 +747,15 @@ NAN_METHOD(ConnectionDescription::postProcessInfo) { info.GetReturnValue().Set(Nan::New(success)); } +NAN_METHOD(ConnectionDescription::copyInfoFromSdp) { + GET_SDP(); + ConnectionDescription* source = + Nan::ObjectWrap::Unwrap(Nan::To(info[0]).ToLocalChecked()); + + std::shared_ptr source_sdp = source->me; + sdp->copyInfoFromSdp(source_sdp); +} + NAN_METHOD(ConnectionDescription::close) { GET_SDP(); obj->me.reset(); diff --git a/erizoAPI/ConnectionDescription.h b/erizoAPI/ConnectionDescription.h index 2730f9db29..964e0c4027 100644 --- a/erizoAPI/ConnectionDescription.h +++ b/erizoAPI/ConnectionDescription.h @@ -83,6 +83,8 @@ class ConnectionDescription : public Nan::ObjectWrap { static NAN_METHOD(postProcessInfo); + static NAN_METHOD(copyInfoFromSdp); + static Nan::Persistent constructor; }; diff --git a/erizoAPI/MediaStream.cc b/erizoAPI/MediaStream.cc index 5a993e18c9..3d610115cd 100644 --- a/erizoAPI/MediaStream.cc +++ b/erizoAPI/MediaStream.cc @@ -176,7 +176,8 @@ NAN_METHOD(MediaStream::init) { if (!me) { return; } - bool r = me->init(); + bool force = info.Length() > 0 ? info[0]->BooleanValue() : false; + bool r = me->init(force); info.GetReturnValue().Set(Nan::New(r)); } diff --git a/erizoAPI/WebRtcConnection.cc b/erizoAPI/WebRtcConnection.cc index 319acd5a99..7bdab88d54 100644 --- a/erizoAPI/WebRtcConnection.cc +++ b/erizoAPI/WebRtcConnection.cc @@ -5,8 +5,11 @@ #include "WebRtcConnection.h" #include "ConnectionDescription.h" #include "MediaStream.h" +#include "AsyncPromiseWorker.h" #include // NOLINT +#include // NOLINT +#include // NOLINT #include "lib/json.hpp" #include "IOThreadPool.h" @@ -23,6 +26,61 @@ using json = nlohmann::json; Nan::Persistent WebRtcConnection::constructor; +// Classes for Async (not in node main thread) operations +class MediaStreamAdder : public AsyncPromiseWorker { + public: + MediaStreamAdder(Nan::Persistent *persistent, + std::shared_ptr wr, + std::shared_ptr ms) : + AsyncPromiseWorker(persistent), connection_(wr), stream_(ms) { + } + ~MediaStreamAdder() {} + void Execute() { + connection_->addMediaStream(stream_)->get_future().wait(); + } + private: + std::shared_ptr connection_; + std::shared_ptr stream_; +}; + +class MediaStreamDeleter : public AsyncPromiseWorker { + public: + MediaStreamDeleter(Nan::Persistent *persistent, + std::shared_ptr wr, + std::string ms) : + AsyncPromiseWorker(persistent), connection_(wr), stream_(ms) { + } + ~MediaStreamDeleter() {} + void Execute() { + connection_->removeMediaStream(stream_)->get_future().wait(); + } + private: + std::shared_ptr connection_; + std::string stream_; +}; + +class CreateOfferWorker : public AsyncPromiseWorker { + public: + CreateOfferWorker(Nan::Persistent *persistent, + std::shared_ptr wr, + bool video_enabled, bool audio_enabled, bool bundle) : + AsyncPromiseWorker(persistent), + connection_(wr), + video_enabled_(video_enabled), + audio_enabled_(audio_enabled), + bundle_(bundle) { + } + ~CreateOfferWorker() {} + void Execute() { + connection_->createOffer(video_enabled_, audio_enabled_, bundle_)->get_future().wait(); + } + private: + std::shared_ptr connection_; + bool video_enabled_; + bool audio_enabled_; + bool bundle_; +}; + DEFINE_LOGGER(WebRtcConnection, "ErizoAPI.WebRtcConnection"); void destroyWebRtcConnectionAsyncHandle(uv_handle_t *handle) { @@ -84,6 +142,7 @@ NAN_MODULE_INIT(WebRtcConnection::Init) { Nan::SetPrototypeMethod(tpl, "setMetadata", setMetadata); Nan::SetPrototypeMethod(tpl, "addMediaStream", addMediaStream); Nan::SetPrototypeMethod(tpl, "removeMediaStream", removeMediaStream); + Nan::SetPrototypeMethod(tpl, "copySdpToLocalDescription", copySdpToLocalDescription); constructor.Reset(tpl->GetFunction()); Nan::Set(target, Nan::New("WebRtcConnection").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked()); @@ -210,7 +269,6 @@ NAN_METHOD(WebRtcConnection::New) { obj->me = std::make_shared(worker, io_worker, wrtcId, iceConfig, rtp_mappings, ext_mappings, obj); obj->Wrap(info.This()); - ELOG_DEBUG("%s, message: Created", obj->toLog()); info.GetReturnValue().Set(info.This()); } else { // TODO(pedro) Check what happens here @@ -249,8 +307,13 @@ NAN_METHOD(WebRtcConnection::createOffer) { bool audio_enabled = info[1]->BooleanValue(); bool bundle = info[2]->BooleanValue(); - bool r = me->createOffer(video_enabled, audio_enabled, bundle); - info.GetReturnValue().Set(Nan::New(r)); + v8::Local resolver = v8::Promise::Resolver::New(info.GetIsolate()); + Nan::Persistent *persistent = new Nan::Persistent(resolver); + + CreateOfferWorker *worker = new CreateOfferWorker(persistent, me, video_enabled, audio_enabled, bundle); + Nan::AsyncQueueWorker(worker); + + info.GetReturnValue().Set(resolver->GetPromise()); } NAN_METHOD(WebRtcConnection::setMetadata) { @@ -290,10 +353,16 @@ NAN_METHOD(WebRtcConnection::setRemoteSdp) { v8::String::Utf8Value param(Nan::To(info[0]).ToLocalChecked()); std::string sdp = std::string(*param); - v8::String::Utf8Value stream_id_param(Nan::To(info[1]).ToLocalChecked()); - std::string stream_id = std::string(*stream_id_param); + v8::Local stream_ids_param = v8::Local::Cast(info[1]); + std::vector stream_ids; + for (uint32_t i = 0; i < stream_ids_param->Length(); i++) { + v8::Local element = stream_ids_param->Get(i); + v8::String::Utf8Value stream_id_param(Nan::To(element).ToLocalChecked()); + std::string stream_id = std::string(*stream_id_param); + stream_ids.push_back(stream_id); + } - bool r = me->setRemoteSdp(sdp, stream_id); + bool r = me->setRemoteSdp(sdp, stream_ids); info.GetReturnValue().Set(Nan::New(r)); } @@ -309,11 +378,18 @@ NAN_METHOD(WebRtcConnection::setRemoteDescription) { Nan::ObjectWrap::Unwrap(Nan::To(info[0]).ToLocalChecked()); auto sdp = std::make_shared(*param->me.get()); - v8::String::Utf8Value stream_id_param(Nan::To(info[1]).ToLocalChecked()); - std::string stream_id = std::string(*stream_id_param); + v8::Local stream_ids_param = v8::Local::Cast(info[1]); + std::vector stream_ids; + for (uint32_t i = 0; i < stream_ids_param->Length(); i++) { + v8::Local element = stream_ids_param->Get(i); + v8::String::Utf8Value stream_id_param(Nan::To(element).ToLocalChecked()); + std::string stream_id = std::string(*stream_id_param); + stream_ids.push_back(stream_id); + } - bool r = me->setRemoteSdpInfo(sdp, stream_id); - info.GetReturnValue().Set(Nan::New(r)); + boost::future future = me->setRemoteSdpInfo(sdp, stream_ids); + future.get(); + info.GetReturnValue().Set(Nan::New(true)); } NAN_METHOD(WebRtcConnection::getLocalDescription) { @@ -331,6 +407,21 @@ NAN_METHOD(WebRtcConnection::getLocalDescription) { info.GetReturnValue().Set(instance); } +NAN_METHOD(WebRtcConnection::copySdpToLocalDescription) { + WebRtcConnection* obj = Nan::ObjectWrap::Unwrap(info.Holder()); + std::shared_ptr me = obj->me; + if (!me) { + return; + } + + ConnectionDescription* source = + Nan::ObjectWrap::Unwrap(Nan::To(info[0]).ToLocalChecked()); + + std::shared_ptr source_sdp = source->me; + + me->copyDataToLocalSdpIndo(source_sdp); +} + NAN_METHOD(WebRtcConnection::addRemoteCandidate) { WebRtcConnection* obj = Nan::ObjectWrap::Unwrap(info.Holder()); std::shared_ptr me = obj->me; @@ -383,9 +474,15 @@ NAN_METHOD(WebRtcConnection::addMediaStream) { } MediaStream* param = Nan::ObjectWrap::Unwrap(Nan::To(info[0]).ToLocalChecked()); - auto wr = std::shared_ptr(param->me); + auto ms = std::shared_ptr(param->me); + + v8::Local resolver = v8::Promise::Resolver::New(info.GetIsolate()); + Nan::Persistent *persistent = new Nan::Persistent(resolver); + + MediaStreamAdder *adder = new MediaStreamAdder(persistent, me, ms); + Nan::AsyncQueueWorker(adder); - me->addMediaStream(wr); + info.GetReturnValue().Set(resolver->GetPromise()); } NAN_METHOD(WebRtcConnection::removeMediaStream) { @@ -397,7 +494,14 @@ NAN_METHOD(WebRtcConnection::removeMediaStream) { v8::String::Utf8Value param(Nan::To(info[0]).ToLocalChecked()); std::string streamId = std::string(*param); - me->removeMediaStream(streamId); + + v8::Local resolver = v8::Promise::Resolver::New(info.GetIsolate()); + Nan::Persistent *persistent = new Nan::Persistent(resolver); + + MediaStreamDeleter *deleter = new MediaStreamDeleter(persistent, me, streamId); + Nan::AsyncQueueWorker(deleter); + + info.GetReturnValue().Set(resolver->GetPromise()); } // Async methods diff --git a/erizoAPI/WebRtcConnection.h b/erizoAPI/WebRtcConnection.h index e9fea54826..3ccd319ab1 100644 --- a/erizoAPI/WebRtcConnection.h +++ b/erizoAPI/WebRtcConnection.h @@ -105,6 +105,8 @@ class WebRtcConnection : public erizo::WebRtcConnectionEventListener, static NAN_METHOD(addMediaStream); static NAN_METHOD(removeMediaStream); + static NAN_METHOD(copySdpToLocalDescription); + static Nan::Persistent constructor; static NAUV_WORK_CB(eventsCallback); diff --git a/erizo_controller/erizoClient/src/ErizoConnectionManager.js b/erizo_controller/erizoClient/src/ErizoConnectionManager.js index ae1888aa23..74e1a76376 100644 --- a/erizo_controller/erizoClient/src/ErizoConnectionManager.js +++ b/erizo_controller/erizoClient/src/ErizoConnectionManager.js @@ -110,8 +110,8 @@ class ErizoConnection extends EventEmitterConst { this.streamsMap.remove(streamId); } - processSignalingMessage(msg) { - this.stack.processSignalingMessage(msg); + processSignalingMessage(msg, streamIds) { + this.stack.processSignalingMessage(msg, streamIds); } sendSignalingMessage(msg) { diff --git a/erizo_controller/erizoClient/src/Room.js b/erizo_controller/erizoClient/src/Room.js index ec72776354..46eed09530 100644 --- a/erizo_controller/erizoClient/src/Room.js +++ b/erizo_controller/erizoClient/src/Room.js @@ -113,9 +113,10 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { const getP2PConnectionOptions = (stream, peerSocket) => { const options = { - callback(msg) { + callback(msg, streamIds) { socket.sendSDP('signaling_message', { streamId: stream.getID(), + streamIds, peerSocket, msg }); }, @@ -222,7 +223,6 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { onStreamFailed(stream); } }); - stream.pc.createOffer(true, false, stream.getID()); }; const createLocalStreamErizoConnection = (streamInput, erizoId, options) => { @@ -240,6 +240,22 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { if (!options.createOffer) { stream.pc.createOffer(false, spec.singlePC, stream.getID()); } }; + const onAutomaticStreamsSubscription = (args) => { + const streamIds = args.streamIds; + const erizoId = args.erizoId; + const options = args.options; + let stream; + streamIds.forEach((id) => { + // Prepare each stream to listen to PC events. + stream = remoteStreams.get(id); + createRemoteStreamErizoConnection(stream, erizoId, options); + }); + // Apply the Offer to only one PC of the streams, since they all should be using the same PC. + if (stream && stream.pc) { + stream.pc.processSignalingMessage(args, streamIds); + } + }; + // We receive an event with a new stream in the room. // type can be "media" or "data" @@ -265,6 +281,10 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { let stream; if (arg.peerId) { stream = remoteStreams.get(arg.peerId); + } else if (arg.peerIds) { + Logger.info('Message from multiple streamIds', arg.peerIds, 'message', + arg.mess); + onAutomaticStreamsSubscription(arg.mess); } else { stream = localStreams.get(arg.streamId); } @@ -539,7 +559,7 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { Logger.info('Subscriber added'); createRemoteStreamErizoConnection(stream, erizoId, options); - + stream.pc.createOffer(true, false, stream.getID()); callback(true); }); }; @@ -865,6 +885,24 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { } }; + // const selectors = { + // '/id': '23', + // '/attributes/group': '23', + // '/attributes/kind': 'professor', + // '/attributes/externalId': '10' + // }; + // const options = {audio: true, video: false, forceTurn: true}; + that.autoSubscribe = (selectors, options, callback) => { + if (!socket) { + return; + } + socket.sendMessage('autoSubscribe', { selectors, options }, (result) => { + if (result) { + callback(result); + } + }); + }; + that.getStreamStats = (stream, callback = () => {}) => { if (!socket) { return 'Error getting stats - no socket'; diff --git a/erizo_controller/erizoClient/src/Socket.js b/erizo_controller/erizoClient/src/Socket.js index ea29b959c8..8656ac6d4d 100644 --- a/erizo_controller/erizoClient/src/Socket.js +++ b/erizo_controller/erizoClient/src/Socket.js @@ -86,6 +86,8 @@ const Socket = (newIo) => { // We receive an event of a stream removed from the room socket.on('onRemoveStream', emit.bind(that, 'onRemoveStream')); + socket.on('onAutomaticStreamsSubscription', emit.bind(that, 'onAutomaticStreamsSubscription')); + // The socket has disconnected socket.on('disconnect', (reason) => { Logger.debug('disconnect', that.id, reason); diff --git a/erizo_controller/erizoClient/src/webrtc-stacks/BaseStack.js b/erizo_controller/erizoClient/src/webrtc-stacks/BaseStack.js index 724dfe031d..07a9ec0c89 100644 --- a/erizo_controller/erizoClient/src/webrtc-stacks/BaseStack.js +++ b/erizo_controller/erizoClient/src/webrtc-stacks/BaseStack.js @@ -98,7 +98,7 @@ const BaseStack = (specInput) => { if (args[0] === 'local') { that.createOffer(args[1], args[2], args[3]); } else { - processOffer(args[1]); + processOffer(args[1], args[2]); } } }; @@ -120,7 +120,7 @@ const BaseStack = (specInput) => { }, streamId); }; - const setLocalDescForAnswer = (sessionDescription) => { + const setLocalDescForAnswer = (streamIds, sessionDescription) => { localDesc = sessionDescription; localSdp = SemanticSdp.SDPInfo.processString(localDesc.sdp); SdpHelpers.setMaxBW(localSdp, specBase); @@ -130,7 +130,7 @@ const BaseStack = (specInput) => { type: localDesc.type, sdp: localDesc.sdp, config: { maxVideoBW: specBase.maxVideoBW }, - }); + }, streamIds); Logger.info('Setting local description', localDesc); that.peerConnection.setLocalDescription(localDesc).then(() => { isNegotiating = false; @@ -139,10 +139,10 @@ const BaseStack = (specInput) => { }).catch(errorCallback); }; - processOffer = (message) => { + processOffer = (message, streamIds) => { const msg = message; if (isNegotiating) { - offerQueue.push(['remote', message]); + offerQueue.push(['remote', message, streamIds]); return; } remoteSdp = SemanticSdp.SDPInfo.processString(msg.sdp); @@ -150,6 +150,12 @@ const BaseStack = (specInput) => { const sessionVersion = remoteSdp && remoteSdp.origin && remoteSdp.origin.sessionVersion; if (latestSessionVersion >= sessionVersion) { Logger.warning(`message: processOffer discarding old sdp sessionVersion: ${sessionVersion}, latestSessionVersion: ${latestSessionVersion}`); + // We send an answer back to finish this negotiation + specBase.callback({ + type: 'answer', + sdp: localDesc.sdp, + config: { maxVideoBW: specBase.maxVideoBW }, + }, streamIds); return; } isNegotiating = true; @@ -160,7 +166,7 @@ const BaseStack = (specInput) => { that.remoteSdp = remoteSdp; that.peerConnection.setRemoteDescription(msg).then(() => { that.peerConnection.createAnswer(that.mediaConstraints) - .then(setLocalDescForAnswer) + .then(setLocalDescForAnswer.bind(this, streamIds)) .catch(errorCallback.bind(null, 'createAnswer', undefined)); specBase.remoteDescriptionSet = true; }).catch(errorCallback.bind(null, 'process Offer', undefined)); @@ -360,9 +366,9 @@ const BaseStack = (specInput) => { that.peerConnection.removeStream(stream); }; - that.processSignalingMessage = (msgInput) => { + that.processSignalingMessage = (msgInput, streamIds) => { if (msgInput.type === 'offer') { - processOffer(msgInput); + processOffer(msgInput, streamIds); } else if (msgInput.type === 'answer') { processAnswer(msgInput); } else if (msgInput.type === 'candidate') { diff --git a/erizo_controller/erizoController/models/Client.js b/erizo_controller/erizoController/models/Client.js index bb4945e05e..0687d69665 100644 --- a/erizo_controller/erizoController/models/Client.js +++ b/erizo_controller/erizoController/models/Client.js @@ -22,6 +22,7 @@ function listenToSocketEvents(client) { client.channel.socketOn('stopRecorder', client.onStopRecorder.bind(client)); client.channel.socketOn('unpublish', client.onUnpublish.bind(client)); client.channel.socketOn('unsubscribe', client.onUnsubscribe.bind(client)); + client.channel.socketOn('autoSubscribe', client.onAutoSubscribe.bind(client)); client.channel.socketOn('getStreamStats', client.onGetStreamStats.bind(client)); client.channel.on('disconnect', client.onDisconnect.bind(client)); } @@ -59,6 +60,139 @@ class Client extends events.EventEmitter { this.channel.sendBuffer(buffer); } + setSelectors(selectors, options) { + this.selectors = selectors; + this.selectorOptions = options; + this.onInternalAutoSubscriptionChange(); + } + + onInternalAutoSubscriptionChange() { + if (!this.selectors) { + return; + } + const subscribableStreams = []; + const unsubscribableStreams = []; + this.room.forEachStream((stream) => { + // We don't subscribe/unsubscribe to own published + if (this.streams.indexOf(stream.getID()) !== -1) { + return; + } + if (stream.meetAnySelector(this.selectors)) { + if (stream.hasData() && this.options.data !== false) { + stream.addDataSubscriber(this.id); + } + if (stream.hasAudio() || stream.hasVideo() || stream.hasScreen()) { + subscribableStreams.push(stream); + } + } else { + if (stream.hasData() && this.options.data !== false) { + stream.removeDataSubscriber(this.id); + } + if (stream.hasAudio() || stream.hasVideo() || stream.hasScreen()) { + unsubscribableStreams.push(stream); + } + } + }); + if (subscribableStreams.length > 0) { + this.onMultipleSubscribe(subscribableStreams, this.selectorOptions); + } + if (unsubscribableStreams.length > 0) { + this.onMultipleUnsubscribe(unsubscribableStreams); + } + } + + onMultipleSubscribe(streams, options) { + if (this.room.p2p) { + streams.forEach((stream) => { + const clientId = stream.getClient(); + const client = this.room.getClientById(clientId); + client.sendMessage('publish_me', { streamId: stream.getID(), peerSocket: this.id }); + }); + return; + } + log.info('message: addMultipleSubscribers requested, ' + + `streams: ${streams}, ` + + `clientId: ${this.id}`); + options.mediaConfiguration = this.token.mediaConfiguration; + options.singlePC = this.options.singlePC || false; + const streamIds = streams.map(stream => stream.getID()); + this.room.controller.addMultipleSubscribers(this.id, streamIds, options, (signMess) => { + // We can receive multiple initializing messages with subsets of streamIds. Each subset + // is sent from a single ErizoJS. + if (signMess.type === 'initializing') { + log.info('message: addMultipleSubscribers, ' + + 'state: SUBSCRIBER_INITIAL, ' + + `clientId: ${this.id}, ` + + `streamIds: ${signMess.streamIds}`); + if (global.config.erizoController.report.session_events) { + const timeStamp = new Date(); + if (signMess.streamIds) { + signMess.streamIds.forEach((streamId) => { + this.room.amqper.broadcast('event', { room: this.room.id, + user: this.id, + name: this.user.name, + type: 'subscribe', + stream: streamId, + timestamp: timeStamp.getTime() }); + }); + } + } + } else if (signMess.type === 'failed') { + // TODO: Add Stats event + log.warn('message: addMultipleSubscribers ICE Failed, ' + + 'state: SUBSCRIBER_FAILED, ' + + `streamId: ${signMess.streamId}, ` + + `clientId: ${this.id}`); + this.sendMessage('connection_failed', { type: 'subscribe', + streamId: signMess.streamId }); + return; + } else if (signMess.type === 'ready') { + log.info('message: addMultipleSubscribers, ' + + 'state: SUBSCRIBER_READY, ' + + `streamId: ${signMess.streamId}, ` + + `clientId: ${this.id}`); + } else if (signMess.type === 'bandwidthAlert') { + this.sendMessage('onBandwidthAlert', { streamID: signMess.streamId, + message: signMess.message, + bandwidth: signMess.bandwidth }); + return; + } else if (signMess === 'timeout') { + log.error('message: addMultipleSubscribers timeout when contacting ErizoJS, ' + + `streamId: ${signMess.streamId}, ` + + `clientId: ${this.id}`); + return; + } + + this.sendMessage('signaling_message_erizo', { mess: signMess, + options, + peerIds: signMess.streamIds }); + }); + } + + onMultipleUnsubscribe(streams) { + if (this.room.p2p) { + streams.forEach((stream) => { + const clientId = stream.getClient(); + const client = this.room.getClientById(clientId); + client.sendMessage('unpublish_me', { streamId: stream.getID(), peerSocket: this.id }); + }); + return; + } + const streamIds = streams.map(stream => stream.getID()); + this.room.controller.removeMultipleSubscribers(this.id, streamIds, (result) => { + if (global.config.erizoController.report.session_events) { + const timeStamp = new Date(); + result.streamIds.forEach((streamId) => { + this.room.amqper.broadcast('event', { room: this.room.id, + user: this.id, + type: 'unsubscribe', + stream: streamId, + timestamp: timeStamp.getTime() }); + }); + } + }); + } + sendMessage(type, arg) { this.channel.sendMessage(type, arg); } @@ -108,13 +242,14 @@ class Client extends events.EventEmitter { const targetClient = this.room.getClientById(message.peerSocket); if (targetClient) { targetClient.sendMessage('signaling_message_peer', - { streamId: message.streamId, peerSocket: this.id, msg: message.msg }); + { streamId:message.streamIds || message.streamId, peerSocket: this.id, msg: message.msg }); } } else { const isControlMessage = message.msg.type === 'control'; if (!isControlMessage || (isControlMessage && this.hasPermission(message.msg.action.name))) { - this.room.controller.processSignaling(this.id, message.streamId, message.msg); + this.room.controller.processSignaling(this.id, message.streamIds || + message.streamId, message.msg); } else { log.info('message: User unauthorized to execute action on stream, action: ' + `${message.msg.action.name}, streamId: ${message.streamId}`); @@ -138,6 +273,9 @@ class Client extends events.EventEmitter { client.sendMessage('onUpdateAttributeStream', message); } }); + this.room.forEachClient((client) => { + client.onInternalAutoSubscriptionChange(); + }); } onPublish(options, sdp, callback) { @@ -225,6 +363,9 @@ class Client extends events.EventEmitter { return; } else if (signMess.type === 'ready') { st.status = PUBLISHER_READY; + this.room.forEachClient((client) => { + client.onInternalAutoSubscriptionChange(); + }); this.room.sendMessage('onAddStream', st.getPublicStream()); log.info('message: addPublisher, ' + 'state: PUBLISHER_READY, ' + @@ -495,6 +636,19 @@ class Client extends events.EventEmitter { } } + onAutoSubscribe(data, callback = () => {}) { + if (!this.hasPermission(Permission.SUBSCRIBE)) { + if (callback) callback(null, 'Unauthorized'); + return; + } + + const selectors = data && data.selectors; + const options = data && data.options; + + this.setSelectors(selectors, options); + callback(); + } + onDisconnect() { const timeStamp = new Date(); diff --git a/erizo_controller/erizoController/models/Stream.js b/erizo_controller/erizoController/models/Stream.js index b297f5f9b9..42dac272ae 100644 --- a/erizo_controller/erizoController/models/Stream.js +++ b/erizo_controller/erizoController/models/Stream.js @@ -67,5 +67,29 @@ exports.Stream = (spec) => { attributes: spec.attributes }; }; + // const selectors = { + // '/id': '23', + // '/attributes/group': '23', + // '/attributes/kind': 'professor', + // '/attributes/externalId': '10' + // }; + that.meetAnySelector = (selectors) => { + // eslint-disable-next-line no-restricted-syntax + for (const selector of Object.keys(selectors)) { + const value = selectors[selector]; + if (selector.startsWith('/attributes')) { + const attribute = selector.replace('/attributes/', ''); + if (that.getAttributes()[attribute] === value) { + return true; + } + } else if (selector === '/id' && value === that.getID()) { + return true; + } else if (selector === '/label' && value === spec.label) { + return true; + } + } + return false; + }; + return that; }; diff --git a/erizo_controller/erizoController/roomController.js b/erizo_controller/erizoController/roomController.js index c75babaf55..50bf588940 100644 --- a/erizo_controller/erizoController/roomController.js +++ b/erizo_controller/erizoController/roomController.js @@ -182,7 +182,13 @@ exports.RoomController = (spec) => { }; that.processSignaling = (clientId, streamId, msg) => { - if (publishers[streamId] !== undefined) { + if (streamId instanceof Array && streamId.length > 0) { + log.info('message: processSignaling, ' + + `clientId: ${clientId}, ` + + `streamIds: ${streamId.length}`); + const args = [clientId, streamId, msg]; + amqper.callRpc(getErizoQueue(streamId[0]), 'processSignaling', args, {}); + } else if (publishers[streamId] !== undefined) { log.info('message: processSignaling, ' + `clientId: ${clientId}, ` + `streamId: ${streamId}`); @@ -330,7 +336,9 @@ exports.RoomController = (spec) => { callback('timeout'); return; } else if (data.type === 'initializing') { - subscribers[streamId].push(clientId); + if (subscribers[streamId].indexOf(clientId) === -1) { + subscribers[streamId].push(clientId); + } } log.info('message: addSubscriber finished, ' + `streamId: ${streamId}, ` + @@ -357,6 +365,83 @@ exports.RoomController = (spec) => { } }; + that.addMultipleSubscribers = (clientId, streamIds, options, callback, retries) => { + if (clientId === null) { + log.warn('message: addMultipleSubscribers null clientId, ' + + `streams: ${streamIds.length}, ` + + `clientId: ${clientId},`, + logger.objectToLog(options.metadata)); + callback('Error: null clientId'); + return; + } + + if (retries === undefined) { retries = 0; } + + streamIds = streamIds.filter(streamId => + publishers[streamId] !== undefined && + subscribers[streamId].indexOf(clientId) === -1); + + if (streamIds.length === 0) { + return; + } + + const erizoIds = [...new Set(streamIds.map(streamId => getErizoQueue(streamId)))]; + + erizoIds.forEach((erizoId) => { + const streamIdsInErizo = streamIds.filter(streamId => getErizoQueue(streamId) === erizoId); + const args = [clientId, streamIdsInErizo, options]; + log.info('message: addMultipleSubscribers, ' + + `streams: ${streamIdsInErizo}, ` + + `clientId: ${clientId},`, + logger.objectToLog(options), + logger.objectToLog(options.metadata)); + + amqper.callRpc(erizoId, 'addMultipleSubscribers', args, + { callback: (data) => { + if (data === 'timeout') { + if (retries < MAX_ERIZOJS_RETRIES) { + retries += 1; + log.warn('message: addMultipleSubscribers ErizoJS timeout, ' + + `clientId: ${clientId}, ` + + `streams: ${streamIdsInErizo}, ` + + `erizoId: ${erizoId}, ` + + `retries: ${retries},`, + logger.objectToLog(options.metadata)); + that.addMultipleSubscribers(clientId, streamIdsInErizo, options, callback, retries); + return; + } + log.warn('message: addMultipleSubscribers ErizoJS timeout no retry, ' + + `clientId: ${clientId}, ` + + `streams: ${streamIdsInErizo.length}, ` + + `erizoId: ${erizoId},`, + logger.objectToLog(options.metadata)); + callback('timeout'); + return; + } else if (data.type === 'initializing') { + if (data.streamIds) { + data.streamIds.forEach((streamId) => { + if (subscribers[streamId].indexOf(clientId) === -1) { + subscribers[streamId].push(clientId); + } + }); + } else if (data.streamId) { + if (subscribers[data.streamId].indexOf(clientId) === -1) { + subscribers[data.streamId].push(clientId); + } + } + } + log.info('message: addMultipleSubscribers finished, ' + + `streams: ${streamIdsInErizo}, ` + + `clientId: ${clientId},`, + logger.objectToLog(options), + logger.objectToLog(options.metadata)); + data.erizoId = publishers[streamIdsInErizo[0]]; + callback(data); + }, + }); + }); + }; + /* * Removes a publisher from the room. This also deletes the associated OneToManyProcessor. */ @@ -435,6 +520,43 @@ exports.RoomController = (spec) => { } }; + /* + * Removes a subscriber from the room. + * This also removes it from the associated OneToManyProcessor. + */ + that.removeMultipleSubscribers = (subscriberId, streamIds, callback) => { + streamIds = streamIds.filter(streamId => + subscribers[streamId] !== undefined && + subscribers[streamId].indexOf(subscriberId) !== -1); + + if (streamIds.length === 0) { + return; + } + + const erizoIds = [...new Set(streamIds.map(streamId => getErizoQueue(streamId)))]; + + erizoIds.forEach((erizoId) => { + const streamIdsInErizo = streamIds.filter(streamId => getErizoQueue(streamId) === erizoId); + log.info('message: removeMultipleSubscribers, ' + + `clientId: ${subscriberId}, ` + + `streamIds: ${streamIdsInErizo}`); + const args = [subscriberId, streamIdsInErizo]; + amqper.callRpc(erizoId, 'removeMultipleSubscribers', args, { + callback: (message) => { + log.info('message: removeMultipleSubscribers finished, ' + + `response: ${message}, ` + + `clientId: ${subscriberId}, ` + + `streamIds: ${streamIds}`); + streamIdsInErizo.forEach((streamId) => { + const newIndex = subscribers[streamId].indexOf(subscriberId); + subscribers[streamId].splice(newIndex, 1); + }); + callback({ result: true, streamIds: streamIdsInErizo }); + }, + }); + }); + }; + /* * Removes all the subscribers related with a client. */ diff --git a/erizo_controller/erizoJS/erizoJSController.js b/erizo_controller/erizoJS/erizoJSController.js index 12923a915f..57896fd0c5 100644 --- a/erizo_controller/erizoJS/erizoJSController.js +++ b/erizo_controller/erizoJS/erizoJSController.js @@ -121,7 +121,23 @@ exports.ErizoJSController = (threadPool, ioThreadPool) => { that.processSignaling = (clientId, streamId, msg) => { log.info('message: Process Signaling message, ' + `streamId: ${streamId}, clientId: ${clientId}`); - if (publishers[streamId] !== undefined) { + if (streamId instanceof Array && streamId.length > 0) { + const streamIds = []; + let lastSubscriber; + streamId.forEach((sid) => { + const publisher = publishers[sid]; + if (publisher.hasSubscriber(clientId)) { + const subscriber = publisher.getSubscriber(clientId); + if (subscriber) { + lastSubscriber = subscriber; + streamIds.push(subscriber.erizoStreamId); + } + } + }); + if (lastSubscriber) { + lastSubscriber.onSignalingMessage(msg, streamIds); + } + } else if (publishers[streamId] !== undefined) { const publisher = publishers[streamId]; if (publisher.hasSubscriber(clientId)) { const subscriber = publisher.getSubscriber(clientId); @@ -219,6 +235,78 @@ exports.ErizoJSController = (threadPool, ioThreadPool) => { } }; + /* + * Adds multiple subscribers to the room. + */ + that.addMultipleSubscribers = (clientId, streamIds, options, callbackRpc) => { + const knownPublishers = streamIds.map(streamId => publishers[streamId]) + .filter(pub => + pub !== undefined && + !pub.getSubscriber(clientId)); + if (knownPublishers.length === 0) { + log.warn('message: addMultipleSubscribers to unknown publisher, ' + + `code: ${WARN_NOT_FOUND}, streamIds: ${streamIds}, ` + + `clientId: ${clientId}`, + logger.objectToLog(options.metadata)); + callbackRpc('callback', { type: 'error' }); + return; + } + + log.debug('message: addMultipleSubscribers to publishers, ' + + `streamIds: ${knownPublishers}, ` + + `clientId: ${clientId}`, + logger.objectToLog(options.metadata)); + + const client = getOrCreateClient(clientId, options.singlePC); + // eslint-disable-next-line no-param-reassign + options.publicIP = that.publicIP; + // eslint-disable-next-line no-param-reassign + options.privateRegexp = that.privateRegexp; + const connection = client.getOrCreateConnection(options); + const promises = []; + knownPublishers.forEach((publisher) => { + const streamId = publisher.streamId; + // eslint-disable-next-line no-param-reassign + options.label = publisher.label; + const subscriber = publisher.addSubscriber(clientId, connection, options); + subscriber.initMediaStream(true); + subscriber.copySdpInfoFromPublisher(); + promises.push(subscriber.promise); + subscriber.on('callback', onAdaptSchemeNotify.bind(this, callbackRpc, 'callback')); + subscriber.on('periodic_stats', onPeriodicStats.bind(this, clientId, streamId)); + subscriber.on('status_event', + onConnectionStatusEvent.bind(this, callbackRpc, clientId, streamId)); + }); + + const knownStreamIds = knownPublishers.map(pub => pub.streamId); + + const constraints = { + audio: true, + video: true, + bundle: true, + }; + + const isNewConnection = connection.init(knownStreamIds[0], constraints); + promises.push(connection.createOfferPromise); + if (options.singlePC) { + if (!isNewConnection) { + callbackRpc('callback', { type: 'initializing', streamIds: knownStreamIds }); + } + Promise.all(promises) + .then(() => { + log.debug(`message: autoSubscription waiting for gathering event`, connection.alreadyGathered, connection.gatheredPromise); + return connection.gatheredPromise; + }) + .then(() => { + const evt = connection.createOffer(); + evt.streamIds = knownStreamIds; + evt.options = options; + log.debug(`message: autoSubscription sending event, type: ${evt.type}, streamId: ${knownStreamIds}, sessionVersion: ${connection.sessionVersion}, sdp: ${evt.sdp}`); + callbackRpc('callback', evt); + }); + } + }; + /* * Removes a publisher from the room. This also deletes the associated OneToManyProcessor. */ diff --git a/erizo_controller/erizoJS/models/Connection.js b/erizo_controller/erizoJS/models/Connection.js index f0a27705ce..58d065d1ca 100644 --- a/erizo_controller/erizoJS/models/Connection.js +++ b/erizo_controller/erizoJS/models/Connection.js @@ -6,6 +6,7 @@ const events = require('events'); const addon = require('./../../../erizoAPI/build/Release/addon'); const logger = require('./../../common/logger').logger; const SessionDescription = require('./SessionDescription'); +const SemanticSdp = require('./../../common/semanticSdp/SemanticSdp'); const log = logger.getLogger('Connection'); @@ -35,8 +36,12 @@ class Connection extends events.EventEmitter { this.options = options; this.trickleIce = options.trickleIce || false; this.metadata = this.options.metadata || {}; - this.isProcessingRemoteSdp = false; + this.isNegotiating = false; this.ready = false; + this.gatheredPromise = new Promise((resolve, reject) => { + this._gatheredResolveFunction = resolve; + this._gatheredRejectFunction = reject; + }); } static _getMediaConfiguration(mediaConfiguration = 'default') { @@ -102,21 +107,32 @@ class Connection extends events.EventEmitter { this.emit('media_stream_event', streamEvent); } - _maybeSendAnswer(evt, streamId, forceOffer = false) { - if (this.isProcessingRemoteSdp) { - return; - } - if (!this.alreadyGathered && !this.trickleIce) { - return; - } + createAnswer() { + return { type: 'answer', sdp: this.getLocalSdp() }; + } + + createOffer() { + return { type: 'offer', sdp: this.getLocalSdp() }; + } + + getLocalSdp() { this.wrtc.localDescription = new SessionDescription(this.wrtc.getLocalDescription()); const sdp = this.wrtc.localDescription.getSdp(this.sessionVersion); this.sessionVersion += 1; let message = sdp.toString(); message = message.replace(this.options.privateRegexp, this.options.publicIP); + return message; + } - const info = { type: this.options.createOffer || forceOffer ? 'offer' : 'answer', sdp: message }; - log.debug(`message: _maybeSendAnswer sending event, type: ${info.type}, streamId: ${streamId}`); + _maybeSendAnswer(evt, streamId, forceOffer = false) { + if (this.isNegotiating) { + return; + } + if (!this.alreadyGathered && !this.trickleIce) { + return; + } + const info = this.options.createOffer || forceOffer ? this.createOffer() : this.createAnswer(); + log.debug(`message: _maybeSendAnswer sending event, type: ${info.type}, streamId: ${streamId}, sessionVersion: ${this.sessionVersion}`); this.emit('status_event', info, evt, streamId); } @@ -143,7 +159,7 @@ class Connection extends events.EventEmitter { this.emit('status_event', info, evt, streamId); } - init(newStreamId) { + init(newStreamId, createOffer = this.options.createOffer) { if (this.initialized) { return false; } @@ -163,7 +179,7 @@ class Connection extends events.EventEmitter { break; case CONN_SDP_PROCESSED: - this.isProcessingRemoteSdp = false; + this.isNegotiating = false; this._maybeSendAnswer(newStatus, streamId); break; @@ -172,8 +188,9 @@ class Connection extends events.EventEmitter { break; case CONN_GATHERED: + this._gatheredResolveFunction(); this.alreadyGathered = true; - this._maybeSendAnswer(newStatus, firstStreamId); + this._maybeSendAnswer(newStatus, firstStreamId, createOffer); break; case CONN_CANDIDATE: @@ -197,42 +214,56 @@ class Connection extends events.EventEmitter { log.error(`message: unknown webrtc status ${newStatus}`); } }); - if (this.options.createOffer) { + if (createOffer) { log.debug('message: create offer requested, id:', this.id); - const audioEnabled = this.options.createOffer.audio; - const videoEnabled = this.options.createOffer.video; - const bundle = this.options.createOffer.bundle; - this.wrtc.createOffer(videoEnabled, audioEnabled, bundle); + const audioEnabled = createOffer.audio; + const videoEnabled = createOffer.video; + const bundle = createOffer.bundle; + this.createOfferPromise = this.wrtc.createOffer(videoEnabled, audioEnabled, bundle); } this.emit('status_event', { type: 'initializing' }); return true; } addMediaStream(id, options, isPublisher) { + let promise = Promise.resolve(); log.info(`message: addMediaStream, connectionId: ${this.id}, mediaStreamId: ${id}`); if (this.mediaStreams.get(id) === undefined) { const mediaStream = this._createMediaStream(id, options, isPublisher); - this.wrtc.addMediaStream(mediaStream); + promise = this.wrtc.addMediaStream(mediaStream); this.mediaStreams.set(id, mediaStream); } + return promise; } removeMediaStream(id) { + let promise = Promise.resolve(); if (this.mediaStreams.get(id) !== undefined) { const label = this.mediaStreams.get(id).label; - this.wrtc.removeMediaStream(id); + promise = this.wrtc.removeMediaStream(id); this.mediaStreams.get(id).close(); this.mediaStreams.delete(id); this._resendLastAnswer(CONN_SDP, id, label, true, true); } else { log.error(`message: Trying to remove mediaStream not found, id: ${id}`); } + return promise; } - setRemoteDescription(sdp, streamId) { - this.isProcessingRemoteSdp = true; + setRemoteDescription(sdp, streamIds) { this.remoteDescription = new SessionDescription(sdp, this.mediaConfiguration); - this.wrtc.setRemoteDescription(this.remoteDescription.connectionDescription, streamId); + this.wrtc.setRemoteDescription(this.remoteDescription.connectionDescription, streamIds); + } + + processOffer(sdp, streamsId) { + const sdpInfo = SemanticSdp.SDPInfo.processString(sdp); + this.setRemoteDescription(sdpInfo, streamsId); + } + + processAnswer(sdp, streamsId) { + this.isNegotiating = false; + const sdpInfo = SemanticSdp.SDPInfo.processString(sdp); + this.setRemoteDescription(sdpInfo, streamsId); } addRemoteCandidate(candidate) { diff --git a/erizo_controller/erizoJS/models/Node.js b/erizo_controller/erizoJS/models/Node.js index 0e18e6a181..6833d3bd30 100644 --- a/erizo_controller/erizoJS/models/Node.js +++ b/erizo_controller/erizoJS/models/Node.js @@ -40,11 +40,12 @@ class Node extends EventEmitter { this.emit(type, message); } - initMediaStream() { + initMediaStream(force = false) { if (!this.mediaStream) { return; } const mediaStream = this.mediaStream; + mediaStream.init(force); if (mediaStream.minVideoBW) { let monitorMinVideoBw = {}; if (mediaStream.scheme) { diff --git a/erizo_controller/erizoJS/models/Publisher.js b/erizo_controller/erizoJS/models/Publisher.js index 06220283dd..331b559483 100644 --- a/erizo_controller/erizoJS/models/Publisher.js +++ b/erizo_controller/erizoJS/models/Publisher.js @@ -158,7 +158,7 @@ class Source extends NodeClass { } if (msg.type === 'offer') { const sdp = SemanticSdp.SDPInfo.processString(msg.sdp); - connection.setRemoteDescription(sdp, this.streamId); + connection.setRemoteDescription(sdp, [this.streamId]); if (msg.config && msg.config.maxVideoBW) { this.mediaStream.setMaxVideoBW(msg.config.maxVideoBW); } @@ -168,7 +168,7 @@ class Source extends NodeClass { } else if (msg.type === 'updatestream') { if (msg.sdp) { const sdp = SemanticSdp.SDPInfo.processString(msg.sdp); - connection.setRemoteDescription(sdp, this.streamId); + connection.setRemoteDescription(sdp, [this.streamId]); if (this.mediaStream) { this.mediaStream.setMaxVideoBW(); } diff --git a/erizo_controller/erizoJS/models/Subscriber.js b/erizo_controller/erizoJS/models/Subscriber.js index 027879ffdb..0777f01c8a 100644 --- a/erizo_controller/erizoJS/models/Subscriber.js +++ b/erizo_controller/erizoJS/models/Subscriber.js @@ -13,7 +13,7 @@ class Subscriber extends NodeClass { super(clientId, streamId, options); this.connection = connection; this.connection.mediaConfiguration = options.mediaConfiguration; - this.connection.addMediaStream(this.erizoStreamId, options, false); + this.promise = this.connection.addMediaStream(this.erizoStreamId, options, false); this._connectionListener = this._emitStatusEvent.bind(this); this._mediaStreamListener = this._onMediaStreamEvent.bind(this); connection.on('status_event', this._connectionListener); @@ -54,6 +54,14 @@ class Subscriber extends NodeClass { this.emit('status_event', evt, status); } + copySdpInfoFromPublisher() { + if (this.publisher && this.publisher.connection && this.publisher.connection.wrtc && + this.publisher.connection.wrtc.localDescription && this.connection && this.connection.wrtc) { + const publisherSdp = this.publisher.connection.wrtc.localDescription.connectionDescription; + this.connection.wrtc.copySdpToLocalDescription(publisherSdp); + } + } + _onConnectionStatusEvent(connectionEvent) { if (connectionEvent.type === 'ready') { if (this.clientId && this.options.browser === 'bowser') { @@ -83,12 +91,17 @@ class Subscriber extends NodeClass { }); } - onSignalingMessage(msg, publisher) { + onSignalingMessage(msg, streamIds) { const connection = this.connection; if (msg.type === 'offer') { - const sdp = SemanticSdp.SDPInfo.processString(msg.sdp); - connection.setRemoteDescription(sdp, this.erizoStreamId); + connection.processOffer(msg.sdp, [this.erizoStreamId]); + if (msg.config && msg.config.maxVideoBW) { + this.mediaStream.setMaxVideoBW(msg.config.maxVideoBW); + } + this.disableDefaultHandlers(); + } else if (msg.type === 'answer') { + connection.processAnswer(msg.sdp, [this.erizoStreamId]); if (msg.config && msg.config.maxVideoBW) { this.mediaStream.setMaxVideoBW(msg.config.maxVideoBW); } @@ -98,7 +111,7 @@ class Subscriber extends NodeClass { } else if (msg.type === 'updatestream') { if (msg.sdp) { const sdp = SemanticSdp.SDPInfo.processString(msg.sdp); - connection.setRemoteDescription(sdp, this.erizoStreamId); + connection.setRemoteDescription(sdp, [this.erizoStreamId]); } if (msg.config) { if (msg.config.slideShowMode !== undefined) { @@ -122,21 +135,23 @@ class Subscriber extends NodeClass { } } } else if (msg.type === 'control') { - publisher.processControlMessage(this.clientId, msg.action); + this.publisher.processControlMessage(this.clientId, msg.action); } } close() { log.debug(`msg: Closing subscriber, streamId:${this.streamId}`); this.publisher = undefined; + let promise = Promise.resolve(); if (this.connection) { - this.connection.removeMediaStream(this.mediaStream.id); + promise = this.connection.removeMediaStream(this.mediaStream.id); this.connection.removeListener('status_event', this._connectionListener); this.connection.removeListener('media_stream_event', this._mediaStreamListener); } if (this.mediaStream && this.mediaStream.monitorInterval) { clearInterval(this.mediaStream.monitorInterval); } + return promise; } } diff --git a/extras/basic_example/basicServer.js b/extras/basic_example/basicServer.js index 09d2442f3c..9ebfbf5b6a 100644 --- a/extras/basic_example/basicServer.js +++ b/extras/basic_example/basicServer.js @@ -132,6 +132,9 @@ const cleanExampleRooms = (callback) => { deleteRoomsIfEmpty(roomsToCheck, () => { callback('done'); }); + }, (err) => { + console.log('Error cleaning example rooms', err); + setTimeout(cleanExampleRooms.bind(this, callback), 3000); }); }; diff --git a/extras/basic_example/public/script.js b/extras/basic_example/public/script.js index 048a90f395..071881704f 100644 --- a/extras/basic_example/public/script.js +++ b/extras/basic_example/public/script.js @@ -70,11 +70,13 @@ const startBasicExample = () => { const mediaConfiguration = getParameterByName('mediaConfiguration') || 'default'; const onlySubscribe = getParameterByName('onlySubscribe'); const onlyPublish = getParameterByName('onlyPublish'); + const autoSubscribe = getParameterByName('autoSubscribe'); console.log('Selected Room', roomName, 'of type', roomType); const config = { audio: true, video: !audioOnly, data: true, screen, + attributes: {}, videoSize: [640, 480, 640, 480], videoFrameRate: [10, 20] }; // If we want screen sharing we have to put our Chrome extension id. @@ -111,6 +113,9 @@ const startBasicExample = () => { room = Erizo.Room({ token }); const subscribeToStreams = (streams) => { + if (autoSubscribe) { + return; + } if (onlyPublish) { return; } @@ -134,6 +139,9 @@ const startBasicExample = () => { if (!onlySubscribe) { room.publish(localStream, options); } + if (autoSubscribe) { + room.autoSubscribe({ '/attributes/type': 'publisher' }, { audio: true, video: true, data: false }, (result) => {}); + } subscribeToStreams(roomEvent.streams); }); @@ -150,6 +158,7 @@ const startBasicExample = () => { room.addEventListener('stream-added', (streamEvent) => { const streams = []; streams.push(streamEvent.stream); + localStream && localStream.setAttributes({ type: 'publisher' }); subscribeToStreams(streams); document.getElementById('recordButton').disabled = false; }); From 8d5a44dc544c1cd795abd8e9fcab7089d6b969e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Fri, 15 Feb 2019 15:41:04 +0100 Subject: [PATCH 04/20] fix gcc errors when building erizo --- erizo/src/erizo/WebRtcConnection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erizo/src/erizo/WebRtcConnection.cpp b/erizo/src/erizo/WebRtcConnection.cpp index bd79701bdf..bcf8067311 100644 --- a/erizo/src/erizo/WebRtcConnection.cpp +++ b/erizo/src/erizo/WebRtcConnection.cpp @@ -222,8 +222,8 @@ boost::future WebRtcConnection::forEachMediaStreamAsync(std::function> p = std::make_shared>(); - return boost::when_all(futures.begin(), futures.end()) - .then([p](boost::future, std::__1::allocator>>>) { + auto f = boost::when_all(futures.begin(), futures.end()); + f.then([p](decltype(f)) { p->set_value(); }); return p->get_future(); From 496ce256d574cd4ea740280271eb93ca4e6e7b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Fri, 15 Feb 2019 16:17:49 +0100 Subject: [PATCH 05/20] fix lint --- erizo/src/erizo/MediaStream.cpp | 3 ++- erizo/src/erizo/SdpInfo.cpp | 6 ++++-- erizo/src/erizo/WebRtcConnection.cpp | 15 ++++++++++----- erizo/src/erizo/rtp/FecReceiverHandler.cpp | 3 ++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/erizo/src/erizo/MediaStream.cpp b/erizo/src/erizo/MediaStream.cpp index 0d9878247e..d0e6886cc9 100644 --- a/erizo/src/erizo/MediaStream.cpp +++ b/erizo/src/erizo/MediaStream.cpp @@ -160,7 +160,8 @@ bool MediaStream::isSinkSSRC(uint32_t ssrc) { } bool MediaStream::setRemoteSdp(std::shared_ptr sdp) { - ELOG_DEBUG("%s message: setting remote SDP to Stream, sending: %d, initialized: %d", toLog(), sending_, pipeline_initialized_); + ELOG_DEBUG("%s message: setting remote SDP to Stream, sending: %d, initialized: %d", + toLog(), sending_, pipeline_initialized_); if (!sending_) { return true; } diff --git a/erizo/src/erizo/SdpInfo.cpp b/erizo/src/erizo/SdpInfo.cpp index 8a87249fc9..1d324c968c 100644 --- a/erizo/src/erizo/SdpInfo.cpp +++ b/erizo/src/erizo/SdpInfo.cpp @@ -473,7 +473,8 @@ namespace erizo { // TODO(pedro): Should provide hints void SdpInfo::createOfferSdp(bool videoEnabled, bool audioEnabled, bool bundle) { - ELOG_DEBUG("Creating offerSDP: video %d, audio %d, bundle %d, payloadVector: %d, extSize: %d", videoEnabled, audioEnabled, bundle, payloadVector.size(), extMapVector.size()); + ELOG_DEBUG("Creating offerSDP: video %d, audio %d, bundle %d, payloadVector: %d, extSize: %d", + videoEnabled, audioEnabled, bundle, payloadVector.size(), extMapVector.size()); if (payloadVector.size() == 0) { payloadVector = internalPayloadVector_; } @@ -509,7 +510,8 @@ namespace erizo { inOutPTMap = offerSdp->inOutPTMap; outInPTMap = offerSdp->outInPTMap; extMapVector = offerSdp->extMapVector; - ELOG_DEBUG("Offer SDP successfully copied, extSize: %d, payloadSize: %d, videoCodecs: %d, audioCodecs: %d", extMapVector.size(), payloadVector.size(), videoCodecs, audioCodecs); + ELOG_DEBUG("Offer SDP successfully copied, extSize: %d, payloadSize: %d, videoCodecs: %d, audioCodecs: %d", + extMapVector.size(), payloadVector.size(), videoCodecs, audioCodecs); } void SdpInfo::setOfferSdp(std::shared_ptr offerSdp) { diff --git a/erizo/src/erizo/WebRtcConnection.cpp b/erizo/src/erizo/WebRtcConnection.cpp index bcf8067311..6214aee998 100644 --- a/erizo/src/erizo/WebRtcConnection.cpp +++ b/erizo/src/erizo/WebRtcConnection.cpp @@ -120,7 +120,8 @@ bool WebRtcConnection::createOfferSync(bool video_enabled, bool audio_enabled, b local_sdp_->internal_dtls_role = PASSIVE; } - ELOG_DEBUG("%s message: Creating sdp offer, isBundle: %d, setup: %d", toLog(), bundle_, local_sdp_->internal_dtls_role); + ELOG_DEBUG("%s message: Creating sdp offer, isBundle: %d, setup: %d", + toLog(), bundle_, local_sdp_->internal_dtls_role); forEachMediaStream([this] (const std::shared_ptr &media_stream) { if (!media_stream->isReady() || media_stream->isPublisher()) { @@ -210,7 +211,8 @@ void WebRtcConnection::forEachMediaStream(std::function WebRtcConnection::forEachMediaStreamAsync(std::function&)> func) { +boost::future WebRtcConnection::forEachMediaStreamAsync( + std::function&)> func) { std::vector> futures; std::for_each(media_streams_.begin(), media_streams_.end(), [func, &futures] (const std::shared_ptr &stream) { @@ -229,7 +231,8 @@ boost::future WebRtcConnection::forEachMediaStreamAsync(std::functionget_future(); } -boost::future WebRtcConnection::setRemoteSdpInfo(std::shared_ptr sdp, std::vector stream_ids) { +boost::future WebRtcConnection::setRemoteSdpInfo( + std::shared_ptr sdp, std::vector stream_ids) { std::shared_ptr> p = std::make_shared>(); boost::future f = p->get_future(); asyncTask([sdp, stream_ids, p] (std::shared_ptr connection) { @@ -327,7 +330,8 @@ boost::future WebRtcConnection::setRemoteSdpsToMediaStreams(std::vector media_stream) { if (auto connection = weak_this.lock()) { media_stream->setRemoteSdp(connection->remote_sdp_); - ELOG_DEBUG("%s message: setting remote SDP to stream, stream: %s", connection->toLog(), media_stream->getId()); + ELOG_DEBUG("%s message: setting remote SDP to stream, stream: %s", + connection->toLog(), media_stream->getId()); auto stream_it = std::find(stream_ids.begin(), stream_ids.end(), media_stream->getId()); if (stream_it != stream_ids.end()) { connection->onRemoteSdpsSetToMediaStreams(media_stream->getId()); @@ -587,7 +591,8 @@ void WebRtcConnection::maybeNotifyWebRtcConnectionEvent(const WebRTCEvent& event conn_event_listener_->notifyEvent(event, message, stream_id); } -std::shared_ptr> WebRtcConnection::asyncTask(std::function)> f) { +std::shared_ptr> WebRtcConnection::asyncTask( + std::function)> f) { auto task_promise = std::make_shared>(); std::weak_ptr weak_this = shared_from_this(); worker_->task([weak_this, f, task_promise] { diff --git a/erizo/src/erizo/rtp/FecReceiverHandler.cpp b/erizo/src/erizo/rtp/FecReceiverHandler.cpp index 84bf5656e2..1f1ece54fa 100644 --- a/erizo/src/erizo/rtp/FecReceiverHandler.cpp +++ b/erizo/src/erizo/rtp/FecReceiverHandler.cpp @@ -33,7 +33,8 @@ void FecReceiverHandler::notifyUpdate() { return; } bool is_slide_show_mode_active = stream->isSlideShowModeEnabled(); - if ((stream->getRemoteSdpInfo() && !stream->getRemoteSdpInfo()->supportPayloadType(RED_90000_PT)) || is_slide_show_mode_active) { + if ((stream->getRemoteSdpInfo() && !stream->getRemoteSdpInfo()->supportPayloadType(RED_90000_PT)) || + is_slide_show_mode_active) { enable(); } else { disable(); From dc066693355f10a71a66ede931f8a85354cb6e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 18 Feb 2019 09:50:47 +0100 Subject: [PATCH 06/20] Fix js lint --- erizo_controller/erizoController/models/Client.js | 4 +++- erizo_controller/erizoController/roomController.js | 2 +- erizo_controller/erizoJS/erizoJSController.js | 2 +- erizo_controller/erizoJS/models/Subscriber.js | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erizo_controller/erizoController/models/Client.js b/erizo_controller/erizoController/models/Client.js index 0687d69665..bc4f759526 100644 --- a/erizo_controller/erizoController/models/Client.js +++ b/erizo_controller/erizoController/models/Client.js @@ -242,7 +242,9 @@ class Client extends events.EventEmitter { const targetClient = this.room.getClientById(message.peerSocket); if (targetClient) { targetClient.sendMessage('signaling_message_peer', - { streamId:message.streamIds || message.streamId, peerSocket: this.id, msg: message.msg }); + { streamId: message.streamIds || message.streamId, + peerSocket: this.id, + msg: message.msg }); } } else { const isControlMessage = message.msg.type === 'control'; diff --git a/erizo_controller/erizoController/roomController.js b/erizo_controller/erizoController/roomController.js index 50bf588940..c23bf9c0a4 100644 --- a/erizo_controller/erizoController/roomController.js +++ b/erizo_controller/erizoController/roomController.js @@ -337,7 +337,7 @@ exports.RoomController = (spec) => { return; } else if (data.type === 'initializing') { if (subscribers[streamId].indexOf(clientId) === -1) { - subscribers[streamId].push(clientId); + subscribers[streamId].push(clientId); } } log.info('message: addSubscriber finished, ' + diff --git a/erizo_controller/erizoJS/erizoJSController.js b/erizo_controller/erizoJS/erizoJSController.js index 57896fd0c5..ad5bdf65de 100644 --- a/erizo_controller/erizoJS/erizoJSController.js +++ b/erizo_controller/erizoJS/erizoJSController.js @@ -294,7 +294,7 @@ exports.ErizoJSController = (threadPool, ioThreadPool) => { } Promise.all(promises) .then(() => { - log.debug(`message: autoSubscription waiting for gathering event`, connection.alreadyGathered, connection.gatheredPromise); + log.debug('message: autoSubscription waiting for gathering event', connection.alreadyGathered, connection.gatheredPromise); return connection.gatheredPromise; }) .then(() => { diff --git a/erizo_controller/erizoJS/models/Subscriber.js b/erizo_controller/erizoJS/models/Subscriber.js index 0777f01c8a..71012eba98 100644 --- a/erizo_controller/erizoJS/models/Subscriber.js +++ b/erizo_controller/erizoJS/models/Subscriber.js @@ -91,7 +91,7 @@ class Subscriber extends NodeClass { }); } - onSignalingMessage(msg, streamIds) { + onSignalingMessage(msg) { const connection = this.connection; if (msg.type === 'offer') { From 4242cc38ab8843c9289627541be0cf437194e47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 18 Feb 2019 11:16:56 +0100 Subject: [PATCH 07/20] Fix lint for basic example --- extras/basic_example/public/script.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extras/basic_example/public/script.js b/extras/basic_example/public/script.js index 071881704f..06a46484aa 100644 --- a/extras/basic_example/public/script.js +++ b/extras/basic_example/public/script.js @@ -140,7 +140,7 @@ const startBasicExample = () => { room.publish(localStream, options); } if (autoSubscribe) { - room.autoSubscribe({ '/attributes/type': 'publisher' }, { audio: true, video: true, data: false }, (result) => {}); + room.autoSubscribe({ '/attributes/type': 'publisher' }, { audio: true, video: true, data: false }, () => {}); } subscribeToStreams(roomEvent.streams); }); @@ -158,7 +158,9 @@ const startBasicExample = () => { room.addEventListener('stream-added', (streamEvent) => { const streams = []; streams.push(streamEvent.stream); - localStream && localStream.setAttributes({ type: 'publisher' }); + if (localStream) { + localStream.setAttributes({ type: 'publisher' }); + } subscribeToStreams(streams); document.getElementById('recordButton').disabled = false; }); From f059e66f2053da2d534bd9c053b18eeb3918b371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 18 Feb 2019 12:35:37 +0100 Subject: [PATCH 08/20] Fix Unit Tests --- erizo_controller/test/utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erizo_controller/test/utils.js b/erizo_controller/test/utils.js index cd887fb996..25c85acbc2 100644 --- a/erizo_controller/test/utils.js +++ b/erizo_controller/test/utils.js @@ -169,6 +169,7 @@ module.exports.reset = () => { scheme: '', periodicPlis: '', close: sinon.stub(), + init: sinon.stub(), setAudioReceiver: sinon.stub(), setVideoReceiver: sinon.stub(), setMaxVideoBW: sinon.stub(), From e32caeb6b74399b01452b95932af3e4234931778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 18 Feb 2019 15:00:28 +0100 Subject: [PATCH 09/20] Add tests for roomController --- .../test/erizoController/roomController.js | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/erizo_controller/test/erizoController/roomController.js b/erizo_controller/test/erizoController/roomController.js index 69698b7ae6..ab5796ef4f 100644 --- a/erizo_controller/test/erizoController/roomController.js +++ b/erizo_controller/test/erizoController/roomController.js @@ -252,7 +252,7 @@ describe('Erizo Controller / Room Controller', () => { expect(callback.callCount).to.equal(1); }); - it('should return error on Publisher timeout', () => { + it('should return error on Subscriber timeout', () => { const callback = sinon.stub(); controller.addSubscriber(kArbitraryClientId, kArbitraryStreamId, @@ -278,7 +278,7 @@ describe('Erizo Controller / Room Controller', () => { expect(callback.args[0][0]).to.equal('Error: null clientId'); }); - it('should fail if Publisher does not exist', () => { + it('should fail if Subscriber does not exist', () => { const kArbitraryUnknownId = 'unknownId'; const callback = sinon.stub(); @@ -324,4 +324,95 @@ describe('Erizo Controller / Room Controller', () => { }); }); }); + + describe('Add Multiple Subscribers', () => { + const kArbitraryClientId = 'id1'; + const kArbitraryOptions = {}; + const kArbitraryStreamId = 'id2'; + const kArbitraryPubOptions = {}; + + beforeEach(() => { + ecchInstanceMock.getErizoJS.callsArgWith(2, 'erizoId'); + controller.addPublisher(kArbitraryClientId, kArbitraryStreamId, + kArbitraryPubOptions, sinon.stub()); + }); + + it('should call Erizo\'s addMultipleSubscribers', () => { + const callback = sinon.stub(); + + controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], + kArbitraryOptions, callback); + expect(amqperMock.callRpc.callCount).to.equal(2); + expect(amqperMock.callRpc.args[1][1]).to.equal('addMultipleSubscribers'); + + amqperMock.callRpc.args[1][3].callback({ type: 'initializing' }); + + expect(callback.callCount).to.equal(1); + }); + + it('should return error on Subscriber timeout', () => { + const callback = sinon.stub(); + + controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], + kArbitraryOptions, callback); + + expect(amqperMock.callRpc.callCount).to.equal(2); + expect(amqperMock.callRpc.args[1][1]).to.equal('addMultipleSubscribers'); + + amqperMock.callRpc.args[1][3].callback('timeout'); + amqperMock.callRpc.args[2][3].callback('timeout'); // First retry + amqperMock.callRpc.args[3][3].callback('timeout'); // Second retry + amqperMock.callRpc.args[4][3].callback('timeout'); // Third retry + + expect(callback.callCount).to.equal(1); + expect(callback.args[0][0]).to.equal('timeout'); + }); + + it('should fail if clientId is null', () => { + const callback = sinon.stub(); + + controller.addMultipleSubscribers(null, [kArbitraryStreamId], kArbitraryOptions, callback); + expect(amqperMock.callRpc.callCount).to.equal(1); + expect(callback.args[0][0]).to.equal('Error: null clientId'); + }); + + it('should fail if Subscriber does not exist', () => { + const kArbitraryUnknownId = 'unknownId'; + const callback = sinon.stub(); + + controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryUnknownId], + kArbitraryOptions, callback); + expect(amqperMock.callRpc.callCount).to.equal(1); + }); + + describe('And Remove', () => { + beforeEach(() => { + controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], + kArbitraryOptions, sinon.stub()); + + amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [kArbitraryStreamId] }); + }); + + it('should call Erizo\'s removeMultipleSubscribers', () => { + controller.removeMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId]); + + expect(amqperMock.callRpc.callCount).to.equal(3); + expect(amqperMock.callRpc.args[2][1]).to.equal('removeMultipleSubscribers'); + }); + + it('should fail if clientId does not exist', () => { + const kArbitraryUnknownId = 'unknownId'; + controller.removeMultipleSubscribers(kArbitraryUnknownId, [kArbitraryStreamId]); + + expect(amqperMock.callRpc.callCount).to.equal(2); + }); + + it('should fail if subscriberId does not exist', () => { + const kArbitraryUnknownId = 'unknownId'; + controller.removeMultipleSubscribers(kArbitraryClientId, [kArbitraryUnknownId]); + + expect(amqperMock.callRpc.callCount).to.equal(2); + }); + }); + }); }); From 7b97cd28acba11f2420deee6e4380465e32ae73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Wed, 20 Feb 2019 00:49:34 +0100 Subject: [PATCH 10/20] Add more unit tests --- .../erizoController/models/Client.js | 6 +- .../test/erizoController/erizoController.js | 92 ++++++++++++++++++- .../test/erizoController/roomController.js | 64 +++++++++++-- erizo_controller/test/utils.js | 2 + 4 files changed, 154 insertions(+), 10 deletions(-) diff --git a/erizo_controller/erizoController/models/Client.js b/erizo_controller/erizoController/models/Client.js index bc4f759526..2044fb2754 100644 --- a/erizo_controller/erizoController/models/Client.js +++ b/erizo_controller/erizoController/models/Client.js @@ -74,9 +74,9 @@ class Client extends events.EventEmitter { const unsubscribableStreams = []; this.room.forEachStream((stream) => { // We don't subscribe/unsubscribe to own published - if (this.streams.indexOf(stream.getID()) !== -1) { - return; - } + // if (this.streams.indexOf(stream.getID()) !== -1) { + // return; + // } if (stream.meetAnySelector(this.selectors)) { if (stream.hasData() && this.options.data !== false) { stream.addDataSubscriber(this.id); diff --git a/erizo_controller/test/erizoController/erizoController.js b/erizo_controller/test/erizoController/erizoController.js index 2f9e43fbf3..81ccba39dc 100644 --- a/erizo_controller/test/erizoController/erizoController.js +++ b/erizo_controller/test/erizoController/erizoController.js @@ -95,6 +95,7 @@ describe('Erizo Controller / Erizo Controller', () => { let onUpdateStreamAttributes; let onPublish; let onSubscribe; + let onAutoSubscribe; let onStartRecorder; let onStopRecorder; let onUnpublish; @@ -241,6 +242,7 @@ describe('Erizo Controller / Erizo Controller', () => { onStopRecorder = mocks.socketInstance.on.withArgs('stopRecorder').args[0][1]; onUnpublish = mocks.socketInstance.on.withArgs('unpublish').args[0][1]; onUnsubscribe = mocks.socketInstance.on.withArgs('unsubscribe').args[0][1]; + onAutoSubscribe = mocks.socketInstance.on.withArgs('autoSubscribe').args[0][1]; done(); }, 0); }); @@ -252,6 +254,7 @@ describe('Erizo Controller / Erizo Controller', () => { .to.equal(1); expect(mocks.socketInstance.on.withArgs('publish').callCount).to.equal(1); expect(mocks.socketInstance.on.withArgs('subscribe').callCount).to.equal(1); + expect(mocks.socketInstance.on.withArgs('autoSubscribe').callCount).to.equal(1); expect(mocks.socketInstance.on.withArgs('startRecorder').callCount).to.equal(1); expect(mocks.socketInstance.on.withArgs('stopRecorder').callCount).to.equal(1); expect(mocks.socketInstance.on.withArgs('unpublish').callCount).to.equal(1); @@ -346,6 +349,7 @@ describe('Erizo Controller / Erizo Controller', () => { onStopRecorder = mocks.socketInstance.on.withArgs('stopRecorder').args[0][1]; onUnpublish = mocks.socketInstance.on.withArgs('unpublish').args[0][1]; onUnsubscribe = mocks.socketInstance.on.withArgs('unsubscribe').args[0][1]; + onAutoSubscribe = mocks.socketInstance.on.withArgs('autoSubscribe').args[0][1]; done(); }, 0); }); @@ -356,7 +360,8 @@ describe('Erizo Controller / Erizo Controller', () => { expect(mocks.socketInstance.on.withArgs('updateStreamAttributes').callCount) .to.equal(1); expect(mocks.socketInstance.on.withArgs('publish').callCount).to.equal(1); - expect(mocks.socketInstance.on.withArgs('subscribe').callCount).to.equal(1); + expect(mocks.socketInstance.on.withArgs('autoSubscribe').callCount).to.equal(1); + expect(mocks.socketInstance.on.withArgs('unsubscribe').callCount).to.equal(1); expect(mocks.socketInstance.on.withArgs('startRecorder').callCount).to.equal(1); expect(mocks.socketInstance.on.withArgs('stopRecorder').callCount).to.equal(1); expect(mocks.socketInstance.on.withArgs('unpublish').callCount).to.equal(1); @@ -506,6 +511,91 @@ describe('Erizo Controller / Erizo Controller', () => { .callCount).to.equal(1); }); + describe('on AutoSubscription', () => { + let data; + const streams = []; + let streamId; + beforeEach(() => { + client.user.permissions = {}; + client.user.permissions[Permission.SUBSCRIBE] = true; + client.user.permissions[Permission.PUBLISH] = true; + + const aOptions = { + audio: true, + video: true, + screen: true, + data: true, + attributes: { type: 'publisher' } }; + const aSdp = ''; + const publishCallback = sinon.stub(); + + onPublish(aOptions, aSdp, publishCallback); + + streamId = publishCallback.args[0][0]; + + data = { + selectors: { '/attributes/type': 'publisher' }, + options: { audio: true, video: true }, + }; + streams.push(streamId); + }); + + it('should call callback', () => { + const subscribeCallback = sinon.stub(); + onAutoSubscribe(data, subscribeCallback); + expect(subscribeCallback.callCount).to.equal(1); + }); + + it('should fail if user is not authorized to subscribe', () => { + const subscribeCallback = sinon.stub(); + client.user.permissions[Permission.SUBSCRIBE] = false; + onAutoSubscribe(data, subscribeCallback); + expect(subscribeCallback.withArgs(null, 'Unauthorized').callCount).to.equal(1); + }); + + it('should call RoomController if any stream meets selector', () => { + const subscribeCallback = sinon.stub(); + data.selectors = { '/attributes/type': 'publisher' }; + onAutoSubscribe(data, subscribeCallback); + expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount) + .to.equal(1); + expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) + .to.equal(0); + }); + + it('should call RoomController any time a stream meets selector', () => { + const subscribeCallback = sinon.stub(); + data.selectors = { '/attributes/type': 'publisher' }; + onAutoSubscribe(data, subscribeCallback); + onAutoSubscribe(data, subscribeCallback); + onAutoSubscribe(data, subscribeCallback); + expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount) + .to.equal(3); + expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) + .to.equal(0); + }); + + it('should not call RoomController if no stream meets a selector', () => { + const subscribeCallback = sinon.stub(); + data.selectors = { '/attributes/type': 'subscriber' }; + onAutoSubscribe(data, subscribeCallback); + expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount).to.equal(0); + expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) + .to.equal(1); + }); + + it('should call RoomController to remove multiple subscribers', () => { + const subscribeCallback = sinon.stub(); + data.selectors = { '/attributes/type': 'publisher' }; + onAutoSubscribe(data, subscribeCallback); + data.selectors = { '/attributes/type': 'subscriber' }; + onAutoSubscribe(data, subscribeCallback); + expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount).to.equal(1); + expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) + .to.equal(1); + }); + }); + describe('on Subscribe', () => { let subscriberOptions; let subscriberSdp; diff --git a/erizo_controller/test/erizoController/roomController.js b/erizo_controller/test/erizoController/roomController.js index ab5796ef4f..bf7fc671f1 100644 --- a/erizo_controller/test/erizoController/roomController.js +++ b/erizo_controller/test/erizoController/roomController.js @@ -350,7 +350,7 @@ describe('Erizo Controller / Room Controller', () => { expect(callback.callCount).to.equal(1); }); - it('should return error on Subscriber timeout', () => { + it('should call Erizo\'s addMultipleSubscribers only once', () => { const callback = sinon.stub(); controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], @@ -359,13 +359,65 @@ describe('Erizo Controller / Room Controller', () => { expect(amqperMock.callRpc.callCount).to.equal(2); expect(amqperMock.callRpc.args[1][1]).to.equal('addMultipleSubscribers'); - amqperMock.callRpc.args[1][3].callback('timeout'); - amqperMock.callRpc.args[2][3].callback('timeout'); // First retry - amqperMock.callRpc.args[3][3].callback('timeout'); // Second retry - amqperMock.callRpc.args[4][3].callback('timeout'); // Third retry + amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [kArbitraryStreamId] }); + + controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], + kArbitraryOptions, callback); + expect(amqperMock.callRpc.callCount).to.equal(2); expect(callback.callCount).to.equal(1); - expect(callback.args[0][0]).to.equal('timeout'); + }); + + it('should call Erizo\'s addMultipleSubscribers multiple times if previous failed', () => { + const callback = sinon.stub(); + + controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], + kArbitraryOptions, callback); + + expect(amqperMock.callRpc.callCount).to.equal(2); + expect(amqperMock.callRpc.args[1][1]).to.equal('addMultipleSubscribers'); + + amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [] }); + + controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], + kArbitraryOptions, callback); + expect(amqperMock.callRpc.callCount).to.equal(3); + expect(amqperMock.callRpc.args[2][1]).to.equal('addMultipleSubscribers'); + + expect(callback.callCount).to.equal(1); + }); + + it('should call Erizo\'s removeMultipleSubscribers', () => { + const callback = sinon.stub(); + + controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], + kArbitraryOptions, callback); + amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [kArbitraryStreamId] }); + + controller.removeMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], callback); + expect(amqperMock.callRpc.callCount).to.equal(3); + expect(amqperMock.callRpc.args[2][1]).to.equal('removeMultipleSubscribers'); + + expect(callback.callCount).to.equal(1); + }); + + it('should call Erizo\'s removeMultipleSubscribers only once', () => { + const callback = sinon.stub(); + + controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], + kArbitraryOptions, callback); + amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [kArbitraryStreamId] }); + + controller.removeMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], callback); + + expect(amqperMock.callRpc.callCount).to.equal(3); + expect(amqperMock.callRpc.args[2][1]).to.equal('removeMultipleSubscribers'); + amqperMock.callRpc.args[2][3].callback(); + + controller.removeMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], callback); + expect(amqperMock.callRpc.callCount).to.equal(3); + + expect(callback.callCount).to.equal(2); }); it('should fail if clientId is null', () => { diff --git a/erizo_controller/test/utils.js b/erizo_controller/test/utils.js index 25c85acbc2..670088a12e 100644 --- a/erizo_controller/test/utils.js +++ b/erizo_controller/test/utils.js @@ -207,6 +207,8 @@ module.exports.reset = () => { addExternalOutput: sinon.stub(), processSignaling: sinon.stub(), addPublisher: sinon.stub(), + addMultipleSubscribers: sinon.stub(), + removeMultipleSubscribers: sinon.stub(), addSubscriber: sinon.stub(), removePublisher: sinon.stub(), removeSubscriber: sinon.stub(), From 8a8b0a46fdf47e8a189d1a6644c6f80723fbcadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Wed, 20 Feb 2019 13:46:29 +0100 Subject: [PATCH 11/20] Use a better name for parameter --- erizo/src/erizo/MediaStream.cpp | 4 ++-- erizo/src/erizo/MediaStream.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erizo/src/erizo/MediaStream.cpp b/erizo/src/erizo/MediaStream.cpp index d0e6886cc9..5b6a5a5684 100644 --- a/erizo/src/erizo/MediaStream.cpp +++ b/erizo/src/erizo/MediaStream.cpp @@ -144,8 +144,8 @@ void MediaStream::close() { }); } -bool MediaStream::init(bool force) { - if (force) { +bool MediaStream::init(bool doNotWaitForRemoteSdp) { + if (doNotWaitForRemoteSdp) { ready_ = true; } return true; diff --git a/erizo/src/erizo/MediaStream.h b/erizo/src/erizo/MediaStream.h index 5ad04f26b0..2c400e6ce9 100644 --- a/erizo/src/erizo/MediaStream.h +++ b/erizo/src/erizo/MediaStream.h @@ -68,7 +68,7 @@ class MediaStream: public MediaSink, public MediaSource, public FeedbackSink, * Destructor. */ virtual ~MediaStream(); - bool init(bool force); + bool init(bool doNotWaitForRemoteSdp); void close() override; virtual uint32_t getMaxVideoBW(); virtual uint32_t getBitrateFromMaxQualityLayer() { return bitrate_from_max_quality_layer_; } From f70bb4a05d4c1e1a397b535f3ed45b6fe5942f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Fri, 1 Mar 2019 12:04:08 +0100 Subject: [PATCH 12/20] Fix normal subscriptions and avoid own auto subscriptions --- erizo/src/erizo/MediaStream.cpp | 27 ++- .../erizoController/models/Client.js | 6 +- .../test/erizoController/erizoController.js | 197 +++++++++++------- erizo_controller/test/utils.js | 4 +- 4 files changed, 150 insertions(+), 84 deletions(-) diff --git a/erizo/src/erizo/MediaStream.cpp b/erizo/src/erizo/MediaStream.cpp index 5b6a5a5684..76bb03737f 100644 --- a/erizo/src/erizo/MediaStream.cpp +++ b/erizo/src/erizo/MediaStream.cpp @@ -165,7 +165,26 @@ bool MediaStream::setRemoteSdp(std::shared_ptr sdp) { if (!sending_) { return true; } - remote_sdp_ = std::make_shared(*sdp.get()); + + std::shared_ptr remote_sdp = std::make_shared(*sdp.get()); + auto video_ssrc_list_it = remote_sdp->video_ssrc_map.find(getLabel()); + auto audio_ssrc_it = remote_sdp->audio_ssrc_map.find(getLabel()); + + if (isPublisher() && !ready_) { + bool stream_found = false; + + if (video_ssrc_list_it != remote_sdp->video_ssrc_map.end() || + audio_ssrc_it != remote_sdp->audio_ssrc_map.end()) { + stream_found = true; + } + + if (!stream_found && isPublisher()) { + return true; + } + } + + remote_sdp_ = remote_sdp; + if (remote_sdp_->videoBandwidth != 0) { ELOG_DEBUG("%s message: Setting remote BW, maxVideoBW: %u", toLog(), remote_sdp_->videoBandwidth); this->rtcp_processor_->setMaxVideoBW(remote_sdp_->videoBandwidth*1000); @@ -179,12 +198,10 @@ bool MediaStream::setRemoteSdp(std::shared_ptr sdp) { } bundle_ = remote_sdp_->isBundle; - auto video_ssrc_list_it = remote_sdp_->video_ssrc_map.find(getLabel()); if (video_ssrc_list_it != remote_sdp_->video_ssrc_map.end()) { setVideoSourceSSRCList(video_ssrc_list_it->second); } - auto audio_ssrc_it = remote_sdp_->audio_ssrc_map.find(getLabel()); if (audio_ssrc_it != remote_sdp_->audio_ssrc_map.end()) { setAudioSourceSSRC(audio_ssrc_it->second); } @@ -390,6 +407,7 @@ void MediaStream::initializePipeline() { pipeline_->addFront(std::make_shared(this)); pipeline_->finalize(); + ELOG_WARN("%s message: Pipeline initialized, id: %s", toLog(), getId()); pipeline_initialized_ = true; } @@ -462,7 +480,7 @@ void MediaStream::onTransportData(std::shared_ptr incoming_packet, T worker_->task([stream_ptr, packet]{ if (!stream_ptr->pipeline_initialized_) { - ELOG_DEBUG("%s message: Pipeline not initialized yet.", stream_ptr->toLog()); + ELOG_WARN("%s message: Pipeline not initialized yet.", stream_ptr->toLog()); return; } @@ -479,6 +497,7 @@ void MediaStream::onTransportData(std::shared_ptr incoming_packet, T } if (stream_ptr->pipeline_) { + // ELOG_WARN("%s message: New packet!!", stream_ptr->toLog()); stream_ptr->pipeline_->read(std::move(packet)); } }); diff --git a/erizo_controller/erizoController/models/Client.js b/erizo_controller/erizoController/models/Client.js index 2044fb2754..bc4f759526 100644 --- a/erizo_controller/erizoController/models/Client.js +++ b/erizo_controller/erizoController/models/Client.js @@ -74,9 +74,9 @@ class Client extends events.EventEmitter { const unsubscribableStreams = []; this.room.forEachStream((stream) => { // We don't subscribe/unsubscribe to own published - // if (this.streams.indexOf(stream.getID()) !== -1) { - // return; - // } + if (this.streams.indexOf(stream.getID()) !== -1) { + return; + } if (stream.meetAnySelector(this.selectors)) { if (stream.hasData() && this.options.data !== false) { stream.addDataSubscriber(this.id); diff --git a/erizo_controller/test/erizoController/erizoController.js b/erizo_controller/test/erizoController/erizoController.js index 81ccba39dc..12361c2254 100644 --- a/erizo_controller/test/erizoController/erizoController.js +++ b/erizo_controller/test/erizoController/erizoController.js @@ -95,7 +95,6 @@ describe('Erizo Controller / Erizo Controller', () => { let onUpdateStreamAttributes; let onPublish; let onSubscribe; - let onAutoSubscribe; let onStartRecorder; let onStopRecorder; let onUnpublish; @@ -242,7 +241,6 @@ describe('Erizo Controller / Erizo Controller', () => { onStopRecorder = mocks.socketInstance.on.withArgs('stopRecorder').args[0][1]; onUnpublish = mocks.socketInstance.on.withArgs('unpublish').args[0][1]; onUnsubscribe = mocks.socketInstance.on.withArgs('unsubscribe').args[0][1]; - onAutoSubscribe = mocks.socketInstance.on.withArgs('autoSubscribe').args[0][1]; done(); }, 0); }); @@ -349,7 +347,6 @@ describe('Erizo Controller / Erizo Controller', () => { onStopRecorder = mocks.socketInstance.on.withArgs('stopRecorder').args[0][1]; onUnpublish = mocks.socketInstance.on.withArgs('unpublish').args[0][1]; onUnsubscribe = mocks.socketInstance.on.withArgs('unsubscribe').args[0][1]; - onAutoSubscribe = mocks.socketInstance.on.withArgs('autoSubscribe').args[0][1]; done(); }, 0); }); @@ -511,88 +508,136 @@ describe('Erizo Controller / Erizo Controller', () => { .callCount).to.equal(1); }); - describe('on AutoSubscription', () => { - let data; - const streams = []; - let streamId; - beforeEach(() => { - client.user.permissions = {}; - client.user.permissions[Permission.SUBSCRIBE] = true; - client.user.permissions[Permission.PUBLISH] = true; - - const aOptions = { - audio: true, - video: true, - screen: true, - data: true, - attributes: { type: 'publisher' } }; - const aSdp = ''; - const publishCallback = sinon.stub(); + describe('Subscriber Connection', () => { + // eslint-disable-next-line no-unused-vars + let onSubscriberReconnect; + let onSubscriberAutoSubscribe; + let onSubscriberTokenCallback; + let onSubscriberToken; + let subscriberClient; + const arbitrarySubscriberSignature = 'c2lnbmF0dXJl'; // signature + const arbitrarySubscriberGoodToken = { + tokenId: 'tokenId2', + host: 'host', + signature: arbitrarySubscriberSignature, + }; + + beforeEach((done) => { + const onConnection = mocks.socketIoInstance.sockets.on.withArgs('connection').args[0][1]; + onConnection(mocks.socketInstance); + signatureMock.update.returns(signatureMock); + signatureMock.digest.returns('signature'); + onSubscriberTokenCallback = sinon.stub(); + onSubscriberToken = mocks.socketInstance.on.withArgs('token').args[1][1]; + onSubscriberToken({ token: arbitrarySubscriberGoodToken }, + onSubscriberTokenCallback); + + callback = amqperMock.callRpc + .withArgs('nuve', 'deleteToken', arbitrarySubscriberGoodToken.tokenId) + .args[0][3].callback; - onPublish(aOptions, aSdp, publishCallback); + setTimeout(() => { + callback({ host: 'host', room: 'roomId', userName: 'user2' }); + setTimeout(() => { + onSubscriberAutoSubscribe = mocks.socketInstance.on + .withArgs('autoSubscribe').args[1][1]; + room.forEachClient((aClient) => { + if (aClient.token.userName === 'user2') { + subscriberClient = aClient; + } + }); + subscriberClient.user.permissions[Permission.SUBSCRIBE] = true; + done(); + }, 0); + }, 0); + }); - streamId = publishCallback.args[0][0]; + describe('on AutoSubscription', () => { + let data; + const streams = []; + let streamId; - data = { - selectors: { '/attributes/type': 'publisher' }, - options: { audio: true, video: true }, - }; - streams.push(streamId); - }); + beforeEach(() => { + subscriberClient.user.permissions = {}; + subscriberClient.user.permissions[Permission.SUBSCRIBE] = true; + subscriberClient.user.permissions[Permission.PUBLISH] = true; + + const aOptions = { + audio: true, + video: true, + screen: true, + data: true, + attributes: { type: 'publisher' } }; + const aSdp = ''; + const publishCallback = sinon.stub(); + + onPublish(aOptions, aSdp, publishCallback); + + streamId = publishCallback.args[0][0]; + + data = { + selectors: { '/attributes/type': 'publisher' }, + options: { audio: true, video: true }, + }; + streams.push(streamId); + }); - it('should call callback', () => { - const subscribeCallback = sinon.stub(); - onAutoSubscribe(data, subscribeCallback); - expect(subscribeCallback.callCount).to.equal(1); - }); + it('should call callback', () => { + const subscribeCallback = sinon.stub(); + onSubscriberAutoSubscribe(data, subscribeCallback); + expect(subscribeCallback.callCount).to.equal(1); + }); - it('should fail if user is not authorized to subscribe', () => { - const subscribeCallback = sinon.stub(); - client.user.permissions[Permission.SUBSCRIBE] = false; - onAutoSubscribe(data, subscribeCallback); - expect(subscribeCallback.withArgs(null, 'Unauthorized').callCount).to.equal(1); - }); + it('should fail if user is not authorized to subscribe', () => { + const subscribeCallback = sinon.stub(); + subscriberClient.user.permissions[Permission.SUBSCRIBE] = false; + onSubscriberAutoSubscribe(data, subscribeCallback); + expect(subscribeCallback.withArgs(null, 'Unauthorized').callCount).to.equal(1); + }); - it('should call RoomController if any stream meets selector', () => { - const subscribeCallback = sinon.stub(); - data.selectors = { '/attributes/type': 'publisher' }; - onAutoSubscribe(data, subscribeCallback); - expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount) - .to.equal(1); - expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) - .to.equal(0); - }); + it('should call RoomController if any stream meets selector', () => { + const subscribeCallback = sinon.stub(); + data.selectors = { '/attributes/type': 'publisher' }; + onSubscriberAutoSubscribe(data, subscribeCallback); + expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount) + .to.equal(1); + expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) + .to.equal(0); + }); - it('should call RoomController any time a stream meets selector', () => { - const subscribeCallback = sinon.stub(); - data.selectors = { '/attributes/type': 'publisher' }; - onAutoSubscribe(data, subscribeCallback); - onAutoSubscribe(data, subscribeCallback); - onAutoSubscribe(data, subscribeCallback); - expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount) - .to.equal(3); - expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) - .to.equal(0); - }); + it('should call RoomController any time a stream meets selector', () => { + const subscribeCallback = sinon.stub(); + data.selectors = { '/attributes/type': 'publisher' }; + onSubscriberAutoSubscribe(data, subscribeCallback); + onSubscriberAutoSubscribe(data, subscribeCallback); + onSubscriberAutoSubscribe(data, subscribeCallback); + expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount) + .to.equal(3); + expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) + .to.equal(0); + }); - it('should not call RoomController if no stream meets a selector', () => { - const subscribeCallback = sinon.stub(); - data.selectors = { '/attributes/type': 'subscriber' }; - onAutoSubscribe(data, subscribeCallback); - expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount).to.equal(0); - expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) - .to.equal(1); - }); + it('should not call RoomController if no stream meets a selector', () => { + const subscribeCallback = sinon.stub(); + data.selectors = { '/attributes/type': 'subscriber' }; + onSubscriberAutoSubscribe(data, subscribeCallback); + expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount) + .to.equal(0); + expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) + .to.equal(1); + }); - it('should call RoomController to remove multiple subscribers', () => { - const subscribeCallback = sinon.stub(); - data.selectors = { '/attributes/type': 'publisher' }; - onAutoSubscribe(data, subscribeCallback); - data.selectors = { '/attributes/type': 'subscriber' }; - onAutoSubscribe(data, subscribeCallback); - expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount).to.equal(1); - expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) - .to.equal(1); + it('should call RoomController to remove multiple subscribers', () => { + const subscribeCallback = sinon.stub(); + data.selectors = { '/attributes/type': 'publisher' }; + onSubscriberAutoSubscribe(data, subscribeCallback); + data.selectors = { '/attributes/type': 'subscriber' }; + onSubscriberAutoSubscribe(data, subscribeCallback); + expect(mocks.roomControllerInstance.addMultipleSubscribers.callCount) + .to.equal(1); + expect(mocks.roomControllerInstance.removeMultipleSubscribers.callCount) + .to.equal(1); + }); }); }); diff --git a/erizo_controller/test/utils.js b/erizo_controller/test/utils.js index 670088a12e..4d44e20bb8 100644 --- a/erizo_controller/test/utils.js +++ b/erizo_controller/test/utils.js @@ -7,6 +7,8 @@ const mock = require('mock-require'); // eslint-disable-next-line import/no-extraneous-dependencies const sinon = require('sinon'); +const goodCrypto = require('crypto'); + module.exports.start = (mockObject) => { mock(mockObject.mockName, mockObject); return mockObject; @@ -83,7 +85,7 @@ module.exports.reset = () => { module.exports.crypto = createMock('crypto', { createHmac: sinon.stub().returns(module.exports.signature), - randomBytes: sinon.stub().returns(new Buffer(16)), + randomBytes: () => goodCrypto.randomBytes(16), }); module.exports.http = createMock('http', { From ab0604965942c3467767a6aa56751cf3afb7594c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 4 Mar 2019 13:24:14 +0100 Subject: [PATCH 13/20] Add support to auto unsubscriptions --- erizo_controller/erizoClient/src/Room.js | 29 +- .../erizoController/models/Client.js | 23 +- .../erizoController/roomController.js | 9 +- erizo_controller/erizoJS/erizoJSController.js | 59 +- erizo_controller/erizoJS/models/Connection.js | 4 +- erizo_controller/erizoJS/models/Subscriber.js | 4 +- package-lock.json | 2529 +++++++++++++++++ 7 files changed, 2638 insertions(+), 19 deletions(-) create mode 100644 package-lock.json diff --git a/erizo_controller/erizoClient/src/Room.js b/erizo_controller/erizoClient/src/Room.js index 46eed09530..20932047ac 100644 --- a/erizo_controller/erizoClient/src/Room.js +++ b/erizo_controller/erizoClient/src/Room.js @@ -256,6 +256,23 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { } }; + const onAutomaticStreamsUnsubscription = (args) => { + const streamIds = args.streamIds; + let stream; + streamIds.forEach((id) => { + stream = remoteStreams.get(id); + }); + // Apply the Offer to only one PC of the streams, since they all should be using the same PC. + if (stream && stream.pc) { + stream.pc.processSignalingMessage(args, streamIds); + } + streamIds.forEach((id) => { + stream = remoteStreams.get(id); + removeStream(stream); + delete stream.failed; + }); + }; + // We receive an event with a new stream in the room. // type can be "media" or "data" @@ -279,12 +296,14 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { const socketOnErizoMessage = (arg) => { let stream; - if (arg.peerId) { - stream = remoteStreams.get(arg.peerId); - } else if (arg.peerIds) { - Logger.info('Message from multiple streamIds', arg.peerIds, 'message', - arg.mess); + if (arg.context === 'auto-streams-subscription') { onAutomaticStreamsSubscription(arg.mess); + return; + } else if (arg.context === 'auto-streams-unsubscription') { + onAutomaticStreamsUnsubscription(arg.mess); + return; + } else if (arg.peerId) { + stream = remoteStreams.get(arg.peerId); } else { stream = localStreams.get(arg.streamId); } diff --git a/erizo_controller/erizoController/models/Client.js b/erizo_controller/erizoController/models/Client.js index bc4f759526..e8f86b5774 100644 --- a/erizo_controller/erizoController/models/Client.js +++ b/erizo_controller/erizoController/models/Client.js @@ -101,7 +101,7 @@ class Client extends events.EventEmitter { } } - onMultipleSubscribe(streams, options) { + onMultipleSubscribe(streams, options = {}) { if (this.room.p2p) { streams.forEach((stream) => { const clientId = stream.getClient(); @@ -165,6 +165,7 @@ class Client extends events.EventEmitter { this.sendMessage('signaling_message_erizo', { mess: signMess, options, + context: signMess.context, peerIds: signMess.streamIds }); }); } @@ -179,10 +180,21 @@ class Client extends events.EventEmitter { return; } const streamIds = streams.map(stream => stream.getID()); - this.room.controller.removeMultipleSubscribers(this.id, streamIds, (result) => { + log.debug('message: removeMultipleSubscribers requested, ' + + `streamIds: ${streamIds}, ` + + `clientId: ${this.id}`); + + this.room.controller.removeMultipleSubscribers(this.id, streamIds, (signMess) => { if (global.config.erizoController.report.session_events) { + if (signMess === 'timeout') { + log.error('message: removeMultipleSubscribers timeout when contacting ErizoJS, ' + + `streamId: ${signMess.streamId}, ` + + `clientId: ${this.id}`); + return; + } + const timeStamp = new Date(); - result.streamIds.forEach((streamId) => { + signMess.streamIds.forEach((streamId) => { this.room.amqper.broadcast('event', { room: this.room.id, user: this.id, type: 'unsubscribe', @@ -190,6 +202,11 @@ class Client extends events.EventEmitter { timestamp: timeStamp.getTime() }); }); } + + this.sendMessage('signaling_message_erizo', { mess: signMess, + options: {}, + context: signMess.context, + peerIds: signMess.streamIds }); }); } diff --git a/erizo_controller/erizoController/roomController.js b/erizo_controller/erizoController/roomController.js index 472cdc916b..9a364ef9bd 100644 --- a/erizo_controller/erizoController/roomController.js +++ b/erizo_controller/erizoController/roomController.js @@ -420,7 +420,7 @@ exports.RoomController = (spec) => { logger.objectToLog(options.metadata)); callback('timeout'); return; - } else if (data.type === 'initializing') { + } else if (data.type === 'multiple-initializing') { if (data.streamIds) { data.streamIds.forEach((streamId) => { if (subscribers[streamId].indexOf(clientId) === -1) { @@ -545,16 +545,15 @@ exports.RoomController = (spec) => { `streamIds: ${streamIdsInErizo}`); const args = [subscriberId, streamIdsInErizo]; amqper.callRpc(erizoId, 'removeMultipleSubscribers', args, { - callback: (message) => { + callback: (data) => { log.info('message: removeMultipleSubscribers finished, ' + - `response: ${message}, ` + `clientId: ${subscriberId}, ` + `streamIds: ${streamIds}`); - streamIdsInErizo.forEach((streamId) => { + data.streamIds.forEach((streamId) => { const newIndex = subscribers[streamId].indexOf(subscriberId); subscribers[streamId].splice(newIndex, 1); }); - callback({ result: true, streamIds: streamIdsInErizo }); + callback(data); }, }); }); diff --git a/erizo_controller/erizoJS/erizoJSController.js b/erizo_controller/erizoJS/erizoJSController.js index 4ba1c3c91f..3646caf791 100644 --- a/erizo_controller/erizoJS/erizoJSController.js +++ b/erizo_controller/erizoJS/erizoJSController.js @@ -99,12 +99,12 @@ exports.ErizoJSController = (threadPool, ioThreadPool) => { callbackRpc('callback', connectionEvent); }; - const closeNode = (node) => { + const closeNode = (node, sendOffer) => { const clientId = node.clientId; const connection = node.connection; log.debug(`message: closeNode, clientId: ${node.clientId}, streamId: ${node.streamId}`); - node.close(); + node.close(sendOffer); const client = clients.get(clientId); if (client === undefined) { @@ -339,15 +339,70 @@ exports.ErizoJSController = (threadPool, ioThreadPool) => { return connection.gatheredPromise; }) .then(() => { + callbackRpc('callback', { type: 'multiple-initializing', streamIds: knownStreamIds }); const evt = connection.createOffer(); evt.streamIds = knownStreamIds; evt.options = options; + evt.context = 'auto-streams-subscription'; log.debug(`message: autoSubscription sending event, type: ${evt.type}, streamId: ${knownStreamIds}, sessionVersion: ${connection.sessionVersion}, sdp: ${evt.sdp}`); callbackRpc('callback', evt); }); } }; + /* + * Removes multiple subscribers from the room. + */ + that.removeMultipleSubscribers = (clientId, streamIds, callbackRpc) => { + const knownPublishers = streamIds.map(streamId => publishers[streamId]) + .filter(pub => + pub !== undefined && + pub.getSubscriber(clientId)); + if (knownPublishers.length === 0) { + log.warn('message: removeMultipleSubscribers from unknown publisher, ' + + `code: ${WARN_NOT_FOUND}, streamIds: ${streamIds}, ` + + `clientId: ${clientId}`); + callbackRpc('callback', { type: 'error' }); + return; + } + + log.debug('message: removeMultipleSubscribers from publishers, ' + + `streamIds: ${knownPublishers}, ` + + `clientId: ${clientId}`); + + const client = clients.get(clientId); + if (!client) { + callbackRpc('callback', { type: 'error' }); + } + + let connection; + + const promises = []; + knownPublishers.forEach((publisher) => { + if (publisher && publisher.hasSubscriber(clientId)) { + const subscriber = publisher.getSubscriber(clientId); + connection = subscriber.connection; + closeNode(subscriber, false); + publisher.removeSubscriber(clientId); + } + }); + + const knownStreamIds = knownPublishers.map(pub => pub.streamId); + + Promise.all(promises) + .then(() => { + log.warn('message: removeMultipleSubscribers waiting for gathering event', connection.alreadyGathered, connection.gatheredPromise); + return connection.gatheredPromise; + }) + .then(() => { + const evt = connection.createOffer(); + evt.streamIds = knownStreamIds; + evt.context = 'auto-streams-unsubscription'; + log.warn(`message: removeMultipleSubscribers sending event, type: ${evt.type}, streamId: ${knownStreamIds}, sessionVersion: ${connection.sessionVersion}, sdp: ${evt.sdp}`); + callbackRpc('callback', evt); + }); + }; + /* * Removes a publisher from the room. This also deletes the associated OneToManyProcessor. */ diff --git a/erizo_controller/erizoJS/models/Connection.js b/erizo_controller/erizoJS/models/Connection.js index 58d065d1ca..2512ba34e4 100644 --- a/erizo_controller/erizoJS/models/Connection.js +++ b/erizo_controller/erizoJS/models/Connection.js @@ -236,14 +236,14 @@ class Connection extends events.EventEmitter { return promise; } - removeMediaStream(id) { + removeMediaStream(id, sendOffer = true) { let promise = Promise.resolve(); if (this.mediaStreams.get(id) !== undefined) { const label = this.mediaStreams.get(id).label; promise = this.wrtc.removeMediaStream(id); this.mediaStreams.get(id).close(); this.mediaStreams.delete(id); - this._resendLastAnswer(CONN_SDP, id, label, true, true); + this._resendLastAnswer(CONN_SDP, id, label, sendOffer, true); } else { log.error(`message: Trying to remove mediaStream not found, id: ${id}`); } diff --git a/erizo_controller/erizoJS/models/Subscriber.js b/erizo_controller/erizoJS/models/Subscriber.js index 71012eba98..bf83e2ac4d 100644 --- a/erizo_controller/erizoJS/models/Subscriber.js +++ b/erizo_controller/erizoJS/models/Subscriber.js @@ -139,12 +139,12 @@ class Subscriber extends NodeClass { } } - close() { + close(sendOffer = true) { log.debug(`msg: Closing subscriber, streamId:${this.streamId}`); this.publisher = undefined; let promise = Promise.resolve(); if (this.connection) { - promise = this.connection.removeMediaStream(this.mediaStream.id); + promise = this.connection.removeMediaStream(this.mediaStream.id, sendOffer); this.connection.removeListener('status_event', this._connectionListener); this.connection.removeListener('media_stream_event', this._mediaStreamListener); } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..de9cefa963 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2529 @@ +{ + "name": "licode", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "acorn": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", + "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "aria-query": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.5.0.tgz", + "integrity": "sha1-heMVLNjMW6sY2+1hzZxPzlT6ecM=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7" + } + }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.7.0" + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "async": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.2.tgz", + "integrity": "sha1-YSpKtF70KnDN6Aa62G7m2wR+g4U=", + "dev": true, + "requires": { + "lodash": "4.16.4" + }, + "dependencies": { + "lodash": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.4.tgz", + "integrity": "sha1-Ac4wa5utExnypVKGdPiCl663ASc=", + "dev": true + } + } + }, + "axobject-query": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz", + "integrity": "sha1-YvWdvFnJ+SQnWco0mWDnov48NsA=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7" + } + }, + "babel-code-frame": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.1" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "chai": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true, + "requires": { + "assertion-error": "1.0.2", + "deep-eql": "0.1.3", + "type-detect": "1.0.0" + }, + "dependencies": { + "assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "deep-eql": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + }, + "dependencies": { + "type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, + "type-detect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + } + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "circular-json": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", + "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "1.0.1" + } + }, + "cli-width": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", + "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.2.6", + "typedarray": "0.0.6" + } + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.23" + } + }, + "damerau-levenshtein": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", + "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, + "doctrine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", + "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", + "dev": true, + "requires": { + "esutils": "2.0.2", + "isarray": "1.0.0" + } + }, + "emoji-regex": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.4.2.tgz", + "integrity": "sha1-owtv7jU9QG2Wz7n6dlvcgol+/24=", + "dev": true + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es-abstract": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz", + "integrity": "sha1-363ndOAb/Nl/lhgCmMRJyGI/uUw=", + "dev": true, + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.0", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, + "es5-ext": { + "version": "0.10.23", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.23.tgz", + "integrity": "sha1-dXi1G+l0IHpUh4IbVlOMIk5Oezg=", + "dev": true, + "requires": { + "es6-iterator": "2.0.1", + "es6-symbol": "3.1.1" + } + }, + "es6-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", + "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.23", + "es6-symbol": "3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.23", + "es6-iterator": "2.0.1", + "es6-set": "0.1.5", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.23", + "es6-iterator": "2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.23" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.23", + "es6-iterator": "2.0.1", + "es6-symbol": "3.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "0.1.5", + "es6-weak-map": "2.0.2", + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "eslint": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "chalk": "1.1.3", + "concat-stream": "1.6.0", + "debug": "2.2.0", + "doctrine": "2.0.0", + "escope": "3.6.0", + "espree": "3.4.3", + "esquery": "1.0.0", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "glob": "7.1.2", + "globals": "9.18.0", + "ignore": "3.3.3", + "imurmurhash": "0.1.4", + "inquirer": "0.12.0", + "is-my-json-valid": "2.16.0", + "is-resolvable": "1.0.0", + "js-yaml": "3.8.4", + "json-stable-stringify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.2", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "1.2.1", + "progress": "1.1.8", + "require-uncached": "1.0.3", + "shelljs": "0.7.8", + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1", + "table": "3.8.3", + "text-table": "0.2.0", + "user-home": "2.0.0" + } + }, + "eslint-config-airbnb": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-15.0.1.tgz", + "integrity": "sha1-e1GI5bfHS5ss5jn9Xh2rqP12Gu0=", + "dev": true, + "requires": { + "eslint-config-airbnb-base": "11.2.0" + } + }, + "eslint-config-airbnb-base": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.2.0.tgz", + "integrity": "sha1-GancRIGib3CQRUXsBAEWh2AY+FM=", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz", + "integrity": "sha1-Wt2BBujJKNssuiMrzZ76hG49oWw=", + "dev": true, + "requires": { + "debug": "2.2.0", + "object-assign": "4.1.1", + "resolve": "1.3.3" + } + }, + "eslint-module-utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", + "integrity": "sha1-q67IJBd2E7ipWymWOeG2+s9HNEk=", + "dev": true, + "requires": { + "debug": "2.6.8", + "pkg-dir": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.6.0.tgz", + "integrity": "sha1-Kku602oHjgUqPIMM49+9a4oSxuU=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1", + "contains-path": "0.1.0", + "debug": "2.6.8", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "0.2.3", + "eslint-module-utils": "2.1.1", + "has": "1.0.1", + "lodash.cond": "4.5.2", + "minimatch": "3.0.4", + "read-pkg-up": "2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "2.0.2", + "isarray": "1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-5.1.0.tgz", + "integrity": "sha1-SoKWNDROepA5Gp+w+9GYEHN9ecU=", + "dev": true, + "requires": { + "aria-query": "0.5.0", + "array-includes": "3.0.3", + "ast-types-flow": "0.0.7", + "axobject-query": "0.1.0", + "damerau-levenshtein": "1.0.4", + "emoji-regex": "6.4.2", + "jsx-ast-utils": "1.4.1" + } + }, + "eslint-plugin-react": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.1.0.tgz", + "integrity": "sha1-J3cKzzn1/UnNCvQIPOWBBOs5DUw=", + "dev": true, + "requires": { + "doctrine": "2.0.0", + "has": "1.0.1", + "jsx-ast-utils": "1.4.1" + } + }, + "espree": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz", + "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=", + "dev": true, + "requires": { + "acorn": "5.0.3", + "acorn-jsx": "3.0.1" + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.23" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "1.2.2", + "object-assign": "4.1.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "flat-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", + "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", + "dev": true, + "requires": { + "circular-json": "0.3.1", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + }, + "dependencies": { + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.1" + } + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", + "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=", + "dev": true + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "1.0.2" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=", + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "dev": true, + "requires": { + "function-bind": "1.1.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=", + "dev": true + }, + "ignore": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", + "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "1.4.0", + "ansi-regex": "2.1.1", + "chalk": "1.1.3", + "cli-cursor": "1.0.2", + "cli-width": "2.1.0", + "figures": "1.7.0", + "lodash": "4.17.2", + "readline2": "1.0.1", + "run-async": "0.1.0", + "rx-lite": "3.1.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "through": "2.3.8" + } + }, + "interpret": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-my-json-valid": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", + "dev": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "1.0.1" + } + }, + "is-resolvable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", + "dev": true, + "requires": { + "tryit": "1.0.3" + } + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "js-tokens": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", + "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=", + "dev": true + }, + "js-yaml": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz", + "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "3.1.3" + } + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsx-ast-utils": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", + "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.2.tgz", + "integrity": "sha1-NKMFW6vgTOQkZ7YH1wAHLH/2v0I=", + "dev": true + }, + "lodash.cond": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", + "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.1.2.tgz", + "integrity": "sha1-Ufk7Qyv34bF1/8Iog8zQvjLbprU=", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.5", + "glob": "7.0.5", + "growl": "1.9.2", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "dependencies": { + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + }, + "dependencies": { + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + } + } + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + }, + "dependencies": { + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "glob": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", + "integrity": "sha1-tCAqaQmbu00pKnwblbZoK2fr3JU=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.3", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + }, + "dependencies": { + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + }, + "dependencies": { + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", + "dev": true, + "requires": { + "brace-expansion": "1.1.6" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + } + } + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + }, + "dependencies": { + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + } + } + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "3.2.0", + "lodash._basecreate": "3.0.3", + "lodash._isiterateecall": "3.0.9" + }, + "dependencies": { + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + }, + "dependencies": { + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + }, + "dependencies": { + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + } + } + } + } + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + } + } + } + } + }, + "mock-require": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-1.3.0.tgz", + "integrity": "sha1-gmFElS5QR2L45pJKqPY5Rl0deiQ=", + "dev": true, + "requires": { + "caller-id": "0.1.0" + }, + "dependencies": { + "caller-id": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-id/-/caller-id-0.1.0.tgz", + "integrity": "sha1-Wb2sCJPRLDhxQIJ5Ix+XRYNk8Hs=", + "dev": true, + "requires": { + "stack-trace": "0.0.9" + }, + "dependencies": { + "stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=", + "dev": true + } + } + } + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.3.0", + "validate-npm-package-license": "3.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "p-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", + "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", + "dev": true + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.1.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "1.1.2" + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + } + } + }, + "readable-stream": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.6.tgz", + "integrity": "sha1-i0Ou125xSDk40SqNRsbPGgCx+BY=", + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "mute-stream": "0.0.5" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "1.3.3" + } + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + }, + "dependencies": { + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + } + } + }, + "resolve": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", + "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "1.1.1", + "onetime": "1.1.0" + } + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "7.1.2", + "interpret": "1.0.3", + "rechoir": "0.6.2" + } + }, + "sinon": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.6.tgz", + "integrity": "sha1-pDEW21lXfIKWNWr+4T+vwjMuWOE=", + "dev": true, + "requires": { + "formatio": "1.1.1", + "lolex": "1.3.2", + "samsam": "1.1.2", + "util": "0.10.3" + }, + "dependencies": { + "formatio": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", + "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", + "dev": true, + "requires": { + "samsam": "1.1.2" + } + }, + "lolex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", + "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", + "dev": true + }, + "samsam": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", + "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + } + } + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supertest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-2.0.1.tgz", + "integrity": "sha1-oFgIHXiPFRXUcA11Aogea3WeRM0=", + "dev": true, + "requires": { + "methods": "1.1.2", + "superagent": "2.3.0" + }, + "dependencies": { + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "superagent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz", + "integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "cookiejar": "2.1.0", + "debug": "2.2.0", + "extend": "3.0.0", + "form-data": "1.0.0-rc4", + "formidable": "1.0.17", + "methods": "1.1.2", + "mime": "1.3.4", + "qs": "6.3.0", + "readable-stream": "2.1.5" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "cookiejar": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.0.tgz", + "integrity": "sha1-hlSWiVObbQ4mm2Y3owS+UIGU2Jg=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + }, + "dependencies": { + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "extend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", + "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=", + "dev": true + }, + "form-data": { + "version": "1.0.0-rc4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", + "integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=", + "dev": true, + "requires": { + "async": "1.5.2", + "combined-stream": "1.0.5", + "mime-types": "2.1.12" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + }, + "dependencies": { + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + } + } + }, + "mime-types": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.12.tgz", + "integrity": "sha1-FSuiVndwIN1GY/VMLnvCY4HnFyk=", + "dev": true, + "requires": { + "mime-db": "1.24.0" + }, + "dependencies": { + "mime-db": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.24.0.tgz", + "integrity": "sha1-4tE/k58AFsbk6a0lqGUvEmxGfww=", + "dev": true + } + } + } + } + }, + "formidable": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.17.tgz", + "integrity": "sha1-71SRSQ+UM7cF+qdyScmQKa40hVk=", + "dev": true + }, + "mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=", + "dev": true + }, + "qs": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.0.tgz", + "integrity": "sha1-9AOyZPI7wBIox0ExtAfxjV6l1EI=", + "dev": true + }, + "readable-stream": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", + "integrity": "sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=", + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + }, + "dependencies": { + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + } + } + } + } + } + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "4.11.8", + "ajv-keywords": "1.5.1", + "chalk": "1.1.3", + "lodash": "4.17.2", + "slice-ansi": "0.0.4", + "string-width": "2.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.0.tgz", + "integrity": "sha1-AwZkVh/BRslCPsfZeP4kV0N/5tA=", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tryit": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "vows": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/vows/-/vows-0.6.4.tgz", + "integrity": "sha1-/w7V1lTQTTl87jLc5DhOqIy08ZM=", + "dev": true, + "requires": { + "diff": "1.0.8", + "eyes": "0.1.8" + }, + "dependencies": { + "diff": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.8.tgz", + "integrity": "sha1-NDJ2MI7Jkbe8giZ+1VvBQR+XFmY=", + "dev": true + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "dev": true + } + } + }, + "winston": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-0.7.3.tgz", + "integrity": "sha1-euMTunP83C7LSqL5zURugphncmY=", + "dev": true, + "requires": { + "async": "0.2.10", + "colors": "0.6.2", + "cycle": "1.0.3", + "eyes": "0.1.8", + "pkginfo": "0.3.1", + "request": "2.16.6", + "stack-trace": "0.0.9" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "dev": true + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "dev": true + }, + "pkginfo": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", + "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=", + "dev": true + }, + "request": { + "version": "2.16.6", + "resolved": "https://registry.npmjs.org/request/-/request-2.16.6.tgz", + "integrity": "sha1-hy/kRa5y3iZrN4edatfclI+gHK0=", + "dev": true, + "requires": { + "aws-sign": "0.2.0", + "cookie-jar": "0.2.0", + "forever-agent": "0.2.0", + "form-data": "0.0.10", + "hawk": "0.10.2", + "json-stringify-safe": "3.0.0", + "mime": "1.2.11", + "node-uuid": "1.4.7", + "oauth-sign": "0.2.0", + "qs": "0.5.6", + "tunnel-agent": "0.2.0" + }, + "dependencies": { + "aws-sign": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/aws-sign/-/aws-sign-0.2.0.tgz", + "integrity": "sha1-xVAThWyBlOyFSgy+yQqrWgTOOsU=", + "dev": true + }, + "cookie-jar": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cookie-jar/-/cookie-jar-0.2.0.tgz", + "integrity": "sha1-ZOzAasl423leS1KQy+SLo3gUAPo=", + "dev": true + }, + "forever-agent": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.2.0.tgz", + "integrity": "sha1-4cJcetROCcOPIzh2x2/MJP+EOx8=", + "dev": true + }, + "form-data": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.0.10.tgz", + "integrity": "sha1-2zRaU3jYau6x7V1VO4aawZLS9e0=", + "dev": true, + "requires": { + "async": "0.2.10", + "combined-stream": "0.0.7", + "mime": "1.2.11" + }, + "dependencies": { + "combined-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", + "dev": true, + "requires": { + "delayed-stream": "0.0.5" + }, + "dependencies": { + "delayed-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=", + "dev": true + } + } + } + } + }, + "hawk": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-0.10.2.tgz", + "integrity": "sha1-mzYd7pWpMWQObVBOBWCaj8OsRdI=", + "dev": true, + "requires": { + "boom": "0.3.8", + "cryptiles": "0.1.3", + "hoek": "0.7.6", + "sntp": "0.1.4" + }, + "dependencies": { + "boom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/boom/-/boom-0.3.8.tgz", + "integrity": "sha1-yM2wQUNZEnQWKMBE7Mcy0dF8Ceo=", + "dev": true, + "requires": { + "hoek": "0.7.6" + } + }, + "cryptiles": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.1.3.tgz", + "integrity": "sha1-GlVnNPBtJLo0hirpy55wmjr7/xw=", + "dev": true, + "requires": { + "boom": "0.3.8" + } + }, + "hoek": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.7.6.tgz", + "integrity": "sha1-YPvZBFV1Qc0rh5Wr8wihs3cOFVo=", + "dev": true + }, + "sntp": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.1.4.tgz", + "integrity": "sha1-XvSBuVGnspr/30r9fyaDj8ESD4Q=", + "dev": true, + "requires": { + "hoek": "0.7.6" + } + } + } + }, + "json-stringify-safe": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-3.0.0.tgz", + "integrity": "sha1-nbew5TDH8onF6MhDKvGRwv91pbM=", + "dev": true + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", + "dev": true + }, + "node-uuid": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", + "integrity": "sha1-baWhdmjEs91ZYjvaEc9/pMH2Cm8=", + "dev": true + }, + "oauth-sign": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.2.0.tgz", + "integrity": "sha1-oOahcV2u0GLzIrYit/5a/RA1tuI=", + "dev": true + }, + "qs": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz", + "integrity": "sha1-MbGtBYVnZRxSaSFQa5qHk5EaA4Q=", + "dev": true + }, + "tunnel-agent": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.2.0.tgz", + "integrity": "sha1-aFPCr7GyEJ5FYp5JK9419Fnqaeg=", + "dev": true + } + } + }, + "stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=", + "dev": true + } + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "0.5.1" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + } + } +} From aa620c543292183d0dedb374637080b4f4721bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 4 Mar 2019 13:29:56 +0100 Subject: [PATCH 14/20] Fix Unit tests --- .../test/erizoController/roomController.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erizo_controller/test/erizoController/roomController.js b/erizo_controller/test/erizoController/roomController.js index bf7fc671f1..a2ea0300ee 100644 --- a/erizo_controller/test/erizoController/roomController.js +++ b/erizo_controller/test/erizoController/roomController.js @@ -359,7 +359,7 @@ describe('Erizo Controller / Room Controller', () => { expect(amqperMock.callRpc.callCount).to.equal(2); expect(amqperMock.callRpc.args[1][1]).to.equal('addMultipleSubscribers'); - amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [kArbitraryStreamId] }); + amqperMock.callRpc.args[1][3].callback({ type: 'multiple-initializing', streamIds: [kArbitraryStreamId] }); controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], kArbitraryOptions, callback); @@ -377,7 +377,7 @@ describe('Erizo Controller / Room Controller', () => { expect(amqperMock.callRpc.callCount).to.equal(2); expect(amqperMock.callRpc.args[1][1]).to.equal('addMultipleSubscribers'); - amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [] }); + amqperMock.callRpc.args[1][3].callback({ type: 'multiple-initializing', streamIds: [] }); controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], kArbitraryOptions, callback); @@ -392,7 +392,7 @@ describe('Erizo Controller / Room Controller', () => { controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], kArbitraryOptions, callback); - amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [kArbitraryStreamId] }); + amqperMock.callRpc.args[1][3].callback({ type: 'multiple-initializing', streamIds: [kArbitraryStreamId] }); controller.removeMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], callback); expect(amqperMock.callRpc.callCount).to.equal(3); @@ -406,13 +406,13 @@ describe('Erizo Controller / Room Controller', () => { controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], kArbitraryOptions, callback); - amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [kArbitraryStreamId] }); + amqperMock.callRpc.args[1][3].callback({ type: 'multiple-initializing', streamIds: [kArbitraryStreamId] }); controller.removeMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], callback); expect(amqperMock.callRpc.callCount).to.equal(3); expect(amqperMock.callRpc.args[2][1]).to.equal('removeMultipleSubscribers'); - amqperMock.callRpc.args[2][3].callback(); + amqperMock.callRpc.args[2][3].callback({type: 'offer', streamIds: [kArbitraryStreamId]}); controller.removeMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], callback); expect(amqperMock.callRpc.callCount).to.equal(3); @@ -442,7 +442,7 @@ describe('Erizo Controller / Room Controller', () => { controller.addMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], kArbitraryOptions, sinon.stub()); - amqperMock.callRpc.args[1][3].callback({ type: 'initializing', streamIds: [kArbitraryStreamId] }); + amqperMock.callRpc.args[1][3].callback({ type: 'multiple-initializing', streamIds: [kArbitraryStreamId] }); }); it('should call Erizo\'s removeMultipleSubscribers', () => { From d9a51414811917facad25d1f5c22045cd30fc5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 4 Mar 2019 13:39:51 +0100 Subject: [PATCH 15/20] Remove wrong logs --- erizo/src/erizo/MediaStream.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erizo/src/erizo/MediaStream.cpp b/erizo/src/erizo/MediaStream.cpp index 76bb03737f..fdddfaa480 100644 --- a/erizo/src/erizo/MediaStream.cpp +++ b/erizo/src/erizo/MediaStream.cpp @@ -407,7 +407,6 @@ void MediaStream::initializePipeline() { pipeline_->addFront(std::make_shared(this)); pipeline_->finalize(); - ELOG_WARN("%s message: Pipeline initialized, id: %s", toLog(), getId()); pipeline_initialized_ = true; } @@ -480,7 +479,7 @@ void MediaStream::onTransportData(std::shared_ptr incoming_packet, T worker_->task([stream_ptr, packet]{ if (!stream_ptr->pipeline_initialized_) { - ELOG_WARN("%s message: Pipeline not initialized yet.", stream_ptr->toLog()); + ELOG_DEBUG("%s message: Pipeline not initialized yet.", stream_ptr->toLog()); return; } @@ -497,7 +496,6 @@ void MediaStream::onTransportData(std::shared_ptr incoming_packet, T } if (stream_ptr->pipeline_) { - // ELOG_WARN("%s message: New packet!!", stream_ptr->toLog()); stream_ptr->pipeline_->read(std::move(packet)); } }); From 16f712f12fa250f653cb5b14781ad3fbf8242c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 4 Mar 2019 13:49:56 +0100 Subject: [PATCH 16/20] Implement negative selectors --- erizo_controller/erizoClient/src/Room.js | 10 ++++++++-- erizo_controller/erizoController/models/Client.js | 14 ++++++++------ .../test/erizoController/erizoController.js | 1 + 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/erizo_controller/erizoClient/src/Room.js b/erizo_controller/erizoClient/src/Room.js index 20932047ac..aeb294b2c4 100644 --- a/erizo_controller/erizoClient/src/Room.js +++ b/erizo_controller/erizoClient/src/Room.js @@ -910,12 +910,18 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { // '/attributes/kind': 'professor', // '/attributes/externalId': '10' // }; + // const negativeSelectors = { + // '/id': '23', + // '/attributes/group': '23', + // '/attributes/kind': 'professor', + // '/attributes/externalId': '10' + // }; // const options = {audio: true, video: false, forceTurn: true}; - that.autoSubscribe = (selectors, options, callback) => { + that.autoSubscribe = (selectors, negativeSelectors, options, callback) => { if (!socket) { return; } - socket.sendMessage('autoSubscribe', { selectors, options }, (result) => { + socket.sendMessage('autoSubscribe', { selectors, negativeSelectors, options }, (result) => { if (result) { callback(result); } diff --git a/erizo_controller/erizoController/models/Client.js b/erizo_controller/erizoController/models/Client.js index e8f86b5774..75bd1c9c0e 100644 --- a/erizo_controller/erizoController/models/Client.js +++ b/erizo_controller/erizoController/models/Client.js @@ -60,14 +60,15 @@ class Client extends events.EventEmitter { this.channel.sendBuffer(buffer); } - setSelectors(selectors, options) { + setSelectors(selectors, negativeSelectors, options) { this.selectors = selectors; + this.negativeSelectors = negativeSelectors; this.selectorOptions = options; this.onInternalAutoSubscriptionChange(); } onInternalAutoSubscriptionChange() { - if (!this.selectors) { + if (!this.selectors && !this.negativeSelectors) { return; } const subscribableStreams = []; @@ -77,7 +78,7 @@ class Client extends events.EventEmitter { if (this.streams.indexOf(stream.getID()) !== -1) { return; } - if (stream.meetAnySelector(this.selectors)) { + if (stream.meetAnySelector(this.selectors) && !stream.meetAnySelector(this.negativeSelectors)) { if (stream.hasData() && this.options.data !== false) { stream.addDataSubscriber(this.id); } @@ -661,10 +662,11 @@ class Client extends events.EventEmitter { return; } - const selectors = data && data.selectors; - const options = data && data.options; + const selectors = (data && data.selectors) || {}; + const negativeSelectors = (data && data.negativeSelectors) || {}; + const options = (data && data.options) || {}; - this.setSelectors(selectors, options); + this.setSelectors(selectors, negativeSelectors, options); callback(); } diff --git a/erizo_controller/test/erizoController/erizoController.js b/erizo_controller/test/erizoController/erizoController.js index 12361c2254..7309edc9ac 100644 --- a/erizo_controller/test/erizoController/erizoController.js +++ b/erizo_controller/test/erizoController/erizoController.js @@ -577,6 +577,7 @@ describe('Erizo Controller / Erizo Controller', () => { data = { selectors: { '/attributes/type': 'publisher' }, + negativeSelectors: {}, options: { audio: true, video: true }, }; streams.push(streamId); From 282f1b38ad0c8a0eee52bb474e57ac50218e3040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 4 Mar 2019 13:51:22 +0100 Subject: [PATCH 17/20] Fix lint --- erizo_controller/erizoController/models/Client.js | 3 ++- erizo_controller/test/erizoController/roomController.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erizo_controller/erizoController/models/Client.js b/erizo_controller/erizoController/models/Client.js index 75bd1c9c0e..4c6760503e 100644 --- a/erizo_controller/erizoController/models/Client.js +++ b/erizo_controller/erizoController/models/Client.js @@ -78,7 +78,8 @@ class Client extends events.EventEmitter { if (this.streams.indexOf(stream.getID()) !== -1) { return; } - if (stream.meetAnySelector(this.selectors) && !stream.meetAnySelector(this.negativeSelectors)) { + if (stream.meetAnySelector(this.selectors) && + !stream.meetAnySelector(this.negativeSelectors)) { if (stream.hasData() && this.options.data !== false) { stream.addDataSubscriber(this.id); } diff --git a/erizo_controller/test/erizoController/roomController.js b/erizo_controller/test/erizoController/roomController.js index a2ea0300ee..817211ee5f 100644 --- a/erizo_controller/test/erizoController/roomController.js +++ b/erizo_controller/test/erizoController/roomController.js @@ -412,7 +412,7 @@ describe('Erizo Controller / Room Controller', () => { expect(amqperMock.callRpc.callCount).to.equal(3); expect(amqperMock.callRpc.args[2][1]).to.equal('removeMultipleSubscribers'); - amqperMock.callRpc.args[2][3].callback({type: 'offer', streamIds: [kArbitraryStreamId]}); + amqperMock.callRpc.args[2][3].callback({ type: 'offer', streamIds: [kArbitraryStreamId] }); controller.removeMultipleSubscribers(kArbitraryClientId, [kArbitraryStreamId], callback); expect(amqperMock.callRpc.callCount).to.equal(3); From fe323c656e6819ef105434cd70ef3cb6730ce134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Mon, 4 Mar 2019 23:43:49 +0100 Subject: [PATCH 18/20] Update basic example --- extras/basic_example/public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/basic_example/public/script.js b/extras/basic_example/public/script.js index 06a46484aa..6c56106f5f 100644 --- a/extras/basic_example/public/script.js +++ b/extras/basic_example/public/script.js @@ -140,7 +140,7 @@ const startBasicExample = () => { room.publish(localStream, options); } if (autoSubscribe) { - room.autoSubscribe({ '/attributes/type': 'publisher' }, { audio: true, video: true, data: false }, () => {}); + room.autoSubscribe({ '/attributes/type': 'publisher' }, {}, { audio: true, video: true, data: false }, () => {}); } subscribeToStreams(roomEvent.streams); }); From d4271ef1ada9756ee756ea6fec2a2a967d463a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Fri, 8 Mar 2019 09:40:14 +0100 Subject: [PATCH 19/20] Apply comments from PR --- erizo/src/erizo/MediaStream.cpp | 2 +- .../erizoController/roomController.js | 4 +-- erizo_controller/erizoJS/erizoJSController.js | 34 ++++++++++--------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/erizo/src/erizo/MediaStream.cpp b/erizo/src/erizo/MediaStream.cpp index 03c3d6f577..9f40b93e56 100644 --- a/erizo/src/erizo/MediaStream.cpp +++ b/erizo/src/erizo/MediaStream.cpp @@ -178,7 +178,7 @@ bool MediaStream::setRemoteSdp(std::shared_ptr sdp) { stream_found = true; } - if (!stream_found && isPublisher()) { + if (!stream_found) { return true; } } diff --git a/erizo_controller/erizoController/roomController.js b/erizo_controller/erizoController/roomController.js index 9a364ef9bd..b63e47405f 100644 --- a/erizo_controller/erizoController/roomController.js +++ b/erizo_controller/erizoController/roomController.js @@ -388,7 +388,7 @@ exports.RoomController = (spec) => { return; } - const erizoIds = [...new Set(streamIds.map(streamId => getErizoQueue(streamId)))]; + const erizoIds = Array.from(new Set(streamIds.map(streamId => getErizoQueue(streamId)))); erizoIds.forEach((erizoId) => { const streamIdsInErizo = streamIds.filter(streamId => getErizoQueue(streamId) === erizoId); @@ -536,7 +536,7 @@ exports.RoomController = (spec) => { return; } - const erizoIds = [...new Set(streamIds.map(streamId => getErizoQueue(streamId)))]; + const erizoIds = Array.from(new Set(streamIds.map(streamId => getErizoQueue(streamId)))); erizoIds.forEach((erizoId) => { const streamIdsInErizo = streamIds.filter(streamId => getErizoQueue(streamId) === erizoId); diff --git a/erizo_controller/erizoJS/erizoJSController.js b/erizo_controller/erizoJS/erizoJSController.js index dfdae38c88..816a043208 100644 --- a/erizo_controller/erizoJS/erizoJSController.js +++ b/erizo_controller/erizoJS/erizoJSController.js @@ -281,6 +281,10 @@ exports.ErizoJSController = (threadPool, ioThreadPool) => { * Adds multiple subscribers to the room. */ that.addMultipleSubscribers = (clientId, streamIds, options, callbackRpc) => { + if (!options.singlePC) { + log.warn('message: addMultipleSubscribers not compatible with no single PC, clientId:', clientId); + } + const knownPublishers = streamIds.map(streamId => publishers[streamId]) .filter(pub => pub !== undefined && @@ -330,22 +334,20 @@ exports.ErizoJSController = (threadPool, ioThreadPool) => { connection.init(knownStreamIds[0], constraints); promises.push(connection.createOfferPromise); - if (options.singlePC) { - Promise.all(promises) - .then(() => { - log.debug('message: autoSubscription waiting for gathering event', connection.alreadyGathered, connection.gatheredPromise); - return connection.gatheredPromise; - }) - .then(() => { - callbackRpc('callback', { type: 'multiple-initializing', streamIds: knownStreamIds, context: 'auto-streams-subscription', options }); - const evt = connection.createOffer(); - evt.streamIds = knownStreamIds; - evt.options = options; - evt.context = 'auto-streams-subscription'; - log.debug(`message: autoSubscription sending event, type: ${evt.type}, streamId: ${knownStreamIds}, sessionVersion: ${connection.sessionVersion}, sdp: ${evt.sdp}`); - callbackRpc('callback', evt); - }); - } + Promise.all(promises) + .then(() => { + log.debug('message: autoSubscription waiting for gathering event', connection.alreadyGathered, connection.gatheredPromise); + return connection.gatheredPromise; + }) + .then(() => { + callbackRpc('callback', { type: 'multiple-initializing', streamIds: knownStreamIds, context: 'auto-streams-subscription', options }); + const evt = connection.createOffer(); + evt.streamIds = knownStreamIds; + evt.options = options; + evt.context = 'auto-streams-subscription'; + log.debug(`message: autoSubscription sending event, type: ${evt.type}, streamId: ${knownStreamIds}, sessionVersion: ${connection.sessionVersion}, sdp: ${evt.sdp}`); + callbackRpc('callback', evt); + }); }; /* From 5603f981f6fd8980774921d52976ec4b4c104f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cervi=C3=B1o?= Date: Fri, 8 Mar 2019 09:41:20 +0100 Subject: [PATCH 20/20] Remove package-lock.json --- package-lock.json | 2529 --------------------------------------------- 1 file changed, 2529 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index de9cefa963..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,2529 +0,0 @@ -{ - "name": "licode", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "acorn": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", - "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=", - "dev": true - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "3.3.0" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "argparse": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", - "dev": true, - "requires": { - "sprintf-js": "1.0.3" - } - }, - "aria-query": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.5.0.tgz", - "integrity": "sha1-heMVLNjMW6sY2+1hzZxPzlT6ecM=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7" - } - }, - "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", - "dev": true, - "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.7.0" - } - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "1.0.3" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, - "async": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.1.2.tgz", - "integrity": "sha1-YSpKtF70KnDN6Aa62G7m2wR+g4U=", - "dev": true, - "requires": { - "lodash": "4.16.4" - }, - "dependencies": { - "lodash": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.4.tgz", - "integrity": "sha1-Ac4wa5utExnypVKGdPiCl663ASc=", - "dev": true - } - } - }, - "axobject-query": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz", - "integrity": "sha1-YvWdvFnJ+SQnWco0mWDnov48NsA=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7" - } - }, - "babel-code-frame": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", - "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.1" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "chai": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", - "dev": true, - "requires": { - "assertion-error": "1.0.2", - "deep-eql": "0.1.3", - "type-detect": "1.0.0" - }, - "dependencies": { - "assertion-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", - "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", - "dev": true - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dev": true, - "requires": { - "type-detect": "0.1.1" - }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } - } - }, - "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", - "dev": true - } - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "circular-json": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", - "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=", - "dev": true - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "1.0.1" - } - }, - "cli-width": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", - "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.2.6", - "typedarray": "0.0.6" - } - }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "0.10.23" - } - }, - "damerau-levenshtein": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", - "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=", - "dev": true - }, - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true, - "requires": { - "ms": "0.7.1" - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "dev": true, - "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.11" - } - }, - "doctrine": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", - "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", - "dev": true, - "requires": { - "esutils": "2.0.2", - "isarray": "1.0.0" - } - }, - "emoji-regex": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.4.2.tgz", - "integrity": "sha1-owtv7jU9QG2Wz7n6dlvcgol+/24=", - "dev": true - }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "dev": true, - "requires": { - "is-arrayish": "0.2.1" - } - }, - "es-abstract": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz", - "integrity": "sha1-363ndOAb/Nl/lhgCmMRJyGI/uUw=", - "dev": true, - "requires": { - "es-to-primitive": "1.1.1", - "function-bind": "1.1.0", - "is-callable": "1.1.3", - "is-regex": "1.0.4" - } - }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "dev": true, - "requires": { - "is-callable": "1.1.3", - "is-date-object": "1.0.1", - "is-symbol": "1.0.1" - } - }, - "es5-ext": { - "version": "0.10.23", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.23.tgz", - "integrity": "sha1-dXi1G+l0IHpUh4IbVlOMIk5Oezg=", - "dev": true, - "requires": { - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" - } - }, - "es6-iterator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", - "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23", - "es6-symbol": "3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23", - "es6-iterator": "2.0.1", - "es6-set": "0.1.5", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23", - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23", - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "0.1.5", - "es6-weak-map": "2.0.2", - "esrecurse": "4.2.0", - "estraverse": "4.2.0" - } - }, - "eslint": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", - "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", - "dev": true, - "requires": { - "babel-code-frame": "6.22.0", - "chalk": "1.1.3", - "concat-stream": "1.6.0", - "debug": "2.2.0", - "doctrine": "2.0.0", - "escope": "3.6.0", - "espree": "3.4.3", - "esquery": "1.0.0", - "estraverse": "4.2.0", - "esutils": "2.0.2", - "file-entry-cache": "2.0.0", - "glob": "7.1.2", - "globals": "9.18.0", - "ignore": "3.3.3", - "imurmurhash": "0.1.4", - "inquirer": "0.12.0", - "is-my-json-valid": "2.16.0", - "is-resolvable": "1.0.0", - "js-yaml": "3.8.4", - "json-stable-stringify": "1.0.1", - "levn": "0.3.0", - "lodash": "4.17.2", - "mkdirp": "0.5.1", - "natural-compare": "1.4.0", - "optionator": "0.8.2", - "path-is-inside": "1.0.2", - "pluralize": "1.2.1", - "progress": "1.1.8", - "require-uncached": "1.0.3", - "shelljs": "0.7.8", - "strip-bom": "3.0.0", - "strip-json-comments": "2.0.1", - "table": "3.8.3", - "text-table": "0.2.0", - "user-home": "2.0.0" - } - }, - "eslint-config-airbnb": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-15.0.1.tgz", - "integrity": "sha1-e1GI5bfHS5ss5jn9Xh2rqP12Gu0=", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "11.2.0" - } - }, - "eslint-config-airbnb-base": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.2.0.tgz", - "integrity": "sha1-GancRIGib3CQRUXsBAEWh2AY+FM=", - "dev": true - }, - "eslint-import-resolver-node": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz", - "integrity": "sha1-Wt2BBujJKNssuiMrzZ76hG49oWw=", - "dev": true, - "requires": { - "debug": "2.2.0", - "object-assign": "4.1.1", - "resolve": "1.3.3" - } - }, - "eslint-module-utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", - "integrity": "sha1-q67IJBd2E7ipWymWOeG2+s9HNEk=", - "dev": true, - "requires": { - "debug": "2.6.8", - "pkg-dir": "1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-plugin-import": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.6.0.tgz", - "integrity": "sha1-Kku602oHjgUqPIMM49+9a4oSxuU=", - "dev": true, - "requires": { - "builtin-modules": "1.1.1", - "contains-path": "0.1.0", - "debug": "2.6.8", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "0.2.3", - "eslint-module-utils": "2.1.1", - "has": "1.0.1", - "lodash.cond": "4.5.2", - "minimatch": "3.0.4", - "read-pkg-up": "2.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "2.0.2", - "isarray": "1.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-5.1.0.tgz", - "integrity": "sha1-SoKWNDROepA5Gp+w+9GYEHN9ecU=", - "dev": true, - "requires": { - "aria-query": "0.5.0", - "array-includes": "3.0.3", - "ast-types-flow": "0.0.7", - "axobject-query": "0.1.0", - "damerau-levenshtein": "1.0.4", - "emoji-regex": "6.4.2", - "jsx-ast-utils": "1.4.1" - } - }, - "eslint-plugin-react": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.1.0.tgz", - "integrity": "sha1-J3cKzzn1/UnNCvQIPOWBBOs5DUw=", - "dev": true, - "requires": { - "doctrine": "2.0.0", - "has": "1.0.1", - "jsx-ast-utils": "1.4.1" - } - }, - "espree": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz", - "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=", - "dev": true, - "requires": { - "acorn": "5.0.3", - "acorn-jsx": "3.0.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "esquery": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", - "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", - "dev": true, - "requires": { - "estraverse": "4.2.0" - } - }, - "esrecurse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", - "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", - "dev": true, - "requires": { - "estraverse": "4.2.0", - "object-assign": "4.1.1" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23" - } - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "1.2.2", - "object-assign": "4.1.1" - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" - } - }, - "flat-cache": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", - "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", - "dev": true, - "requires": { - "circular-json": "0.3.1", - "del": "2.2.2", - "graceful-fs": "4.1.11", - "write": "0.2.1" - }, - "dependencies": { - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "5.0.0", - "is-path-cwd": "1.0.0", - "is-path-in-cwd": "1.0.0", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "rimraf": "2.6.1" - } - }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "7.1.2", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - } - } - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "function-bind": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", - "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=", - "dev": true - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "1.0.2" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=", - "dev": true - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "dev": true, - "requires": { - "function-bind": "1.1.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "hosted-git-info": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=", - "dev": true - }, - "ignore": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", - "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "1.4.0", - "ansi-regex": "2.1.1", - "chalk": "1.1.3", - "cli-cursor": "1.0.2", - "cli-width": "2.1.0", - "figures": "1.7.0", - "lodash": "4.17.2", - "readline2": "1.0.1", - "run-async": "0.1.0", - "rx-lite": "3.1.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "through": "2.3.8" - } - }, - "interpret": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", - "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "1.1.1" - } - }, - "is-callable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", - "dev": true - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-my-json-valid": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", - "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", - "dev": true, - "requires": { - "generate-function": "2.0.0", - "generate-object-property": "1.2.0", - "jsonpointer": "4.0.1", - "xtend": "4.0.1" - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", - "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", - "dev": true, - "requires": { - "is-path-inside": "1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", - "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", - "dev": true, - "requires": { - "path-is-inside": "1.0.2" - } - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "1.0.1" - } - }, - "is-resolvable": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", - "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", - "dev": true, - "requires": { - "tryit": "1.0.3" - } - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "js-tokens": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", - "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=", - "dev": true - }, - "js-yaml": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz", - "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=", - "dev": true, - "requires": { - "argparse": "1.0.9", - "esprima": "3.1.3" - } - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsx-ast-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", - "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "strip-bom": "3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } - } - }, - "lodash": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.2.tgz", - "integrity": "sha1-NKMFW6vgTOQkZ7YH1wAHLH/2v0I=", - "dev": true - }, - "lodash.cond": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", - "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "mocha": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.1.2.tgz", - "integrity": "sha1-Ufk7Qyv34bF1/8Iog8zQvjLbprU=", - "dev": true, - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.9.0", - "debug": "2.2.0", - "diff": "1.4.0", - "escape-string-regexp": "1.0.5", - "glob": "7.0.5", - "growl": "1.9.2", - "json3": "3.3.2", - "lodash.create": "3.1.1", - "mkdirp": "0.5.1", - "supports-color": "3.1.2" - }, - "dependencies": { - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true - }, - "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": "1.0.1" - }, - "dependencies": { - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - } - } - }, - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true, - "requires": { - "ms": "0.7.1" - }, - "dependencies": { - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true - } - } - }, - "diff": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", - "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "glob": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", - "integrity": "sha1-tCAqaQmbu00pKnwblbZoK2fr3JU=", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.3", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - }, - "dependencies": { - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - }, - "dependencies": { - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", - "dev": true, - "requires": { - "brace-expansion": "1.1.6" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", - "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - }, - "dependencies": { - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - } - } - } - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - }, - "dependencies": { - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - } - } - }, - "growl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", - "dev": true - }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, - "lodash.create": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", - "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", - "dev": true, - "requires": { - "lodash._baseassign": "3.2.0", - "lodash._basecreate": "3.0.3", - "lodash._isiterateecall": "3.0.9" - }, - "dependencies": { - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "3.0.1", - "lodash.keys": "3.1.2" - }, - "dependencies": { - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" - }, - "dependencies": { - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - } - } - } - } - }, - "lodash._basecreate": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", - "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "supports-color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", - "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", - "dev": true, - "requires": { - "has-flag": "1.0.0" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - } - } - } - } - }, - "mock-require": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-1.3.0.tgz", - "integrity": "sha1-gmFElS5QR2L45pJKqPY5Rl0deiQ=", - "dev": true, - "requires": { - "caller-id": "0.1.0" - }, - "dependencies": { - "caller-id": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-id/-/caller-id-0.1.0.tgz", - "integrity": "sha1-Wb2sCJPRLDhxQIJ5Ix+XRYNk8Hs=", - "dev": true, - "requires": { - "stack-trace": "0.0.9" - }, - "dependencies": { - "stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=", - "dev": true - } - } - } - } - }, - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", - "dev": true, - "requires": { - "hosted-git-info": "2.5.0", - "is-builtin-module": "1.0.0", - "semver": "5.3.0", - "validate-npm-package-license": "3.0.1" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "p-limit": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", - "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", - "dev": true - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "1.1.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "1.3.1" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "2.0.1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "2.3.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "2.0.4" - } - }, - "pkg-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", - "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "dev": true, - "requires": { - "find-up": "1.1.2" - } - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "2.0.0", - "normalize-package-data": "2.4.0", - "path-type": "2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "2.1.0", - "read-pkg": "2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - } - } - }, - "readable-stream": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.6.tgz", - "integrity": "sha1-i0Ou125xSDk40SqNRsbPGgCx+BY=", - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "mute-stream": "0.0.5" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "1.3.3" - } - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "0.1.0", - "resolve-from": "1.0.1" - }, - "dependencies": { - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - } - } - }, - "resolve": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", - "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", - "dev": true, - "requires": { - "path-parse": "1.0.5" - } - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "1.1.1", - "onetime": "1.1.0" - } - }, - "rimraf": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "1.4.0" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "dev": true - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true - }, - "shelljs": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", - "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", - "dev": true, - "requires": { - "glob": "7.1.2", - "interpret": "1.0.3", - "rechoir": "0.6.2" - } - }, - "sinon": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.6.tgz", - "integrity": "sha1-pDEW21lXfIKWNWr+4T+vwjMuWOE=", - "dev": true, - "requires": { - "formatio": "1.1.1", - "lolex": "1.3.2", - "samsam": "1.1.2", - "util": "0.10.3" - }, - "dependencies": { - "formatio": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", - "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", - "dev": true, - "requires": { - "samsam": "1.1.2" - } - }, - "lolex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", - "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", - "dev": true - }, - "samsam": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", - "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - } - } - } - } - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "spdx-correct": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", - "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", - "dev": true, - "requires": { - "spdx-license-ids": "1.2.2" - } - }, - "spdx-expression-parse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", - "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", - "dev": true - }, - "spdx-license-ids": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", - "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", - "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supertest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-2.0.1.tgz", - "integrity": "sha1-oFgIHXiPFRXUcA11Aogea3WeRM0=", - "dev": true, - "requires": { - "methods": "1.1.2", - "superagent": "2.3.0" - }, - "dependencies": { - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "superagent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz", - "integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "cookiejar": "2.1.0", - "debug": "2.2.0", - "extend": "3.0.0", - "form-data": "1.0.0-rc4", - "formidable": "1.0.17", - "methods": "1.1.2", - "mime": "1.3.4", - "qs": "6.3.0", - "readable-stream": "2.1.5" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "cookiejar": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.0.tgz", - "integrity": "sha1-hlSWiVObbQ4mm2Y3owS+UIGU2Jg=", - "dev": true - }, - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true, - "requires": { - "ms": "0.7.1" - }, - "dependencies": { - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true - } - } - }, - "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", - "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=", - "dev": true - }, - "form-data": { - "version": "1.0.0-rc4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", - "integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=", - "dev": true, - "requires": { - "async": "1.5.2", - "combined-stream": "1.0.5", - "mime-types": "2.1.12" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - }, - "dependencies": { - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - } - } - }, - "mime-types": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.12.tgz", - "integrity": "sha1-FSuiVndwIN1GY/VMLnvCY4HnFyk=", - "dev": true, - "requires": { - "mime-db": "1.24.0" - }, - "dependencies": { - "mime-db": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.24.0.tgz", - "integrity": "sha1-4tE/k58AFsbk6a0lqGUvEmxGfww=", - "dev": true - } - } - } - } - }, - "formidable": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.17.tgz", - "integrity": "sha1-71SRSQ+UM7cF+qdyScmQKa40hVk=", - "dev": true - }, - "mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", - "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=", - "dev": true - }, - "qs": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.0.tgz", - "integrity": "sha1-9AOyZPI7wBIox0ExtAfxjV6l1EI=", - "dev": true - }, - "readable-stream": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", - "integrity": "sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=", - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" - }, - "dependencies": { - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - } - } - } - } - } - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "table": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "4.11.8", - "ajv-keywords": "1.5.1", - "chalk": "1.1.3", - "lodash": "4.17.2", - "slice-ansi": "0.0.4", - "string-width": "2.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.0.tgz", - "integrity": "sha1-AwZkVh/BRslCPsfZeP4kV0N/5tA=", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "tryit": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", - "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "1.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", - "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", - "dev": true, - "requires": { - "spdx-correct": "1.0.2", - "spdx-expression-parse": "1.0.4" - } - }, - "vows": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/vows/-/vows-0.6.4.tgz", - "integrity": "sha1-/w7V1lTQTTl87jLc5DhOqIy08ZM=", - "dev": true, - "requires": { - "diff": "1.0.8", - "eyes": "0.1.8" - }, - "dependencies": { - "diff": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.8.tgz", - "integrity": "sha1-NDJ2MI7Jkbe8giZ+1VvBQR+XFmY=", - "dev": true - }, - "eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", - "dev": true - } - } - }, - "winston": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-0.7.3.tgz", - "integrity": "sha1-euMTunP83C7LSqL5zURugphncmY=", - "dev": true, - "requires": { - "async": "0.2.10", - "colors": "0.6.2", - "cycle": "1.0.3", - "eyes": "0.1.8", - "pkginfo": "0.3.1", - "request": "2.16.6", - "stack-trace": "0.0.9" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", - "dev": true - }, - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", - "dev": true - }, - "cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", - "dev": true - }, - "eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", - "dev": true - }, - "pkginfo": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", - "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=", - "dev": true - }, - "request": { - "version": "2.16.6", - "resolved": "https://registry.npmjs.org/request/-/request-2.16.6.tgz", - "integrity": "sha1-hy/kRa5y3iZrN4edatfclI+gHK0=", - "dev": true, - "requires": { - "aws-sign": "0.2.0", - "cookie-jar": "0.2.0", - "forever-agent": "0.2.0", - "form-data": "0.0.10", - "hawk": "0.10.2", - "json-stringify-safe": "3.0.0", - "mime": "1.2.11", - "node-uuid": "1.4.7", - "oauth-sign": "0.2.0", - "qs": "0.5.6", - "tunnel-agent": "0.2.0" - }, - "dependencies": { - "aws-sign": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/aws-sign/-/aws-sign-0.2.0.tgz", - "integrity": "sha1-xVAThWyBlOyFSgy+yQqrWgTOOsU=", - "dev": true - }, - "cookie-jar": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/cookie-jar/-/cookie-jar-0.2.0.tgz", - "integrity": "sha1-ZOzAasl423leS1KQy+SLo3gUAPo=", - "dev": true - }, - "forever-agent": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.2.0.tgz", - "integrity": "sha1-4cJcetROCcOPIzh2x2/MJP+EOx8=", - "dev": true - }, - "form-data": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.0.10.tgz", - "integrity": "sha1-2zRaU3jYau6x7V1VO4aawZLS9e0=", - "dev": true, - "requires": { - "async": "0.2.10", - "combined-stream": "0.0.7", - "mime": "1.2.11" - }, - "dependencies": { - "combined-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", - "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", - "dev": true, - "requires": { - "delayed-stream": "0.0.5" - }, - "dependencies": { - "delayed-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", - "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=", - "dev": true - } - } - } - } - }, - "hawk": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-0.10.2.tgz", - "integrity": "sha1-mzYd7pWpMWQObVBOBWCaj8OsRdI=", - "dev": true, - "requires": { - "boom": "0.3.8", - "cryptiles": "0.1.3", - "hoek": "0.7.6", - "sntp": "0.1.4" - }, - "dependencies": { - "boom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/boom/-/boom-0.3.8.tgz", - "integrity": "sha1-yM2wQUNZEnQWKMBE7Mcy0dF8Ceo=", - "dev": true, - "requires": { - "hoek": "0.7.6" - } - }, - "cryptiles": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.1.3.tgz", - "integrity": "sha1-GlVnNPBtJLo0hirpy55wmjr7/xw=", - "dev": true, - "requires": { - "boom": "0.3.8" - } - }, - "hoek": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.7.6.tgz", - "integrity": "sha1-YPvZBFV1Qc0rh5Wr8wihs3cOFVo=", - "dev": true - }, - "sntp": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.1.4.tgz", - "integrity": "sha1-XvSBuVGnspr/30r9fyaDj8ESD4Q=", - "dev": true, - "requires": { - "hoek": "0.7.6" - } - } - } - }, - "json-stringify-safe": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-3.0.0.tgz", - "integrity": "sha1-nbew5TDH8onF6MhDKvGRwv91pbM=", - "dev": true - }, - "mime": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", - "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", - "dev": true - }, - "node-uuid": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", - "integrity": "sha1-baWhdmjEs91ZYjvaEc9/pMH2Cm8=", - "dev": true - }, - "oauth-sign": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.2.0.tgz", - "integrity": "sha1-oOahcV2u0GLzIrYit/5a/RA1tuI=", - "dev": true - }, - "qs": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz", - "integrity": "sha1-MbGtBYVnZRxSaSFQa5qHk5EaA4Q=", - "dev": true - }, - "tunnel-agent": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.2.0.tgz", - "integrity": "sha1-aFPCr7GyEJ5FYp5JK9419Fnqaeg=", - "dev": true - } - } - }, - "stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=", - "dev": true - } - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "0.5.1" - } - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - } - } -}